Bug 1486954 - Part I, Encrypt credit card numbers with OS key store. r=MattN
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 17 Oct 2018 02:31:04 +0000
changeset 490741 024dfb5b2d676ee40c45c9fb8b85674b8f6a04e1
parent 490740 69d881a7866706d50389e433904d5946106e42d1
child 490742 21162d81c6d8252f629f537eb2b760f0bd2db8cb
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersMattN
bugs1486954
milestone65.0a1
Bug 1486954 - Part I, Encrypt credit card numbers with OS key store. r=MattN This patch morphs MasterPassword.jsm to OSKeyStore.jsm while keeping the same API, as an adaptor between the API and the native API exposed as nsIOSKeyStore.idl. Noted that OS Key Store has the concept of "recovery phrase" that we won't be adopting here. The recovery phrase, together with our label, allow the user to re-create the same key in OS key store. Test case changes are needed because we have started asking for login in places where we'll only do previously when "master password is enabled". This also made some "when master password is enabled" tests invalid because it is always considered enabled. Some more test changes are needed simply because they previously rely on the stable order of microtask resolutions (and the stable # of promises for a specific operation). That has certainly changed with OSKeyStore. The credit card form autofill is only enabled on Nightly. Differential Revision: https://phabricator.services.mozilla.com/D4498
browser/components/payments/content/paymentDialogWrapper.js
browser/components/payments/test/browser/head.js
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/FormAutofillStorage.jsm
browser/extensions/formautofill/MasterPassword.jsm
browser/extensions/formautofill/OSKeyStore.jsm
browser/extensions/formautofill/content/manageDialog.js
browser/extensions/formautofill/moz.build
browser/extensions/formautofill/test/browser/browser.ini
browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
browser/extensions/formautofill/test/browser/head.js
browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
browser/extensions/formautofill/test/mochitest/test_clear_form.html
browser/extensions/formautofill/test/unit/head.js
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_creditCardRecords.js
browser/extensions/formautofill/test/unit/test_getRecords.js
browser/extensions/formautofill/test/unit/test_masterPassword.js
browser/extensions/formautofill/test/unit/test_osKeyStore.js
browser/extensions/formautofill/test/unit/xpcshell.ini
security/manager/ssl/nsIOSKeyStore.idl
services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
toolkit/modules/CreditCard.jsm
toolkit/modules/tests/browser/browser_CreditCard.js
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -16,18 +16,18 @@ const paymentUISrv = Cc["@mozilla.org/do
                      .getService(Ci.nsIPaymentUIService);
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
                                "resource:///modules/BrowserWindowTracker.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => {
   let storage;
   try {
     storage = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {})
                          .formAutofillStorage;
@@ -163,17 +163,17 @@ var paymentDialogWrapper = {
     let cardData = this.temporaryStore.creditCards.get(guid) ||
                    await formAutofillStorage.creditCards.get(guid);
     if (!cardData) {
       throw new Error(`Basic card not found in storage: ${guid}`);
     }
 
     let cardNumber;
     try {
-      cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
+      cardNumber = await OSKeyStore.decrypt(cardData["cc-number-encrypted"], true);
     } catch (ex) {
       if (ex.result != Cr.NS_ERROR_ABORT) {
         throw ex;
       }
       // User canceled master password entry
       return null;
     }
 
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -16,16 +16,17 @@ const SAVE_ADDRESS_DEFAULT_PREF = "dom.p
 
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
                      .getService(Ci.nsIPaymentUIService).wrappedJSObject;
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
 const {formAutofillStorage} = ChromeUtils.import(
   "resource://formautofill/FormAutofillStorage.jsm", {});
+const {OSKeyStoreTestUtils} = ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", {});
 const {PaymentTestUtils: PTU} = ChromeUtils.import(
   "resource://testing-common/PaymentTestUtils.jsm", {});
 ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
 ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 function getPaymentRequests() {
   return Array.from(paymentSrv.enumerate());
 }
@@ -357,20 +358,22 @@ add_task(async function setup_head() {
       return;
     }
     if (msg.errorMessage == "AbortError: The operation was aborted. " &&
         msg.sourceName == "" && msg.lineNumber == 0) {
       return;
     }
     ok(false, msg.message || msg.errorMessage);
   });
+  OSKeyStoreTestUtils.setup();
   await setupFormAutofillStorage();
-  registerCleanupFunction(function cleanup() {
+  registerCleanupFunction(async function cleanup() {
     paymentSrv.cleanup();
     cleanupFormAutofillStorage();
+    await OSKeyStoreTestUtils.cleanup();
     Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
     Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
     Services.prefs.clearUserPref(SAVE_ADDRESS_DEFAULT_PREF);
     SpecialPowers.postConsoleSentinel();
     // CreditCard.jsm is imported into the global scope. It needs to be deleted
     // else it outlives the test and is reported as a leak.
     delete window.CreditCard;
   });
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -37,17 +37,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   CreditCard: "resource://gre/modules/CreditCard.jsm",
   FormAutofillPreferences: "resource://formautofill/FormAutofillPreferences.jsm",
   FormAutofillDoorhanger: "resource://formautofill/FormAutofillDoorhanger.jsm",
   FormAutofillUtils: "resource://formautofill/FormAutofillUtils.jsm",
-  MasterPassword: "resource://formautofill/MasterPassword.jsm",
+  OSKeyStore: "resource://formautofill/OSKeyStore.jsm",
 });
 
 this.log = null;
 FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
 
 const {
   ENABLED_AUTOFILL_ADDRESSES_PREF,
   ENABLED_AUTOFILL_CREDITCARDS_PREF,
@@ -220,18 +220,18 @@ FormAutofillParent.prototype = {
         if (data.guid) {
           await this.formAutofillStorage.addresses.update(data.guid, data.address);
         } else {
           await this.formAutofillStorage.addresses.add(data.address);
         }
         break;
       }
       case "FormAutofill:SaveCreditCard": {
-        if (!await MasterPassword.ensureLoggedIn()) {
-          log.warn("User canceled master password entry");
+        if (!await OSKeyStore.ensureLoggedIn()) {
+          log.warn("User canceled encryption login");
           return;
         }
         await this.formAutofillStorage.creditCards.add(data.creditcard);
         break;
       }
       case "FormAutofill:RemoveAddresses": {
         data.guids.forEach(guid => this.formAutofillStorage.addresses.remove(guid));
         break;
@@ -248,22 +248,22 @@ FormAutofillParent.prototype = {
         const win = BrowserWindowTracker.getTopWindow();
         win.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
         break;
       }
       case "FormAutofill:GetDecryptedString": {
         let {cipherText, reauth} = data;
         let string;
         try {
-          string = await MasterPassword.decrypt(cipherText, reauth);
+          string = await OSKeyStore.decrypt(cipherText, reauth);
         } catch (e) {
           if (e.result != Cr.NS_ERROR_ABORT) {
             throw e;
           }
-          log.warn("User canceled master password entry");
+          log.warn("User canceled encryption login");
         }
         target.sendAsyncMessage("FormAutofill:DecryptedString", string);
         break;
       }
     }
   },
 
   /**
@@ -287,17 +287,17 @@ FormAutofillParent.prototype = {
       Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this);
       Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
     }
   },
 
   /**
    * Get the records from profile store and return results back to content
    * process. It will decrypt the credit card number and append
-   * "cc-number-decrypted" to each record if MasterPassword isn't set.
+   * "cc-number-decrypted" to each record if OSKeyStore isn't set.
    *
    * @private
    * @param  {string} data.collectionName
    *         The name used to specify which collection to retrieve records.
    * @param  {string} data.searchString
    *         The typed string for filtering out the matched records.
    * @param  {string} data.info
    *         The input autocomplete property's information.
@@ -312,37 +312,37 @@ FormAutofillParent.prototype = {
     }
 
     let recordsInCollection = await collection.getAll();
     if (!info || !info.fieldName || !recordsInCollection.length) {
       target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
       return;
     }
 
-    let isCCAndMPEnabled = collectionName == CREDITCARDS_COLLECTION_NAME && MasterPassword.isEnabled;
-    // We don't filter "cc-number" when MasterPassword is set.
+    let isCCAndMPEnabled = collectionName == CREDITCARDS_COLLECTION_NAME && OSKeyStore.isEnabled;
+    // We don't filter "cc-number" when OSKeyStore is set.
     if (isCCAndMPEnabled && info.fieldName == "cc-number") {
       recordsInCollection = recordsInCollection.filter(record => !!record["cc-number"]);
       target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
       return;
     }
 
     let records = [];
     let lcSearchString = searchString.toLowerCase();
 
     for (let record of recordsInCollection) {
       let fieldValue = record[info.fieldName];
       if (!fieldValue) {
         continue;
       }
 
       // Cache the decrypted "cc-number" in each record for content to preview
-      // when MasterPassword isn't set.
+      // when OSKeyStore isn't set.
       if (!isCCAndMPEnabled && record["cc-number-encrypted"]) {
-        record["cc-number-decrypted"] = await MasterPassword.decrypt(record["cc-number-encrypted"]);
+        record["cc-number-decrypted"] = await OSKeyStore.decrypt(record["cc-number-encrypted"]);
       }
 
       // Filter "cc-number" based on the decrypted one.
       if (info.fieldName == "cc-number") {
         fieldValue = record["cc-number-decrypted"];
       }
 
       if (collectionName == ADDRESSES_COLLECTION_NAME && record.country
@@ -533,18 +533,18 @@ FormAutofillParent.prototype = {
         return;
       }
 
       if (state == "disable") {
         Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false);
         return;
       }
 
-      if (!await MasterPassword.ensureLoggedIn()) {
-        log.warn("User canceled master password entry");
+      if (!await OSKeyStore.ensureLoggedIn()) {
+        log.warn("User canceled encryption login");
         return;
       }
 
       let changedGUIDs = [];
       if (creditCard.guid) {
         if (state == "update") {
           await this.formAutofillStorage.creditCards.update(creditCard.guid, creditCard.record, true);
           changedGUIDs.push(creditCard.guid);
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -137,18 +137,18 @@ ChromeUtils.import("resource://formautof
 ChromeUtils.defineModuleGetter(this, "CreditCard",
                                "resource://gre/modules/CreditCard.jsm");
 ChromeUtils.defineModuleGetter(this, "JSONFile",
                                "resource://gre/modules/JSONFile.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillNameUtils",
                                "resource://formautofill/FormAutofillNameUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.defineModuleGetter(this, "PhoneNumber",
                                "resource://formautofill/phonenumberutils/PhoneNumber.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
@@ -1599,28 +1599,28 @@ class CreditCards extends AutofillRecord
       hasNewComputedFields = true;
     }
 
     // Encrypt credit card number
     if (!("cc-number-encrypted" in creditCard)) {
       if ("cc-number" in creditCard) {
         let ccNumber = creditCard["cc-number"];
         creditCard["cc-number"] = CreditCard.getLongMaskedNumber(ccNumber);
-        creditCard["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+        creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
       } else {
         creditCard["cc-number-encrypted"] = "";
       }
     }
 
     return hasNewComputedFields;
   }
 
   async _stripComputedFields(creditCard) {
     if (creditCard["cc-number-encrypted"]) {
-      creditCard["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+      creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
     }
     await super._stripComputedFields(creditCard);
   }
 
   _normalizeFields(creditCard) {
     this._normalizeCCName(creditCard);
     this._normalizeCCNumber(creditCard);
     this._normalizeCCExpirationDate(creditCard);
@@ -1687,22 +1687,22 @@ class CreditCards extends AutofillRecord
     let clonedTargetCreditCard = this._clone(targetCreditCard);
     this._normalizeRecord(clonedTargetCreditCard);
     for (let creditCard of this._data) {
       let isDuplicate = await Promise.all(this.VALID_FIELDS.map(async field => {
         if (!clonedTargetCreditCard[field]) {
           return !creditCard[field];
         }
         if (field == "cc-number" && creditCard[field]) {
-          if (MasterPassword.isEnabled) {
-            // Compare the masked numbers instead when the master password is
-            // enabled because we don't want to leak the credit card number.
+          if (OSKeyStore.isEnabled) {
+            // Compare the masked numbers instead when decryption requires a password
+            // because we don't want to leak the credit card number.
             return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
           }
-          return (clonedTargetCreditCard[field] == await MasterPassword.decrypt(creditCard["cc-number-encrypted"]));
+          return (clonedTargetCreditCard[field] == await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]));
         }
         return clonedTargetCreditCard[field] == creditCard[field];
       })).then(fieldResults => fieldResults.every(result => result));
       if (isDuplicate) {
         return creditCard.guid;
       }
     }
     return null;
rename from browser/extensions/formautofill/MasterPassword.jsm
rename to browser/extensions/formautofill/OSKeyStore.jsm
--- a/browser/extensions/formautofill/MasterPassword.jsm
+++ b/browser/extensions/formautofill/OSKeyStore.jsm
@@ -1,184 +1,262 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
- * Helpers for the Master Password Dialog.
- * In the future the Master Password implementation may move here.
+ * Helpers for using OS Key Store.
  */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
-  "MasterPassword",
+  "OSKeyStore",
 ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
-                                   "@mozilla.org/login-manager/crypto/SDR;1",
-                                   Ci.nsILoginManagerCrypto);
+ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
+                                   "@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
+
+// Skip reauth during tests, only works in non-official builds.
+const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
+
+var OSKeyStore = {
+  /**
+   * On macOS this becomes part of the name label visible on Keychain Acesss as
+   * "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
+   */
+  STORE_LABEL: AppConstants.MOZ_APP_NAME,
 
-var MasterPassword = {
-  get _token() {
-    let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
-    return tokendb.getInternalKeyToken();
+  /**
+   * Consider the module is initialized as locked. OS might unlock without a
+   * prompt.
+   * @type {Boolean}
+   */
+  _isLocked: true,
+
+  _pendingUnlockPromise: null,
+
+  /**
+   * @returns {boolean} Always considered enabled because OS store is always
+   *                    protected via OS user login password.
+   *                    TODO: Figure out if the affacted behaviors
+   *                    (e.g. like bug 1486954 or confirming payment transaction)
+   *                    is correct or not.
+   */
+  get isEnabled() {
+    return true;
   },
 
   /**
-   * @returns {boolean} True if a master password is set and false otherwise.
+   * @returns {boolean} True if logged in (i.e. decrypt(reauth = false) will
+   *                    not retrigger a dialog) and false if not.
+   *                    User might log out elsewhere in the OS, so even if this
+   *                    is true a prompt might still pop up.
    */
-  get isEnabled() {
-    return this._token.hasPassword;
+  get isLoggedIn() {
+    return !this._isLocked;
   },
 
   /**
-   * @returns {boolean} True if master password is logged in and false if not.
+   * @returns {boolean} True if there is another login dialog existing and false
+   *                    otherwise.
    */
-  get isLoggedIn() {
-    return Services.logins.isLoggedIn;
+  get isUIBusy() {
+    return !!this._pendingUnlockPromise;
   },
 
   /**
-   * @returns {boolean} True if there is another master password login dialog
-   *                    existing and false otherwise.
+   * If the test pref exist and applicable,
+   * this method will dispatch a observer message and return
+   * to simulate successful reauth, or throw to simulate
+   * failed reauth.
+   *
+   * @returns {boolean} True when reauth should NOT be skipped,
+   *                    false when reauth has been skipped.
+   * @throws            If it needs to simulate reauth login failure.
    */
-  get isUIBusy() {
-    return Services.logins.uiBusy;
+  _maybeSkipReauthForTest() {
+    // Don't take test reauth pref in the following configurations.
+    if (nativeOSKeyStore.isNSSKeyStore ||
+        AppConstants.MOZILLA_OFFICIAL ||
+        !this._testReauth) {
+      return true;
+    }
+
+    // Skip this reauth because there is no way to mock the
+    // native dialog in the testing environment, for now.
+    log.debug("_ensureReauth: _testReauth: ", this._testReauth);
+    switch (this._testReauth) {
+      case "pass":
+        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
+        return false;
+      case "cancel":
+        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
+        throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
+      default:
+        throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
+    }
   },
 
   /**
-   * Ensure the master password is logged in. It will display the master password
-   * login prompt or do nothing if it's logged in already. If an existing MP
+   * Ensure the store in use is logged in. It will display the OS login
+   * login prompt or do nothing if it's logged in already. If an existing login
    * prompt is already prompted, the result from it will be used instead.
    *
+   * Note: This method must set _pendingUnlockPromise before returning the
+   * promise (i.e. the first |await|), otherwise we'll risk re-entry.
+   * This is why there aren't an |await| in the method. The method is marked as
+   * |async| to communicate that it's async.
+   *
    * @param   {boolean} reauth Prompt the login dialog no matter it's logged in
    *                           or not if it's set to true.
    * @returns {Promise<boolean>} True if it's logged in or no password is set
    *                             and false if it's still not logged in (prompt
    *                             canceled or other error).
    */
   async ensureLoggedIn(reauth = false) {
-    if (!this.isEnabled) {
-      return true;
+    if (this._pendingUnlockPromise) {
+      log.debug("ensureLoggedIn: Has a pending unlock operation");
+      return this._pendingUnlockPromise;
     }
+    log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);
+
+    // TODO: Implementing re-auth by passing this value to the native implementation
+    // in some way. Set this to false for now to ignore the reauth request (bug 1429265).
+    reauth = false;
+
+    let unlockPromise = Promise.resolve().then(async () => {
+      if (reauth) {
+        reauth = this._maybeSkipReauthForTest();
+      }
 
-    if (this.isLoggedIn && !reauth) {
-      return true;
-    }
+      if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
+        log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
+        let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
+        // TODO We should somehow have a dialog to ask the user to write this down,
+        // and another dialog somewhere for the user to restore the secret with it.
+        // (Intentionally not printing it out in the console)
+        log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
+      }
+    });
 
-    // If a prompt is already showing then wait for and focus it.
-    if (this.isUIBusy) {
-      return this.waitForExistingDialog();
+    if (nativeOSKeyStore.isNSSKeyStore) {
+      // Workaround bug 1492305: NSS-implemented methods don't reject when user cancels.
+      unlockPromise = unlockPromise.then(() => {
+        log.debug("ensureLoggedIn: isNSSKeyStore: ", reauth, Services.logins.isLoggedIn);
+        // User has hit the cancel button on the master password prompt.
+        // We must reject the promise chain here.
+        if (!Services.logins.isLoggedIn) {
+          throw Components.Exception("User canceled OS unlock entry (Workaround)", Cr.NS_ERROR_FAILURE);
+        }
+      });
     }
 
-    let token = this._token;
-    try {
-      // 'true' means always prompt for token password. User will be prompted until
-      // clicking 'Cancel' or entering the correct password.
-      token.login(true);
-    } catch (e) {
-      // An exception will be thrown if the user cancels the login prompt dialog.
-      // User is also logged out.
-    }
+    unlockPromise = unlockPromise.then(() => {
+      log.debug("ensureLoggedIn: Logged in");
+      this._pendingUnlockPromise = null;
+      this._isLocked = false;
 
-    // If we triggered a master password prompt, notify observers.
-    if (token.isLoggedIn()) {
-      Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
-    } else {
-      Services.obs.notifyObservers(null, "passwordmgr-crypto-loginCanceled");
-    }
+      return true;
+    }, (err) => {
+      log.debug("ensureLoggedIn: Not logged in", err);
+      this._pendingUnlockPromise = null;
+      this._isLocked = true;
 
-    return token.isLoggedIn();
+      return false;
+    });
+
+    this._pendingUnlockPromise = unlockPromise;
+
+    return this._pendingUnlockPromise;
   },
 
   /**
    * Decrypts cipherText.
    *
+   * Note: In the event of an rejection, check the result property of the Exception
+   *       object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
+   *       don't show that dialog), apart from other errors (e.g., gracefully
+   *       recover from that and still shows the dialog.)
+   *
    * @param   {string} cipherText Encrypted string including the algorithm details.
    * @param   {boolean} reauth True if we want to force the prompt to show up
    *                    even if the user is already logged in.
    * @returns {Promise<string>} resolves to the decrypted string, or rejects otherwise.
    */
   async decrypt(cipherText, reauth = false) {
     if (!await this.ensureLoggedIn(reauth)) {
-      throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
     }
-    return cryptoSDR.decrypt(cipherText);
+    let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
+    return String.fromCharCode.apply(String, bytes);
   },
 
   /**
    * Encrypts a string and returns cipher text containing algorithm information used for decryption.
    *
    * @param   {string} plainText Original string without encryption.
    * @returns {Promise<string>} resolves to the encrypted string (with algorithm), otherwise rejects.
    */
   async encrypt(plainText) {
     if (!await this.ensureLoggedIn()) {
-      throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
     }
 
-    return cryptoSDR.encrypt(plainText);
+    // Convert plain text into a UTF-8 binary string
+    plainText = unescape(encodeURIComponent(plainText));
+
+    // Convert it to an array
+    let textArr = [];
+    for (let char of plainText) {
+      textArr.push(char.charCodeAt(0));
+    }
+
+    let rawEncryptedText = await nativeOSKeyStore.asyncEncryptBytes(this.STORE_LABEL, textArr.length, textArr);
+
+    // Mark the output with a version number.
+    return rawEncryptedText;
   },
 
   /**
-   * Resolve when master password dialogs are closed, immediately if none are open.
+   * Resolve when the login dialogs are closed, immediately if none are open.
    *
    * An existing MP dialog will be focused and will request attention.
    *
    * @returns {Promise<boolean>}
    *          Resolves with whether the user is logged in to MP.
    */
   async waitForExistingDialog() {
-    if (!this.isUIBusy) {
-      log.debug("waitForExistingDialog: Dialog isn't showing. isLoggedIn:", this.isLoggedIn);
-      return this.isLoggedIn;
+    if (this.isUIBusy) {
+      return this._pendingUnlockPromise;
     }
-
-    return new Promise((resolve) => {
-      log.debug("waitForExistingDialog: Observing the open dialog");
-      let observer = {
-        QueryInterface: ChromeUtils.generateQI([
-          Ci.nsIObserver,
-          Ci.nsISupportsWeakReference,
-        ]),
+    return this.isLoggedIn;
+  },
 
-        observe(subject, topic, data) {
-          log.debug("waitForExistingDialog: Got notification:", topic);
-          // Only run observer once.
-          Services.obs.removeObserver(this, "passwordmgr-crypto-login");
-          Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
-          if (topic == "passwordmgr-crypto-loginCanceled") {
-            resolve(false);
-            return;
-          }
-
-          resolve(true);
-        },
-      };
+  /**
+   * Remove the store. For tests.
+   */
+  async cleanup() {
+    return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
+  },
 
-      // Possible leak: it's possible that neither of these notifications
-      // will fire, and if that happens, we'll leak the observer (and
-      // never return). We should guarantee that at least one of these
-      // will fire.
-      // See bug XXX.
-      Services.obs.addObserver(observer, "passwordmgr-crypto-login");
-      Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled");
-
-      // Focus and draw attention to the existing master password dialog for the
-      // occassions where it's not attached to the current window.
-      let promptWin = Services.wm.getMostRecentWindow("prompt:promptPassword");
-      promptWin.focus();
-      promptWin.getAttention();
-    });
+  /**
+   * Check if the implementation is using the NSS key store.
+   * If so, tests will be able to handle the reauth dialog.
+   */
+  get isNSSKeyStore() {
+    return nativeOSKeyStore.isNSSKeyStore;
   },
 };
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
   return new ConsoleAPI({
-    maxLogLevelPref: "masterPassword.loglevel",
-    prefix: "Master Password",
+    maxLogLevelPref: "extensions.formautofill.loglevel",
+    prefix: "OSKeyStore",
   });
 });
+
+XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_testReauth", TEST_ONLY_REAUTH, "");
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -14,18 +14,18 @@ ChromeUtils.import("resource://formautof
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 ChromeUtils.defineModuleGetter(this, "CreditCard",
                                "resource://gre/modules/CreditCard.jsm");
 ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                                "resource://formautofill/FormAutofillStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 
 this.log = null;
 FormAutofill.defineLazyLogGetter(this, "manageAddresses");
 
 class ManageRecords {
   constructor(subStorageName, elements) {
     this._storageInitPromise = formAutofillStorage.initialize();
     this._subStorageName = subStorageName;
@@ -312,35 +312,34 @@ class ManageAddresses extends ManageReco
 }
 
 class ManageCreditCards extends ManageRecords {
   constructor(elements) {
     super("creditCards", elements);
     elements.add.setAttribute("searchkeywords", FormAutofillUtils.EDIT_CREDITCARD_KEYWORDS
                                                   .map(key => FormAutofillUtils.stringBundle.GetStringFromName(key))
                                                   .join("\n"));
-    this._hasMasterPassword = MasterPassword.isEnabled;
+    this._hasOSKeyStore = OSKeyStore.isEnabled;
     this._isDecrypted = false;
-    if (this._hasMasterPassword) {
+    if (this._hasOSKeyStore) {
       elements.showHideCreditCards.setAttribute("hidden", true);
     }
   }
 
   /**
    * Open the edit address dialog to create/edit a credit card.
    *
    * @param  {object} creditCard [optional]
    */
   async openEditDialog(creditCard) {
-    // If master password is set, ask for password if user is trying to edit an
-    // existing credit card.
-    if (!creditCard || !this._hasMasterPassword || await MasterPassword.ensureLoggedIn(true)) {
+    // Ask for reauth if user is trying to edit an existing credit card.
+    if (!creditCard || !this._hasOSKeyStore || await OSKeyStore.ensureLoggedIn(true)) {
       let decryptedCCNumObj = {};
       if (creditCard) {
-        decryptedCCNumObj["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+        decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
       }
       let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
       this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, "resizable=no", {
         record: decryptedCreditCard,
       });
     }
   }
 
--- a/browser/extensions/formautofill/moz.build
+++ b/browser/extensions/formautofill/moz.build
@@ -27,16 +27,18 @@ elif CONFIG['OS_ARCH'] == 'Darwin':
         'skin/osx/editDialog.css',
     ]
 elif CONFIG['OS_ARCH'] == 'WINNT':
     FINAL_TARGET_FILES.features['formautofill@mozilla.org'].chrome.res += [
         'skin/windows/autocomplete-item.css',
         'skin/windows/editDialog.css',
     ]
 
+TESTING_JS_MODULES += ['test/fixtures/OSKeyStoreTestUtils.jsm']
+
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
--- a/browser/extensions/formautofill/test/browser/browser.ini
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -10,17 +10,18 @@ support-files =
 [browser_autocomplete_footer.js]
 skip-if = verify
 [browser_autocomplete_marked_back_forward.js]
 [browser_autocomplete_marked_detached_tab.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_check_installed.js]
 [browser_creditCard_doorhanger.js]
 skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1425884
-[browser_creditCard_fill_master_password.js]
+[browser_creditCard_fill_cancel_login.js]
+skip-if = true # Re-auth is not implemented, cannot cancel OS key store login (bug 1429265)
 [browser_dropdown_layout.js]
 [browser_editAddressDialog.js]
 [browser_editCreditCardDialog.js]
 skip-if = (verify && (os == 'linux'))
 [browser_first_time_use_doorhanger.js]
 skip-if = verify
 [browser_insecure_form.js]
 skip-if = (os == 'linux' && !debug) || (os == 'win') # bug 1456284
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
@@ -46,16 +46,17 @@ add_task(async function test_submit_cred
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
+      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
@@ -64,54 +65,66 @@ add_task(async function test_submit_cred
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
+      await onChanged;
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
   is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_untouched_creditCard_form() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+      await osKeyStoreLoginShown;
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
@@ -120,16 +133,19 @@ add_task(async function test_submit_chan
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
 
@@ -144,16 +160,17 @@ add_task(async function test_submit_chan
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card in storage");
   is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"], "name field still exists");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
@@ -269,88 +286,16 @@ add_task(async function test_submit_cred
   let creditCardPref = SpecialPowers.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
   is(creditCards.length, 0, "No credit card in storage");
   is(creditCardPref, false, "Credit card is disabled");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
 });
 
-add_task(async function test_submit_creditCard_saved_with_mp_enabled() {
-  LoginTestUtils.masterPassword.enable();
-  // Login with the masterPassword in LoginTestUtils.
-  let masterPasswordDialogShown = waitForMasterPasswordDialog(true);
-  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
-    async function(browser) {
-      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                       "popupshown");
-      await ContentTask.spawn(browser, null, async function() {
-        let form = content.document.getElementById("form");
-        let name = form.querySelector("#cc-name");
-        name.focus();
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        name.setUserInput("User 0");
-
-        let number = form.querySelector("#cc-number");
-        number.setUserInput("6387060366272981");
-
-        // Wait 1000ms before submission to make sure the input value applied
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        form.querySelector("input[type=submit]").click();
-      });
-
-      await promiseShown;
-      await clickDoorhangerButton(MAIN_BUTTON);
-      await masterPasswordDialogShown;
-      await TestUtils.topicObserved("formautofill-storage-changed");
-    }
-  );
-
-  let creditCards = await getCreditCards();
-  is(creditCards.length, 1, "1 credit card in storage");
-  is(creditCards[0]["cc-name"], "User 0", "Verify the name field");
-  is(creditCards[0]["cc-number"], "************2981", "Verify the card number field");
-  LoginTestUtils.masterPassword.disable();
-  await removeAllRecords();
-});
-
-add_task(async function test_submit_creditCard_saved_with_mp_enabled_but_canceled() {
-  LoginTestUtils.masterPassword.enable();
-  let masterPasswordDialogShown = waitForMasterPasswordDialog();
-  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
-    async function(browser) {
-      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                       "popupshown");
-      await ContentTask.spawn(browser, null, async function() {
-        let form = content.document.getElementById("form");
-        let name = form.querySelector("#cc-name");
-        name.focus();
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        name.setUserInput("User 2");
-
-        let number = form.querySelector("#cc-number");
-        number.setUserInput("5471839082338112");
-
-        // Wait 1000ms before submission to make sure the input value applied
-        await new Promise(resolve => setTimeout(resolve, 1000));
-        form.querySelector("input[type=submit]").click();
-      });
-
-      await promiseShown;
-      await clickDoorhangerButton(MAIN_BUTTON);
-      await masterPasswordDialogShown;
-    }
-  );
-
-  await sleep(1000);
-  let creditCards = await getCreditCards();
-  is(creditCards.length, 0, "No credit cards in storage");
-  LoginTestUtils.masterPassword.disable();
-});
-
 add_task(async function test_submit_creditCard_with_sync_account() {
   await SpecialPowers.pushPrefEnv({
     "set": [
       [SYNC_USERNAME_PREF, "foo@bar.com"],
       [SYNC_CREDITCARDS_AVAILABLE_PREF, true],
     ],
   });
 
@@ -440,16 +385,18 @@ add_task(async function test_submit_manu
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_3);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
@@ -462,195 +409,255 @@ add_task(async function test_submit_manu
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 3", "Verify the name field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_autofill_form_name() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+      await osKeyStoreLoginShown;
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let name = form.querySelector("#cc-name");
+          return name.value == "John Doe";
+        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.setUserInput("User 1");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
-
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0]["cc-name"], "User 1", "cc-name field is updated");
   is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_autofill_form_exp_date() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let name = form.querySelector("#cc-name");
+          return name.value == "John Doe";
+        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let year = form.querySelector("#cc-exp-year");
         year.setUserInput("2020");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
+      await osKeyStoreLoginShown;
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 1, "Still 1 credit card");
   is(creditCards[0]["cc-exp-year"], "2020", "cc-exp-year field is updated");
   is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_create_new_autofill_form() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
+      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await openPopupOn(browser, "form #cc-name");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let name = form.querySelector("#cc-name");
+          return name.value == "John Doe";
+        }, "Credit card detail never fills");
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.setUserInput("User 1");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(SECONDARY_BUTTON);
+      await osKeyStoreLoginShown;
+      await onChanged;
     }
   );
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "2 credit cards in storage");
   is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"],
      "Original record's cc-name field is unchanged");
   is(creditCards[1]["cc-name"], "User 1", "cc-name field in the new record");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_update_duplicate_autofill_form() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await SpecialPowers.pushPrefEnv({
     "set": [
       [CREDITCARDS_USED_STATUS_PREF, 0],
     ],
   });
   await saveCreditCard({
     "cc-number": "6387060366272981",
   });
   await saveCreditCard({
     "cc-number": "5038146897157463",
   });
   let creditCards = await getCreditCards();
   is(creditCards.length, 2, "2 credit card in storage");
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+  let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+                                       (subject, data) => data == "notifyUsed");
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "form #cc-number");
       await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
       await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
       await ContentTask.spawn(browser, null, async function() {
+        await ContentTaskUtils.waitForCondition(() => {
+          let form = content.document.getElementById("form");
+          let number = form.querySelector("#cc-number");
+          return number.value == "6387060366272981";
+        }, "Should be the first credit card number");
+
+        // Change number to the second credit card number
         let form = content.document.getElementById("form");
         let number = form.querySelector("#cc-number");
-        is(number.value, "6387060366272981", "Should be the first credit card number");
-        // Change number to the second credit card number
         number.setUserInput("5038146897157463");
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+      await osKeyStoreLoginShown;
     }
   );
+  await onUsed;
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "Still 2 credit card");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 1,
     "User neither sees the doorhanger nor uses autofill but somehow has a record in the storage");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_creditCard_with_invalid_network() {
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
                                                        "popupshown");
+      let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
       await ContentTask.spawn(browser, null, async function() {
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
@@ -659,16 +666,17 @@ add_task(async function test_submit_cred
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
+      await onChanged;
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
   is(creditCards[0]["cc-type"], undefined, "Invalid network/cc-type was not saved");
 
rename from browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
rename to browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
@@ -1,25 +1,25 @@
 "use strict";
 
-add_task(async function test_fill_creditCard_with_mp_enabled_but_canceled() {
+add_task(async function test_fill_creditCard_but_cancel_login() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await saveCreditCard(TEST_CREDIT_CARD_2);
 
-  LoginTestUtils.masterPassword.enable();
-  registerCleanupFunction(() => {
-    LoginTestUtils.masterPassword.disable();
-  });
-
-  let masterPasswordDialogShown = waitForMasterPasswordDialog(false); // cancel
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); // cancel
   await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
     async function(browser) {
       await openPopupOn(browser, "#cc-name");
       const ccItem = getDisplayedPopupItems(browser)[0];
       await EventUtils.synthesizeMouseAtCenter(ccItem, {});
-      await Promise.all([masterPasswordDialogShown, expectPopupClose(browser)]);
+      await Promise.all([osKeyStoreLoginShown, expectPopupClose(browser)]);
 
       await ContentTask.spawn(browser, {}, async function() {
         is(content.document.querySelector("#cc-name").value, "", "Check name");
         is(content.document.querySelector("#cc-number").value, "", "Check number");
       });
     }
   );
 });
--- a/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
@@ -102,67 +102,16 @@ add_task(async function test_creditCards
   is(selRecords.length, 1, "One credit card is shown");
 
   await removeCreditCards([selRecords.options[0].value]);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
   is(selRecords.length, 0, "Credit card is removed");
   win.close();
 });
 
-add_task(async function test_showCreditCards() {
-  await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
-  await saveCreditCard(TEST_CREDIT_CARD_1);
-  await saveCreditCard(TEST_CREDIT_CARD_2);
-  await saveCreditCard(TEST_CREDIT_CARD_3);
-
-  let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
-  await waitForFocusAndFormReady(win);
-
-  let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
-  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
-
-  is(btnShowHideCreditCards.disabled, false, "Show credit cards button enabled");
-  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
-
-  // Show credit card numbers
-  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
-  is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
-  is(selRecords[1].text, "4929001587121045, Timothy Berners-Lee", "Decrypted credit card 2");
-  is(selRecords[2].text, "4111111111111111, John Doe", "Decrypted credit card 1");
-  is(btnShowHideCreditCards.textContent, "Hide Credit Cards", "Label should be 'Hide Credit Cards'");
-
-  // Hide credit card numbers
-  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
-  is(selRecords[0].text, "**** 7870", "Masked credit card 3");
-  is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
-  is(selRecords[2].text, "**** 1111, John Doe", "Masked credit card 1");
-  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
-
-  // Show credit card numbers again to test if they revert back to masked form when reloaded
-  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
-  // Ensure credit card numbers are shown again
-  is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
-  // Remove a card to trigger reloading
-  await removeCreditCards([selRecords.options[2].value]);
-  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
-  is(selRecords[0].text, "**** 7870", "Masked credit card 3");
-  is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
-
-  // Remove the rest of the cards
-  await removeCreditCards([selRecords.options[1].value]);
-  await removeCreditCards([selRecords.options[0].value]);
-  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
-  is(btnShowHideCreditCards.disabled, true, "Show credit cards button is disabled when there is no card");
-
-  win.close();
-});
-
 add_task(async function test_showCreditCardIcons() {
   await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
   await saveCreditCard(TEST_CREDIT_CARD_1);
   let unknownCard = Object.assign({}, TEST_CREDIT_CARD_3, {"cc-type": "gringotts"});
   await saveCreditCard(unknownCard);
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
@@ -188,45 +137,54 @@ add_task(async function test_showCreditC
   }
 
   await removeCreditCards([option0.value, option1.value]);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
   is(selRecords.length, 0, "Credit card is removed");
   win.close();
 });
 
+add_task(async function test_hasEditLoginPrompt() {
+  if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+    todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+    return;
+  }
 
-add_task(async function test_hasMasterPassword() {
   await saveCreditCard(TEST_CREDIT_CARD_1);
-  LoginTestUtils.masterPassword.enable();
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await waitForFocusAndFormReady(win);
 
   let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
   let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
   let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
   let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
-  let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
-  let masterPasswordDialogShown = waitForMasterPasswordDialog();
+  // let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
 
   is(btnShowHideCreditCards.hidden, true, "Show credit cards button is hidden");
 
-  // Master password dialog should show when trying to edit a credit card record.
   EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+
+  // Login dialog should show when trying to edit a credit card record.
+  // TODO: test disabled because re-auth is not implemented yet (bug 1429265).
+  /*
+  let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
   EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
-  await masterPasswordDialogShown;
+  await osKeyStoreLoginShown;
+  await new Promise(resolve => waitForFocus(resolve, win));
+  await new Promise(resolve => executeSoon(resolve));
+  */
 
-  // Master password is not required for removing credit cards.
+  // Login is not required for removing credit cards.
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
   await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
   is(selRecords.length, 0, "Credit card is removed");
 
   // gSubDialog.open should be called when trying to add a credit card,
-  // no master password is required.
+  // no OS login dialog is required.
   window.gSubDialog = {
     open: url => is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"),
   };
   EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win);
   delete window.gSubDialog;
 
   win.close();
 });
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -3,32 +3,31 @@
             TEST_ADDRESS_IE_1,
             TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3, FORM_URL, CREDITCARD_FORM_URL,
             FTU_PREF, ENABLED_AUTOFILL_ADDRESSES_PREF, AUTOFILL_CREDITCARDS_AVAILABLE_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF,
             SUPPORTED_COUNTRIES_PREF,
             SYNC_USERNAME_PREF, SYNC_ADDRESSES_PREF, SYNC_CREDITCARDS_PREF, SYNC_CREDITCARDS_AVAILABLE_PREF, CREDITCARDS_USED_STATUS_PREF,
             DEFAULT_REGION_PREF,
             sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
             getAddresses, saveAddress, removeAddresses, saveCreditCard,
-            getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
+            getDisplayedPopupItems, getDoorhangerCheckbox,
             getNotification, getDoorhangerButton, removeAllRecords, testDialog */
 
 "use strict";
 
-ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
-ChromeUtils.import("resource://formautofill/MasterPassword.jsm", this);
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", this);
 
 const MANAGE_ADDRESSES_DIALOG_URL = "chrome://formautofill/content/manageAddresses.xhtml";
 const MANAGE_CREDIT_CARDS_DIALOG_URL = "chrome://formautofill/content/manageCreditCards.xhtml";
 const EDIT_ADDRESS_DIALOG_URL = "chrome://formautofill/content/editAddress.xhtml";
 const EDIT_CREDIT_CARD_DIALOG_URL = "chrome://formautofill/content/editCreditCard.xhtml";
 const BASE_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/";
 const FORM_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html";
-const CREDITCARD_FORM_URL =
-  "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
+const CREDITCARD_FORM_URL = "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
 const FTU_PREF = "extensions.formautofill.firstTimeUse";
 const CREDITCARDS_USED_STATUS_PREF = "extensions.formautofill.creditCards.used";
 const ENABLED_AUTOFILL_ADDRESSES_PREF = "extensions.formautofill.addresses.enabled";
 const AUTOFILL_CREDITCARDS_AVAILABLE_PREF = "extensions.formautofill.creditCards.available";
 const ENABLED_AUTOFILL_CREDITCARDS_PREF = "extensions.formautofill.creditCards.enabled";
 const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";
 const SYNC_USERNAME_PREF = "services.sync.username";
 const SYNC_ADDRESSES_PREF = "services.sync.engine.addresses";
@@ -321,34 +320,16 @@ async function clickDoorhangerButton(but
 function getDoorhangerCheckbox() {
   return getNotification().checkbox;
 }
 
 function getDoorhangerButton(button) {
   return getNotification()[button];
 }
 
-
-// Wait for the master password dialog to popup and enter the password to log in
-// if "login" is "true" or dismiss it directly if otherwise.
-function waitForMasterPasswordDialog(login = false) {
-  info("expecting master password dialog loaded");
-  let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
-  return dialogShown.then(([subject]) => {
-    let dialog = subject.Dialog;
-    is(dialog.args.title, "Password Required", "Master password dialog shown");
-    if (login) {
-      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
-      dialog.ui.button0.click();
-    } else {
-      dialog.ui.button1.click();
-    }
-  });
-}
-
 async function removeAllRecords() {
   let addresses = await getAddresses();
   if (addresses.length) {
     await removeAddresses(addresses.map(address => address.guid));
   }
 
   let creditCards = await getCreditCards();
   if (creditCards.length) {
@@ -361,19 +342,26 @@ async function waitForFocusAndFormReady(
     new Promise(resolve => waitForFocus(resolve, win)),
     BrowserTestUtils.waitForEvent(win, "FormReady"),
   ]);
 }
 
 async function testDialog(url, testFn, arg = undefined) {
   if (url == EDIT_CREDIT_CARD_DIALOG_URL && arg && arg.record) {
     arg.record = Object.assign({}, arg.record, {
-      "cc-number": await MasterPassword.decrypt(arg.record["cc-number-encrypted"]),
+      "cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
     });
   }
   let win = window.openDialog(url, null, "width=600,height=600", arg);
   await waitForFocusAndFormReady(win);
   let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
   await testFn(win);
   return unloadPromise;
 }
 
+add_task(function setup() {
+  OSKeyStoreTestUtils.setup();
+});
+
 registerCleanupFunction(removeAllRecords);
+registerCleanupFunction(async () => {
+  await OSKeyStoreTestUtils.cleanup();
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "OSKeyStoreTestUtils",
+];
+
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
+// TODO: Consider AppConstants.MOZILLA_OFFICIAL to decide if we could test re-auth (bug 1429265).
+/*
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+*/
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+ChromeUtils.import("resource://testing-common/TestUtils.jsm");
+
+var OSKeyStoreTestUtils = {
+  /*
+  TEST_ONLY_REAUTH: "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin",
+  */
+
+  setup() {
+    // TODO: run tests with master password enabled to ensure NSS-implemented
+    // key store prompts on re-auth (bug 1429265)
+    /*
+    LoginTestUtils.masterPassword.enable();
+    */
+
+    this.ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL;
+    OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2);
+  },
+
+  async cleanup() {
+    // TODO: run tests with master password enabled to ensure NSS-implemented
+    // key store prompts on re-auth (bug 1429265)
+    /*
+    LoginTestUtils.masterPassword.disable();
+    */
+
+    await OSKeyStore.cleanup();
+    OSKeyStore.STORE_LABEL = this.ORIGINAL_STORE_LABEL;
+  },
+
+  canTestOSKeyStoreLogin() {
+    // TODO: return true based on whether or not we could test the prompt on
+    // the platform (bug 1429265).
+    /*
+    return OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+    */
+    return true;
+  },
+
+  // Wait for the master password dialog to popup and enter the password to log in
+  // if "login" is "true" or dismiss it directly if otherwise.
+  async waitForOSKeyStoreLogin(login = false) {
+    // TODO: Always resolves for now, because we are skipping re-auth on all
+    // platforms (bug 1429265).
+    /*
+    if (OSKeyStore.isNSSKeyStore) {
+      await this.waitForMasterPasswordDialog(login);
+      return;
+    }
+
+    const str = login ? "pass" : "cancel";
+
+    Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, str);
+
+    await TestUtils.topicObserved("oskeystore-testonly-reauth",
+      (subject, data) => data == str);
+
+    Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, "");
+    */
+  },
+
+  async waitForMasterPasswordDialog(login = false) {
+    let [subject] = await TestUtils.topicObserved("common-dialog-loaded");
+
+    let dialog = subject.Dialog;
+    if (dialog.args.title !== "Password Required") {
+      throw new Error("Incorrect master password dialog title");
+    }
+
+    if (login) {
+      dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
+      dialog.ui.button0.click();
+    } else {
+      dialog.ui.button1.click();
+    }
+    await TestUtils.waitForTick();
+  },
+};
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -1,10 +1,11 @@
 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
+/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/AddTask.js */
 /* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
 /* eslint-disable no-unused-vars */
 
 "use strict";
 
 let formFillChromeScript;
 let defaultTextColor;
 let expectingPopup = null;
@@ -196,16 +197,25 @@ async function cleanUpCreditCards() {
   return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards", "FormAutofillTest:CreditCardsCleanedUp");
 }
 
 async function cleanUpStorage() {
   await cleanUpAddresses();
   await cleanUpCreditCards();
 }
 
+async function canTestOSKeyStoreLogin() {
+  let {canTest} = await invokeAsyncChromeTask("FormAutofillTest:CanTestOSKeyStoreLogin", "FormAutofillTest:CanTestOSKeyStoreLoginResult");
+  return canTest;
+}
+
+async function waitForOSKeyStoreLogin(login = false) {
+  await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", "FormAutofillTest:OSKeyStoreLoggedIn", {login});
+}
+
 function patchRecordCCNumber(record) {
   const number = record["cc-number"];
   const ccNumberFmt = {
     affix: "****",
     label: number.substr(-4),
   };
 
   return Object.assign({}, record, {ccNumberFmt});
@@ -258,16 +268,22 @@ function formAutoFillCommonSetup() {
   formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
   formFillChromeScript.addMessageListener("onpopupshown", ({results}) => {
     gLastAutoCompleteResults = results;
     if (gPopupShownListener) {
       gPopupShownListener({results});
     }
   });
 
+  add_task(async function setup() {
+    formFillChromeScript.sendAsyncMessage("setup");
+    info(`expecting the storage setup`);
+    await formFillChromeScript.promiseOneMessage("setup-finished");
+  });
+
   SimpleTest.registerCleanupFunction(async () => {
     formFillChromeScript.sendAsyncMessage("cleanup");
     info(`expecting the storage cleanup`);
     await formFillChromeScript.promiseOneMessage("cleanup-finished");
 
     formFillChromeScript.destroy();
     expectingPopup = null;
   });
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -1,16 +1,19 @@
 // assert is available to chrome scripts loaded via SpecialPowers.loadChromeScript.
 /* global assert */
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
 
 let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
 
 const {ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME} = FormAutofillUtils;
 
 var ParentUtils = {
   async _getRecords(collectionName) {
     return new Promise(resolve => {
@@ -109,19 +112,25 @@ var ParentUtils = {
     if (guids.length == 0) {
       sendAsyncMessage("FormAutofillTest:CreditCardsCleanedUp");
       return;
     }
 
     await this.operateCreditCard("remove", {guids}, "FormAutofillTest:CreditCardsCleanedUp");
   },
 
+  setup() {
+    OSKeyStoreTestUtils.setup();
+  },
+
   async cleanup() {
     await this.cleanUpAddresses();
     await this.cleanUpCreditCards();
+    await OSKeyStoreTestUtils.cleanup();
+
     Services.obs.removeObserver(this, "formautofill-storage-changed");
   },
 
   _areRecordsMatching(recordA, recordB, collectionName) {
     for (let field of formAutofillStorage[collectionName].VALID_FIELDS) {
       if (recordA[field] !== recordB[field]) {
         return false;
       }
@@ -210,13 +219,28 @@ addMessageListener("FormAutofillTest:Rem
 addMessageListener("FormAutofillTest:CheckCreditCards", (msg) => {
   ParentUtils.checkCreditCards(msg);
 });
 
 addMessageListener("FormAutofillTest:CleanUpCreditCards", (msg) => {
   ParentUtils.cleanUpCreditCards();
 });
 
-addMessageListener("cleanup", () => {
-  ParentUtils.cleanup().then(() => {
-    sendAsyncMessage("cleanup-finished", {});
-  });
+addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", (msg) => {
+  sendAsyncMessage("FormAutofillTest:CanTestOSKeyStoreLoginResult",
+    {canTest: OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL});
+});
+
+addMessageListener("FormAutofillTest:OSKeyStoreLogin", async (msg) => {
+  await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
+  sendAsyncMessage("FormAutofillTest:OSKeyStoreLoggedIn");
 });
+
+addMessageListener("setup", async () => {
+  ParentUtils.setup();
+  sendAsyncMessage("setup-finished", {});
+});
+
+addMessageListener("cleanup", async () => {
+  await ParentUtils.cleanup();
+
+  sendAsyncMessage("cleanup-finished", {});
+});
--- a/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
@@ -145,41 +145,60 @@ add_task(async function check_search_res
   await setInput("#cc-name", "");
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(["John Smith"], false);
 
   await SpecialPowers.popPrefEnv();
 });
 
+let canTest;
+
 // Autofill the credit card from dropdown menu.
 add_task(async function check_fields_after_form_autofill() {
+  canTest = await canTestOSKeyStoreLogin();
+  if (!canTest) {
+    todo(canTest, "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   await setInput("#cc-exp-year", 202);
 
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(cc => JSON.stringify({
     primary: cc["cc-exp-year"],
     secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
   })));
 
   synthesizeKey("KEY_ArrowDown");
+  let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+  await new Promise(resolve => SimpleTest.executeSoon(resolve));
   await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+  await osKeyStoreLoginShown;
 });
 
 // Fallback to history search after autofill address.
 add_task(async function check_fallback_after_form_autofill() {
+  if (!canTest) {
+    return;
+  }
+
   await setInput("#cc-name", "", true);
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(["John Smith"], false);
 });
 
 // Resume form autofill once all the autofilled fileds are changed.
 add_task(async function check_form_autofill_resume() {
+  if (!canTest) {
+    return;
+  }
+
   document.querySelector("#cc-name").blur();
   document.querySelector("#form1").reset();
 
   await setInput("#cc-name", "");
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
     primary: cc["cc-name"],
--- a/browser/extensions/formautofill/test/mochitest/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_clear_form.html
@@ -98,19 +98,26 @@ add_task(async function clear_modified_f
   await setInput("#tel", "+1111111111", true);
 
   await triggerPopupAndHoverItem("#street-address", 0);
   await confirmClear("#street-address");
   checkIsFormCleared({tel: "+1111111111"});
 });
 
 add_task(async function clear_distinct_section() {
+  if (!(await canTestOSKeyStoreLogin())) {
+    todo(false, "Cannot test OS key store login on official builds.");
+    return;
+  }
+
   document.getElementById("form1").reset();
   await triggerPopupAndHoverItem("#cc-name", 0);
+  let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
   await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
+  await osKeyStoreLoginShown;
 
   await triggerPopupAndHoverItem("#organization", 0);
   await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
   await triggerPopupAndHoverItem("#street-address", 0);
   await confirmClear("#street-address");
 
   for (const [id, val] of Object.entries(MOCK_CC_STORAGE[0])) {
     const element = document.getElementById(id);
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -94,17 +94,17 @@ async function initProfileStorage(fileNa
 
   let onChanged = TestUtils.topicObserved(
     "formautofill-storage-changed",
     (subject, data) =>
       data == "add" &&
       subject.wrappedJSObject.collectionName == collectionName
   );
   for (let record of records) {
-    Assert.ok(profileStorage[collectionName].add(record));
+    Assert.ok(await profileStorage[collectionName].add(record));
     await onChanged;
   }
   await profileStorage._saveImmediately();
   return profileStorage;
 }
 
 function verifySectionFieldDetails(sections, expectedResults) {
   Assert.equal(sections.length, expectedResults.length, "Expected section count.");
@@ -218,8 +218,18 @@ add_task(async function head_initialize(
     Services.prefs.clearUserPref("extensions.formautofill.creditCards.available");
     Services.prefs.clearUserPref("extensions.formautofill.heuristics.enabled");
     Services.prefs.clearUserPref("extensions.formautofill.section.enabled");
     Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
   });
 
   await loadExtension();
 });
+
+let OSKeyStoreTestUtils;
+add_task(async function os_key_store_setup() {
+  ({OSKeyStoreTestUtils} =
+    ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", {}));
+  OSKeyStoreTestUtils.setup();
+  registerCleanupFunction(async function cleanup() {
+    await OSKeyStoreTestUtils.cleanup();
+  });
+});
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -1,19 +1,18 @@
 /*
  * Test for form auto fill content helper fill all inputs function.
  */
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 "use strict";
 
-let MasterPassword;
 add_task(async function setup() {
   ChromeUtils.import("resource://formautofill/FormAutofillHandler.jsm");
-  ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
+  ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
 });
 
 const TESTCASES = [
   {
     description: "Form without autocomplete property",
     document: `<form><input id="given-name"><input id="family-name">
                <input id="street-addr"><input id="city"><select id="country"></select>
                <input id='email'><input id="tel"></form>`,
@@ -472,38 +471,31 @@ const TESTCASES_FILL_SELECT = [
 function do_test(testcases, testFn) {
   for (let tc of testcases) {
     (function() {
       let testcase = tc;
       add_task(async function() {
         info("Starting testcase: " + testcase.description);
         let ccNumber = testcase.profileData["cc-number"];
         if (ccNumber) {
-          testcase.profileData["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+          testcase.profileData["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
           delete testcase.profileData["cc-number"];
         }
 
         let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                   testcase.document);
         let form = doc.querySelector("form");
         let formLike = FormLikeFactory.createFromForm(form);
         let handler = new FormAutofillHandler(formLike);
         let promises = [];
-        // Replace the interal decrypt method with MasterPassword API
+        // Replace the internal decrypt method with OSKeyStore API,
+        // but don't pass the reauth parameter to avoid triggering
+        // reauth login dialog in these tests.
         let decryptHelper = async (cipherText, reauth) => {
-          let string;
-          try {
-            string = await MasterPassword.decrypt(cipherText, reauth);
-          } catch (e) {
-            if (e.result != Cr.NS_ERROR_ABORT) {
-              throw e;
-            }
-            info("User canceled master password entry");
-          }
-          return string;
+          return OSKeyStore.decrypt(cipherText, false);
         };
 
         handler.collectFormFields();
 
         let focusedInput = doc.getElementById(testcase.focusedInputId);
         handler.focusedInput = focusedInput;
 
         for (let section of handler.sections) {
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -650,23 +650,17 @@ add_task(async function test_getDuplicat
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 
   // Numbers with the same last 4 digits shouldn't be treated as a duplicate.
   record = Object.assign({}, TEST_CREDIT_CARD_3);
   let last4Digits = record["cc-number"].substr(-4);
   // This number differs from TEST_CREDIT_CARD_3 by swapping the order of the
   // 09 and 90 adjacent digits, which is still a valid credit card number.
   record["cc-number"] = "358999378390" + last4Digits;
-  Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 
-  // ... However, we treat numbers with the same last 4 digits as a duplicate if
-  // the master password is enabled.
-  let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
-  let token = tokendb.getInternalKeyToken();
-  token.reset();
-  token.initPassword("password");
+  // We treat numbers with the same last 4 digits as a duplicate.
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), guid);
 
-  // ... Even though the master password is enabled and the last 4 digits are the
-  // same, an invalid credit card number should never be treated as a duplicate.
+  // Even though the last 4 digits are the same, an invalid credit card number
+  // should never be treated as a duplicate.
   record["cc-number"] = "************" + last4Digits;
   Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
 });
--- a/browser/extensions/formautofill/test/unit/test_getRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_getRecords.js
@@ -2,19 +2,20 @@
  * Test for make sure getRecords can retrieve right collection from storage.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 let FormAutofillParent;
+let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillParent} = ChromeUtils.import("resource://formautofill/FormAutofillParent.jsm", {}));
-  ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 const TEST_ADDRESS_1 = {
   "given-name": "Timothy",
   "additional-name": "John",
   "family-name": "Berners-Lee",
   organization: "World Wide Web Consortium",
   "street-address": "32 Vassar Street\nMIT Room 32-G524",
@@ -171,84 +172,63 @@ add_task(async function test_getRecords_
   let formAutofillParent = new FormAutofillParent();
 
   await formAutofillParent.init();
   await formAutofillParent.formAutofillStorage.initialize();
   let collection = formAutofillParent.formAutofillStorage.creditCards;
   let encryptedCCRecords = await Promise.all([TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2].map(async record => {
     let clonedRecord = Object.assign({}, record);
     clonedRecord["cc-number"] = CreditCard.getLongMaskedNumber(record["cc-number"]);
-    clonedRecord["cc-number-encrypted"] = await MasterPassword.encrypt(record["cc-number"]);
+    clonedRecord["cc-number-encrypted"] = await OSKeyStore.encrypt(record["cc-number"]);
     return clonedRecord;
   }));
   sinon.stub(collection, "getAll", () =>
     Promise.resolve([Object.assign({}, encryptedCCRecords[0]), Object.assign({}, encryptedCCRecords[1])]));
-  let CreditCardsWithDecryptedNumber = [
-    Object.assign({}, encryptedCCRecords[0], {"cc-number-decrypted": TEST_CREDIT_CARD_1["cc-number"]}),
-    Object.assign({}, encryptedCCRecords[1], {"cc-number-decrypted": TEST_CREDIT_CARD_2["cc-number"]}),
-  ];
 
   let testCases = [
     {
-      description: "If the search string could match 1 creditCard (without masterpassword)",
-      filter: {
-        collectionName: "creditCards",
-        info: {fieldName: "cc-name"},
-        searchString: "John Doe",
-      },
-      expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
-    },
-    {
-      description: "If the search string could match multiple creditCards (without masterpassword)",
+      description: "If the search string could match multiple creditCards",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "John",
       },
-      expectedResult: CreditCardsWithDecryptedNumber,
+      expectedResult: encryptedCCRecords,
     },
     {
-      description: "If the search string could not match any creditCard (without masterpassword)",
+      description: "If the search string could not match any creditCard",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "T",
       },
       expectedResult: [],
     },
     {
-      description: "If the search number string could match 1 creditCard (without masterpassword)",
-      filter: {
-        collectionName: "creditCards",
-        info: {fieldName: "cc-number"},
-        searchString: "411",
-      },
-      expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
-    },
-    {
-      description: "If the search string could match multiple creditCards (without masterpassword)",
+      description: "Return all creditCards if focused field is cc number; " +
+        "if the search string could match multiple creditCards",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-number"},
         searchString: "4",
       },
-      expectedResult: CreditCardsWithDecryptedNumber,
+      expectedResult: encryptedCCRecords,
     },
     {
-      description: "If the search string could match 1 creditCard (with masterpassword)",
+      description: "If the search string could match 1 creditCard",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-name"},
         searchString: "John Doe",
       },
       mpEnabled: true,
       expectedResult: encryptedCCRecords.slice(0, 1),
     },
     {
-      description: "Return all creditCards if focused field is cc number (with masterpassword)",
+      description: "Return all creditCards if focused field is cc number",
       filter: {
         collectionName: "creditCards",
         info: {fieldName: "cc-number"},
         searchString: "411",
       },
       mpEnabled: true,
       expectedResult: encryptedCCRecords,
     },
deleted file mode 100644
--- a/browser/extensions/formautofill/test/unit/test_masterPassword.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Tests of MasterPassword.jsm
- */
-
-"use strict";
-const {MockRegistrar} =
-  ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", {});
-
-let MasterPassword;
-add_task(async function setup() {
-  ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
-});
-
-const TESTCASES = [{
-  description: "With master password set",
-  masterPassword: "fakemp",
-  mpEnabled: true,
-},
-{
-  description: "Without master password set",
-  masterPassword: "", // "" means no master password
-  mpEnabled: false,
-}];
-
-
-// Tests that PSM can successfully ask for a password from the user and relay it
-// back to NSS. Does so by mocking out the actual dialog and "filling in" the
-// password. Also tests that providing an incorrect password will fail (well,
-// technically the user will just get prompted again, but if they then cancel
-// the dialog the overall operation will fail).
-
-let gMockPrompter = {
-  passwordToTry: null,
-  numPrompts: 0,
-
-  // This intentionally does not use arrow function syntax to avoid an issue
-  // where in the context of the arrow function, |this != gMockPrompter| due to
-  // how objects get wrapped when going across xpcom boundaries.
-  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
-    this.numPrompts++;
-    if (this.numPrompts > 1) { // don't keep retrying a bad password
-      return false;
-    }
-    equal(text,
-          "Please enter your master password.",
-          "password prompt text should be as expected");
-    equal(checkMsg, null, "checkMsg should be null");
-    ok(this.passwordToTry, "passwordToTry should be non-null");
-    password.value = this.passwordToTry;
-    return true;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
-};
-
-// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
-// to call promptPassword. We return the mock one, above.
-let gWindowWatcher = {
-  getNewPrompter: () => gMockPrompter,
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
-};
-
-// Ensure that the appropriate initialization has happened.
-do_get_profile();
-
-let windowWatcherCID =
-  MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
-                         gWindowWatcher);
-registerCleanupFunction(() => {
-  MockRegistrar.unregister(windowWatcherCID);
-});
-
-TESTCASES.forEach(testcase => {
-  add_task(async function test_encrypt_decrypt() {
-    info("Starting testcase: " + testcase.description);
-
-    let token = MasterPassword._token;
-    token.initPassword(testcase.masterPassword);
-
-    // Test only: Force the token login without asking for master password
-    token.login(/* force */ false);
-    Assert.equal(testcase.mpEnabled, token.isLoggedIn(), "Token should now be logged into");
-    Assert.equal(MasterPassword.isEnabled, testcase.mpEnabled);
-
-    let testText = "test string";
-    let cipherText = await MasterPassword.encrypt(testText);
-    Assert.notEqual(testText, cipherText);
-    let plainText = await MasterPassword.decrypt(cipherText);
-    Assert.equal(testText, plainText);
-    if (token.isLoggedIn()) {
-      // Reset state.
-      gMockPrompter.numPrompts = 0;
-      token.logoutSimple();
-
-      ok(!token.isLoggedIn(),
-         "Token should be logged out after calling logoutSimple()");
-
-      // Try with the correct password.
-      gMockPrompter.passwordToTry = testcase.masterPassword;
-      await MasterPassword.encrypt(testText);
-      Assert.equal(gMockPrompter.numPrompts, 1, "should have prompted for encryption");
-
-      // Reset state.
-      gMockPrompter.numPrompts = 0;
-      token.logoutSimple();
-
-      try {
-        // Try with the incorrect password.
-        gMockPrompter.passwordToTry = "XXX";
-        await MasterPassword.decrypt(cipherText);
-        throw new Error("Not receiving canceled master password error");
-      } catch (e) {
-        Assert.equal(e.message, "User canceled master password entry");
-      }
-    }
-
-    token.reset();
-  });
-});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_osKeyStore.js
@@ -0,0 +1,146 @@
+/**
+ * Tests of OSKeyStore.jsm
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
+
+let OSKeyStore;
+add_task(async function setup() {
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
+});
+
+// Ensure that the appropriate initialization has happened.
+do_get_profile();
+
+// For NSS key store, mocking out the dialog and control it from here.
+let gMockPrompter = {
+  passwordToTry: "hunter2",
+  resolve: null,
+  login: undefined,
+
+  // This intentionally does not use arrow function syntax to avoid an issue
+  // where in the context of the arrow function, |this != gMockPrompter| due to
+  // how objects get wrapped when going across xpcom boundaries.
+  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
+    equal(text,
+          "Please enter your master password.",
+          "password prompt text should be as expected");
+    equal(checkMsg, null, "checkMsg should be null");
+    if (this.login) {
+      password.value = this.passwordToTry;
+    }
+    this.resolve();
+    this.resolve = null;
+
+    return this.login;
+  },
+
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
+};
+
+// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
+// to call promptPassword. We return the mock one, above.
+let gWindowWatcher = {
+  getNewPrompter: () => gMockPrompter,
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
+};
+
+let nssToken;
+
+const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
+
+async function waitForReauth(login = false) {
+  if (OSKeyStore.isNSSKeyStore) {
+    gMockPrompter.login = login;
+    await new Promise(resolve => { gMockPrompter.resolve = resolve; });
+
+    return;
+  }
+
+  let value = login ? "pass" : "cancel";
+  Services.prefs.setStringPref(TEST_ONLY_REAUTH, value);
+  await TestUtils.topicObserved("oskeystore-testonly-reauth",
+    (subject, data) => data == value);
+}
+
+const testText = "test string";
+let cipherText;
+
+add_task(async function test_encrypt_decrypt() {
+  Assert.equal(OSKeyStore.isEnabled, true);
+
+  Assert.equal(await OSKeyStore.ensureLoggedIn(), true, "Started logged in.");
+
+  cipherText = await OSKeyStore.encrypt(testText);
+  Assert.notEqual(testText, cipherText);
+
+  let plainText = await OSKeyStore.decrypt(cipherText);
+  Assert.equal(testText, plainText);
+});
+
+// TODO: skipped because re-auth is not implemented (bug 1429265).
+add_task(async function test_reauth() {
+  let canTest = OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+  if (!canTest) {
+    todo_check_false(canTest,
+      "test_reauth: Cannot test OS key store login on official builds.");
+    return;
+  }
+
+  if (OSKeyStore.isNSSKeyStore) {
+    let windowWatcherCID;
+    windowWatcherCID =
+      MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+                             gWindowWatcher);
+    registerCleanupFunction(() => {
+      MockRegistrar.unregister(windowWatcherCID);
+    });
+
+    // If we use the NSS key store implementation test that everything works
+    // when a master password is set.
+    // Set an initial password.
+    let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
+                    .getService(Ci.nsIPK11TokenDB);
+    nssToken = tokenDB.getInternalKeyToken();
+    nssToken.initPassword("hunter2");
+  }
+
+  let reauthObserved = waitForReauth(false);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  try {
+    await OSKeyStore.decrypt(cipherText, true);
+    throw new Error("Not receiving canceled OS unlock error");
+  } catch (ex) {
+    Assert.equal(ex.message, "User canceled OS unlock entry");
+    Assert.equal(ex.result, Cr.NS_ERROR_ABORT);
+  }
+  await reauthObserved;
+
+  reauthObserved = waitForReauth(false);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  Assert.equal(await OSKeyStore.ensureLoggedIn(true), false, "Reauth cancelled.");
+  await reauthObserved;
+
+  reauthObserved = waitForReauth(true);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  let plainText2 = await OSKeyStore.decrypt(cipherText, true);
+  await reauthObserved;
+  Assert.equal(testText, plainText2);
+
+  reauthObserved = waitForReauth(true);
+  await new Promise(resolve => TestUtils.executeSoon(resolve));
+  Assert.equal(await OSKeyStore.ensureLoggedIn(true), true, "Reauth logged in.");
+  await reauthObserved;
+}).skip();
+
+add_task(async function test_decryption_failure() {
+  try {
+    await OSKeyStore.decrypt("Malformed cipher text");
+    throw new Error("Not receiving decryption error");
+  } catch (ex) {
+    Assert.notEqual(ex.result, Cr.NS_ERROR_ABORT);
+  }
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -33,20 +33,20 @@ support-files =
 [test_getCategoriesFromFieldNames.js]
 [test_getFormInputDetails.js]
 [test_getInfo.js]
 [test_getRecords.js]
 [test_isAvailable.js]
 [test_isCJKName.js]
 [test_isFieldEligibleForAutofill.js]
 [test_markAsAutofillField.js]
-[test_masterPassword.js]
 [test_migrateRecords.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
+[test_osKeyStore.js]
 [test_parseAddressFormat.js]
 [test_profileAutocompleteResult.js]
 [test_phoneNumber.js]
 [test_reconcile.js]
 [test_savedFieldNames.js]
 [test_toOneLineAddress.js]
 [test_storage_tombstones.js]
 [test_storage_remove.js]
--- a/security/manager/ssl/nsIOSKeyStore.idl
+++ b/security/manager/ssl/nsIOSKeyStore.idl
@@ -10,17 +10,17 @@ interface nsIOSKeyStore: nsISupports {
   /**
    * This interface provides encryption and decryption operations for data at
    * rest. The key used to encrypt and decrypt the data is stored in the OS
    * key store.
    *
    * Usage:
    *
    * // obtain the singleton OSKeyStore instance
-   * const oskeystore = Cc["@mozilla.org/oskeystore;1"].getService(Ci.nsIOSKeyStore);
+   * const oskeystore = Cc["@mozilla.org/security/oskeystore;1"].getService(Ci.nsIOSKeyStore);
    *
    * const PASSWORD_LABEL = "mylabel1";
    * const COOKIE_LABEL = "mylabel2";
    *
    * // Unlock the key store.
    * // Note that this is not necesssary. The key store will be unlocked
    * // automatically when an operation is performed on it.
    * await oskeystore.asyncUnlock();
--- a/services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
@@ -9,18 +9,18 @@
 
 var EXPORTED_SYMBOLS = ["Address", "CreditCard", "DumpAddresses", "DumpCreditCards"];
 
 ChromeUtils.import("resource://tps/logger.jsm");
 
 ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                                "resource://formautofill/FormAutofillStorage.jsm");
 
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 
 class FormAutofillBase {
   constructor(props, subStorageName, fields) {
     this._subStorageName = subStorageName;
     this._fields = fields;
 
     this.props = {};
     this.updateProps = null;
@@ -106,17 +106,17 @@ const CREDIT_CARD_FIELDS = [
 class CreditCard extends FormAutofillBase {
   constructor(props) {
     super(props, "creditCards", CREDIT_CARD_FIELDS);
   }
 
   async Find() {
     const storage = await this.getStorage();
     await Promise.all(storage._data.map(
-      async entry => entry["cc-number"] = await MasterPassword.decrypt(entry["cc-number-encrypted"])));
+      async entry => entry["cc-number"] = await OSKeyStore.decrypt(entry["cc-number-encrypted"])));
     return storage._data.find(entry => {
       return this._fields.every(field => entry[field] === this.props[field]);
     });
   }
 }
 
 async function DumpCreditCards() {
   await DumpStorage("creditCards");
--- a/toolkit/modules/CreditCard.jsm
+++ b/toolkit/modules/CreditCard.jsm
@@ -1,18 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["CreditCard"];
 
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+                               "resource://formautofill/OSKeyStore.jsm");
 
 // The list of known and supported credit card network ids ("types")
 // This list mirrors the networks from dom/payments/BasicCardPayment.cpp
 // and is defined by https://www.w3.org/Payments/card-network-ids
 const SUPPORTED_NETWORKS = Object.freeze([
   "amex",
   "cartebancaire",
   "diners",
@@ -203,17 +203,17 @@ class CreditCard {
    * true, decrypted credit card numbers are shown instead.
    */
   async getLabel({showNumbers} = {}) {
     let parts = [];
     let label;
 
     if (showNumbers) {
       if (this._encryptedNumber) {
-        label = await MasterPassword.decrypt(this._encryptedNumber);
+        label = await OSKeyStore.decrypt(this._encryptedNumber);
       } else {
         label = this._number;
       }
     }
     if (this._unmodifiedNumber && !label) {
       if (this.isValidNumber()) {
         label = this.maskedNumber;
       } else {
--- a/toolkit/modules/tests/browser/browser_CreditCard.js
+++ b/toolkit/modules/tests/browser/browser_CreditCard.js
@@ -1,45 +1,46 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
-ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
 
 let oldGetters = {};
 let gFakeLoggedIn = true;
 
 add_task(function setup() {
-  oldGetters._token = Object.getOwnPropertyDescriptor(MasterPassword, "_token").get;
-  oldGetters.isEnabled = Object.getOwnPropertyDescriptor(MasterPassword, "isEnabled").get;
-  oldGetters.isLoggedIn = Object.getOwnPropertyDescriptor(MasterPassword, "isLoggedIn").get;
-  MasterPassword.__defineGetter__("_token", () => { return {hasPassword: true}; });
-  MasterPassword.__defineGetter__("isEnabled", () => true);
-  MasterPassword.__defineGetter__("isLoggedIn", () => gFakeLoggedIn);
-  registerCleanupFunction(() => {
-    MasterPassword.__defineGetter__("_token", oldGetters._token);
-    MasterPassword.__defineGetter__("isEnabled", oldGetters.isEnabled);
-    MasterPassword.__defineGetter__("isLoggedIn", oldGetters.isLoggedIn);
+  OSKeyStoreTestUtils.setup();
+  oldGetters.isEnabled = Object.getOwnPropertyDescriptor(OSKeyStore, "isEnabled").get;
+  oldGetters.isLoggedIn = Object.getOwnPropertyDescriptor(OSKeyStore, "isLoggedIn").get;
+  OSKeyStore.__defineGetter__("isEnabled", () => true);
+  OSKeyStore.__defineGetter__("isLoggedIn", () => gFakeLoggedIn);
+  registerCleanupFunction(async () => {
+    OSKeyStore.__defineGetter__("isEnabled", oldGetters.isEnabled);
+    OSKeyStore.__defineGetter__("isLoggedIn", oldGetters.isLoggedIn);
+    await OSKeyStoreTestUtils.cleanup();
 
-    // CreditCard.jsm and MasterPassword.jsm are imported into the global scope
-    // -- the window -- above. If they're not deleted, they outlive the test and
-    // are reported as a leak.
-    delete window.MasterPassword;
+    // CreditCard.jsm, OSKeyStore.jsm, and OSKeyStoreTestUtils.jsm are imported
+    // into the global scope -- the window -- above. If they're not deleted,
+    // they outlive the test and are reported as a leak.
+    delete window.OSKeyStore;
     delete window.CreditCard;
+    delete window.OSKeyStoreTestUtils;
   });
 });
 
-add_task(async function test_getLabel_withMasterPassword() {
-  ok(MasterPassword.isEnabled, "Confirm that MasterPassword is faked and thinks it is enabled");
-  ok(MasterPassword.isLoggedIn, "Confirm that MasterPassword is faked and thinks it is logged in");
+add_task(async function test_getLabel_withOSKeyStore() {
+  ok(OSKeyStore.isEnabled, "Confirm that OSKeyStore is faked and thinks it is enabled");
+  ok(OSKeyStore.isLoggedIn, "Confirm that OSKeyStore is faked and thinks it is logged in");
 
   const ccNumber = "4111111111111111";
-  const encryptedNumber = await MasterPassword.encrypt(ccNumber);
-  const decryptedNumber = await MasterPassword.decrypt(encryptedNumber);
+  const encryptedNumber = await OSKeyStore.encrypt(ccNumber);
+  const decryptedNumber = await OSKeyStore.decrypt(encryptedNumber);
   is(decryptedNumber, ccNumber, "Decrypted CC number should match original");
 
   const name = "Foxkeh";
   const creditCard = new CreditCard({encryptedNumber, name: "Foxkeh"});
   const label = await creditCard.getLabel({showNumbers: true});
   is(label, `${ccNumber}, ${name}`);
 });