Merge inbound to mozilla-central. a=merge
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Tue, 23 Oct 2018 12:27:03 +0300
changeset 498951 6e96c7ec0d11
parent 498933 dfa1eb1d036f (current diff)
parent 498950 74f6186ded6d (diff)
child 498955 141e6f82a895
child 498979 99748ccfba86
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.0a1
first release with
nightly linux32
6e96c7ec0d11 / 65.0a1 / 20181023100120 / files
nightly linux64
6e96c7ec0d11 / 65.0a1 / 20181023100120 / files
nightly mac
6e96c7ec0d11 / 65.0a1 / 20181023100120 / files
nightly win32
6e96c7ec0d11 / 65.0a1 / 20181023100120 / files
nightly win64
6e96c7ec0d11 / 65.0a1 / 20181023100120 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
browser/extensions/formautofill/MasterPassword.jsm
browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
browser/extensions/formautofill/test/unit/test_masterPassword.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;
@@ -150,30 +150,30 @@ var paymentDialogWrapper = {
     });
 
     return address;
   },
 
   /**
    * @param {string} guid The GUID of the basic card record from storage.
    * @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
-   * @throws if the user cancels entering their master password or an error decrypting
+   * @throws If there is an error decrypting
    * @returns {nsIBasicCardResponseData?} returns response data or null (if the
    *                                      master password dialog was cancelled);
    */
   async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
     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;
     }
 
@@ -502,18 +502,26 @@ var paymentDialogWrapper = {
   },
 
   async onPay({
     selectedPayerAddressGUID: payerGUID,
     selectedPaymentCardGUID: paymentCardGUID,
     selectedPaymentCardSecurityCode: cardSecurityCode,
     selectedShippingAddressGUID: shippingGUID,
   }) {
-    let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
-                                                                            cardSecurityCode);
+    let methodData;
+    try {
+      methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
+                                                                          cardSecurityCode);
+    } catch (ex) {
+      // TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user
+      // to re-enter credit card # from management UI.
+      Cu.reportError(ex);
+      return;
+    }
 
     if (!methodData) {
       // TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
       // Master Password dialog.
       Cu.reportError("Bug 1429265/Bug 1429205: User canceled master password entry");
       return;
     }
 
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -322,17 +322,17 @@ let DUPED_ADDRESSES = {
 };
 
 let BASIC_CARDS_1 = {
   "53f9d009aed2": {
     billingAddressGUID: "68gjdh354j",
     methodName: "basic-card",
     "cc-number": "************5461",
     "guid": "53f9d009aed2",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1505240896213,
     "timeLastModified": 1515609524588,
     "timeLastUsed": 10000,
     "timesUsed": 0,
     "cc-name": "John Smith",
     "cc-exp-month": 6,
     "cc-exp-year": 2024,
     "cc-type": "visa",
@@ -340,17 +340,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Smith",
     "cc-exp": "2024-06",
   },
   "9h5d4h6f4d1s": {
     methodName: "basic-card",
     "cc-number": "************0954",
     "guid": "9h5d4h6f4d1s",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 50000,
     "timesUsed": 0,
     "cc-name": "Jane Doe",
     "cc-exp-month": 5,
     "cc-exp-year": 2023,
     "cc-type": "mastercard",
@@ -358,17 +358,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Doe",
     "cc-exp": "2023-05",
   },
   "123456789abc": {
     methodName: "basic-card",
     "cc-number": "************1234",
     "guid": "123456789abc",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 90000,
     "timesUsed": 0,
     "cc-name": "Jane Fields",
     "cc-given-name": "Jane",
     "cc-additional-name": "",
     "cc-family-name": "Fields",
@@ -392,17 +392,17 @@ let BASIC_CARDS_1 = {
     "cc-exp-month": 6,
     "cc-exp-year": 2023,
     "cc-exp": "2023-06",
   },
   "missing-cc-name": {
     methodName: "basic-card",
     "cc-number": "************8563",
     "guid": "missing-cc-name",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 30000,
     "timesUsed": 0,
     "cc-exp-month": 8,
     "cc-exp-year": 2024,
     "cc-exp": "2024-08",
   },
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 head = head.js
 prefs =
   browser.pagethumbnails.capturing_disabled=true
   dom.payments.request.enabled=true
+  extensions.formautofill.creditCards.available=true
 skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
 support-files =
   blank_page.html
 
 [browser_address_edit.js]
 skip-if = verify && debug && os == 'mac'
 [browser_card_edit.js]
 skip-if = (verify && debug && os == 'mac') || (os == 'linux' && debug) # bug 1465673
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -16,16 +16,18 @@ 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 +359,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,44 +312,33 @@ 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.
-    if (isCCAndMPEnabled && info.fieldName == "cc-number") {
+    let isCC = collectionName == CREDITCARDS_COLLECTION_NAME;
+    // We don't filter "cc-number"
+    if (isCC && 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.
-      if (!isCCAndMPEnabled && record["cc-number-encrypted"]) {
-        record["cc-number-decrypted"] = await MasterPassword.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
           && !FormAutofill.supportedCountries.includes(record.country)) {
         // Address autofill isn't supported for the record's country so we don't
         // want to attempt to potentially incorrectly fill the address fields.
         continue;
       }
 
       if (lcSearchString && !String(fieldValue).toLowerCase().startsWith(lcSearchString)) {
@@ -533,18 +522,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,33 +137,36 @@ 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, "cryptoSDR",
+                                   "@mozilla.org/login-manager/crypto/SDR;1",
+                                   Ci.nsILoginManagerCrypto);
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
                                           "nsICryptoHash", "initWithString");
 
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 const STORAGE_SCHEMA_VERSION = 1;
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
 
 const VALID_ADDRESS_FIELDS = [
   "given-name",
   "additional-name",
   "family-name",
   "organization",
   "street-address",
   "address-level3",
@@ -259,23 +262,24 @@ class AutofillRecords {
 
     this.VALID_FIELDS = validFields;
     this.VALID_COMPUTED_FIELDS = validComputedFields;
 
     this._store = store;
     this._collectionName = collectionName;
     this._schemaVersion = schemaVersion;
 
-    Promise.all(this._data.map(record => this._migrateRecord(record)))
-      .then(hasChangesArr => {
-        let dataHasChanges = hasChangesArr.find(hasChanges => hasChanges);
-        if (dataHasChanges) {
-          this._store.saveSoon();
-        }
-      });
+    this._initializePromise =
+      Promise.all(this._data.map(async (record, index) => this._migrateRecord(record, index)))
+        .then(hasChangesArr => {
+          let dataHasChanges = hasChangesArr.includes(true);
+          if (dataHasChanges) {
+            this._store.saveSoon();
+          }
+        });
   }
 
   /**
    * Gets the schema version number.
    *
    * @returns {number}
    *          The current schema version number.
    */
@@ -299,16 +303,24 @@ class AutofillRecords {
   _ensureMatchingVersion(record) {
     if (record.version != this.version) {
       throw new Error(`Got unknown record version ${
         record.version}; want ${this.version}`);
     }
   }
 
   /**
+   * Initialize the records in the collection, resolves when the migration completes.
+   * @returns {Promise}
+   */
+  initialize() {
+    return this._initializePromise;
+  }
+
+  /**
    * Adds a new record.
    *
    * @param {Object} record
    *        The new record for saving.
    * @param {boolean} [options.sourceSync = false]
    *        Did sync generate this addition?
    * @returns {Promise<string>}
    *          The GUID of the newly added item..
@@ -1171,36 +1183,47 @@ class AutofillRecords {
   }
 
   _findIndexByGUID(guid, {includeDeleted = false} = {}) {
     return this._data.findIndex(record => {
       return record.guid == guid && (!record.deleted || includeDeleted);
     });
   }
 
-  async _migrateRecord(record) {
+  async _migrateRecord(record, index) {
     let hasChanges = false;
 
     if (record.deleted) {
       return hasChanges;
     }
 
     if (!record.version || isNaN(record.version) || record.version < 1) {
       this.log.warn("Invalid record version:", record.version);
 
       // Force to run the migration.
       record.version = 0;
     }
 
     if (record.version < this.version) {
       hasChanges = true;
-      record.version = this.version;
+
+      record = await this._computeMigratedRecord(record);
 
-      // Force to recompute fields if we upgrade the schema.
-      await this._stripComputedFields(record);
+      if (record.deleted) {
+        // record is deleted by _computeMigratedRecord(),
+        // go ahead and put it in the store.
+        this._data[index] = record;
+        return hasChanges;
+      }
+
+      // Compute the computed fields before putting it to store.
+      await this.computeFields(record);
+      this._data[index] = record;
+
+      return hasChanges;
     }
 
     hasChanges |= await this.computeFields(record);
     return hasChanges;
   }
 
   _normalizeRecord(record, preserveEmptyFields = false) {
     this._normalizeFields(record);
@@ -1251,16 +1274,34 @@ class AutofillRecords {
     this._store.data[this._collectionName] = [];
     this._store.saveSoon();
     Services.obs.notifyObservers({wrappedJSObject: {
       sourceSync,
       collectionName: this._collectionName,
     }}, "formautofill-storage-changed", "removeAll");
   }
 
+  /**
+   * Strip the computed fields based on the record version.
+   * @param   {Object} record      The record to migrate
+   * @returns {Object}             Migrated record.
+   *                               Record is always cloned, with version updated,
+   *                               with computed fields stripped.
+   *                               Could be a tombstone record, if the record
+   *                               should be discorded.
+   */
+  async _computeMigratedRecord(record) {
+    if (!record.deleted) {
+      record = this._clone(record);
+      await this._stripComputedFields(record);
+      record.version = this.version;
+    }
+    return record;
+  }
+
   async _stripComputedFields(record) {
     this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
   }
 
   // An interface to be inherited.
   _recordReadProcessor(record) {}
 
   // An interface to be inherited.
@@ -1599,28 +1640,82 @@ 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 _computeMigratedRecord(creditCard) {
+    if (creditCard["cc-number-encrypted"]) {
+      switch (creditCard.version) {
+        case 1: {
+          if (!cryptoSDR.isLoggedIn) {
+            // We cannot decrypt the data, so silently remove the record for
+            // the user.
+            if (creditCard.deleted) {
+              break;
+            }
+
+            this.log.warn("Removing version 1 credit card record to migrate to new encryption:", creditCard.guid);
+
+            // Replace the record with a tombstone record here,
+            // regardless of existence of sync metadata.
+            let existingSync = this._getSyncMetaData(creditCard);
+            creditCard = {
+              guid: creditCard.guid,
+              timeLastModified: Date.now(),
+              deleted: true,
+            };
+
+            if (existingSync) {
+              creditCard._sync = existingSync;
+              existingSync.changeCounter++;
+            }
+            break;
+          }
+
+          creditCard = this._clone(creditCard);
+
+          // Decrypt the cc-number using version 1 encryption.
+          let ccNumber = cryptoSDR.decrypt(creditCard["cc-number-encrypted"]);
+          // Re-encrypt the cc-number with version 2 encryption.
+          creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
+          break;
+        }
+
+        default:
+          throw new Error("Unknown credit card version to migrate: " + creditCard.version);
+      }
+    }
+    return super._computeMigratedRecord(creditCard);
+  }
+
   async _stripComputedFields(creditCard) {
     if (creditCard["cc-number-encrypted"]) {
-      creditCard["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+      try {
+        creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
+      } catch (ex) {
+        if (ex.result == Cr.NS_ERROR_ABORT) {
+          throw ex;
+        }
+        // Quietly recover from encryption error,
+        // so existing credit card entry with undecryptable number
+        // can be updated.
+      }
     }
     await super._stripComputedFields(creditCard);
   }
 
   _normalizeFields(creditCard) {
     this._normalizeCCName(creditCard);
     this._normalizeCCNumber(creditCard);
     this._normalizeCCExpirationDate(creditCard);
@@ -1671,38 +1766,62 @@ class CreditCards extends AutofillRecord
   }
 
   _validateFields(creditCard) {
     if (!creditCard["cc-number"]) {
       throw new Error("Missing/invalid cc-number");
     }
   }
 
+  _ensureMatchingVersion(record) {
+    if (!record.version || isNaN(record.version) || record.version < 1) {
+      throw new Error(`Got invalid record version ${
+        record.version}; want ${this.version}`);
+    }
+
+    if (record.version < this.version) {
+      switch (record.version) {
+        case 1:
+          // The difference between version 1 and 2 is only about the encryption
+          // method used for the cc-number-encrypted field.
+          // As long as the record is already decrypted, it is safe to bump the
+          // version directly.
+          if (!record["cc-number-encrypted"]) {
+            record.version = this.version;
+          } else {
+            throw new Error("Unexpected record migration path.");
+          }
+          break;
+        default:
+          throw new Error("Unknown credit card version to match: " + record.version);
+      }
+    }
+
+    return super._ensureMatchingVersion(record);
+  }
+
   /**
    * Normalize the given record and return the first matched guid if storage has the same record.
    * @param {Object} targetCreditCard
    *        The credit card for duplication checking.
    * @returns {Promise<string|null>}
    *          Return the first guid if storage has the same credit card and null otherwise.
    */
   async getDuplicateGuid(targetCreditCard) {
     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.
-            return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
-          }
-          return (clonedTargetCreditCard[field] == await MasterPassword.decrypt(creditCard["cc-number-encrypted"]));
+          // 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] == creditCard[field];
       })).then(fieldResults => fieldResults.every(result => result));
       if (isDuplicate) {
         return creditCard.guid;
       }
     }
     return null;
@@ -1800,17 +1919,33 @@ FormAutofillStorage.prototype = {
    * @rejects  JavaScript exception.
    */
   initialize() {
     if (!this._initializePromise) {
       this._store = new JSONFile({
         path: this._path,
         dataPostProcessor: this._dataPostProcessor.bind(this),
       });
-      this._initializePromise = this._store.load();
+      this._initializePromise = this._store.load()
+        .then(() => {
+          let initializeAutofillRecords = [this.addresses.initialize()];
+          if (FormAutofill.isAutofillCreditCardsEnabled) {
+            initializeAutofillRecords.push(this.creditCards.initialize());
+          } else {
+            // Make creditCards records unavailable to other modules
+            // because we never initialize it.
+            Object.defineProperty(this, "creditCards", {
+              get() {
+                throw new Error("CreditCards is not initialized. " +
+                                "Please restart if you flip the pref manually.");
+              },
+            });
+          }
+          return Promise.all(initializeAutofillRecords);
+        });
     }
     return this._initializePromise;
   },
 
   _dataPostProcessor(data) {
     data.version = this.version;
     if (!data.addresses) {
       data.addresses = [];
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -12,17 +12,17 @@ const ADDRESS_REFERENCES_EXT = "addressR
 
 const ADDRESSES_COLLECTION_NAME = "addresses";
 const CREDITCARDS_COLLECTION_NAME = "creditCards";
 const MANAGE_ADDRESSES_KEYWORDS = ["manageAddressesTitle", "addNewAddressTitle"];
 const EDIT_ADDRESS_KEYWORDS = [
   "givenName", "additionalName", "familyName", "organization2", "streetAddress",
   "state", "province", "city", "country", "zip", "postalCode", "email", "tel",
 ];
-const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle", "showCreditCardsBtnLabel"];
+const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle"];
 const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear", "cardNetwork"];
 const FIELD_STATES = {
   NORMAL: "NORMAL",
   AUTO_FILLED: "AUTO_FILLED",
   PREVIEW: "PREVIEW",
 };
 const SECTION_TYPES = {
   ADDRESS: "address",
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,251 @@
 /* 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} 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 isLoggedIn() {
+    return !this._isLocked;
   },
 
   /**
-   * @returns {boolean} True if a master password is set and false otherwise.
+   * @returns {boolean} True if there is another login dialog existing and false
+   *                    otherwise.
    */
-  get isEnabled() {
-    return this._token.hasPassword;
+  get isUIBusy() {
+    return !!this._pendingUnlockPromise;
   },
 
   /**
-   * @returns {boolean} True if master password is logged in and false if not.
+   * 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 isLoggedIn() {
-    return Services.logins.isLoggedIn;
+  _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);
+    }
   },
 
   /**
-   * @returns {boolean} True if there is another master password login dialog
-   *                    existing and false otherwise.
-   */
-  get isUIBusy() {
-    return Services.logins.uiBusy;
-  },
-
-  /**
-   * 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/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -15,29 +15,27 @@
 </head>
 <body dir="&locale.dir;">
   <fieldset>
     <legend data-localization="creditCardsListHeader"/>
     <select id="credit-cards" size="9" multiple="multiple"/>
   </fieldset>
   <div id="controls-container">
     <button id="remove" disabled="disabled" data-localization="removeBtnLabel"/>
-    <button id="show-hide-credit-cards" data-localization="showCreditCardsBtnLabel"/>
     <!-- Wrapper is used to properly compute the search tooltip position -->
     <div>
       <button id="add" data-localization="addBtnLabel"/>
     </div>
     <button id="edit" disabled="disabled" data-localization="editBtnLabel"/>
   </div>
   <script type="application/javascript">
     "use strict";
     /* global ManageCreditCards */
     new ManageCreditCards({
       records: document.getElementById("credit-cards"),
       controlsContainer: document.getElementById("controls-container"),
       remove: document.getElementById("remove"),
-      showHideCreditCards: document.getElementById("show-hide-credit-cards"),
       add: document.getElementById("add"),
       edit: document.getElementById("edit"),
     });
   </script>
 </body>
 </html>
--- 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,43 @@ 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._isDecrypted = false;
-    if (this._hasMasterPassword) {
-      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 || await OSKeyStore.ensureLoggedIn(true)) {
       let decryptedCCNumObj = {};
       if (creditCard) {
-        decryptedCCNumObj["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+        try {
+          decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
+        } catch (ex) {
+          if (ex.result == Cr.NS_ERROR_ABORT) {
+            // User shouldn't be ask to reauth here, but it could happen.
+            // Return here and skip opening the dialog.
+            return;
+          }
+          // We've got ourselves a real error.
+          // Recover from encryption error so the user gets a chance to re-enter
+          // unencrypted credit card number.
+          decryptedCCNumObj["cc-number"] = "";
+          Cu.reportError(ex);
+        }
       }
       let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
       this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, "resizable=no", {
         record: decryptedCreditCard,
       });
     }
   }
 
@@ -358,22 +366,16 @@ class ManageCreditCards extends ManageRe
       encryptedNumber: creditCard["cc-number-encrypted"],
       number: creditCard["cc-number"],
       name: creditCard["cc-name"],
       network: creditCard["cc-type"],
     });
     return cardObj.getLabel({showNumbers: showCreditCards});
   }
 
-  async toggleShowHideCards(options) {
-    this._isDecrypted = !this._isDecrypted;
-    this.updateShowHideButtonState();
-    await this.updateLabels(options, this._isDecrypted);
-  }
-
   async updateLabels(options, isDecrypted) {
     for (let option of options) {
       option.text = await this.getLabel(option.record, isDecrypted);
     }
     // For testing only: Notify when credit cards labels have been updated
     this._elements.records.dispatchEvent(new CustomEvent("LabelsUpdated"));
   }
 
@@ -391,30 +393,15 @@ class ManageCreditCards extends ManageRe
         option.setAttribute("cc-type", record["cc-type"]);
       } else {
         option.removeAttribute("cc-type");
       }
     }
   }
 
   updateButtonsStates(selectedCount) {
-    this.updateShowHideButtonState();
     super.updateButtonsStates(selectedCount);
   }
 
-  updateShowHideButtonState() {
-    if (this._elements.records.length) {
-      this._elements.showHideCreditCards.removeAttribute("disabled");
-    } else {
-      this._elements.showHideCreditCards.setAttribute("disabled", true);
-    }
-    this._elements.showHideCreditCards.textContent =
-      this._isDecrypted ? FormAutofillUtils.stringBundle.GetStringFromName("hideCreditCardsBtnLabel") :
-                          FormAutofillUtils.stringBundle.GetStringFromName("showCreditCardsBtnLabel");
-  }
-
   handleClick(event) {
-    if (event.target == this._elements.showHideCreditCards) {
-      this.toggleShowHideCards(this._elements.records.options);
-    }
     super.handleClick(event);
   }
 }
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -99,18 +99,16 @@ savedCreditCardsBtnLabel = Saved Credit Cards…
 # LOCALIZATION NOTE (manageAddressesTitle, manageCreditCardsTitle): The dialog title for the list of addresses or
 # credit cards in browser preferences.
 manageAddressesTitle = Saved Addresses
 manageCreditCardsTitle = Saved Credit Cards
 # LOCALIZATION NOTE (addressesListHeader, creditCardsListHeader): The header for the list of addresses or credit cards
 # in browser preferences.
 addressesListHeader = Addresses
 creditCardsListHeader = Credit Cards
-showCreditCardsBtnLabel = Show Credit Cards
-hideCreditCardsBtnLabel = Hide Credit Cards
 removeBtnLabel = Remove
 addBtnLabel = Add…
 editBtnLabel = Edit…
 # LOCALIZATION NOTE (manageDialogsWidth): This strings sets the default width for windows used to manage addresses and
 # credit cards.
 manageDialogsWidth = 560px
 
 # LOCALIZATION NOTE (addNewAddressTitle, editAddressTitle): The dialog title for creating or editing addresses
--- 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
@@ -1,32 +1,29 @@
 "use strict";
 
 const TEST_SELECTORS = {
   selRecords: "#credit-cards",
   btnRemove: "#remove",
-  btnShowHideCreditCards: "#show-hide-credit-cards",
   btnAdd: "#add",
   btnEdit: "#edit",
 };
 
 const DIALOG_SIZE = "width=600,height=400";
 
 add_task(async function test_manageCreditCardsInitialState() {
   await BrowserTestUtils.withNewTab({gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL}, async function(browser) {
     await ContentTask.spawn(browser, TEST_SELECTORS, (args) => {
       let selRecords = content.document.querySelector(args.selRecords);
       let btnRemove = content.document.querySelector(args.btnRemove);
-      let btnShowHideCreditCards = content.document.querySelector(args.btnShowHideCreditCards);
       let btnAdd = content.document.querySelector(args.btnAdd);
       let btnEdit = content.document.querySelector(args.btnEdit);
 
       is(selRecords.length, 0, "No credit card");
       is(btnRemove.disabled, true, "Remove button disabled");
-      is(btnShowHideCreditCards.disabled, true, "Show Credit Cards button disabled");
       is(btnAdd.disabled, false, "Add button enabled");
       is(btnEdit.disabled, true, "Edit button disabled");
     });
   });
 });
 
 add_task(async function test_cancelManageCreditCardsDialogWithESC() {
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL);
@@ -102,67 +99,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 +134,51 @@ 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");
+  EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
 
-  // 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
@@ -311,17 +311,17 @@ add_task(async function test_add() {
   let creditCards = await profileStorage.creditCards.getAll();
 
   Assert.equal(creditCards.length, 2);
 
   do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
   do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
 
   Assert.notEqual(creditCards[0].guid, undefined);
-  Assert.equal(creditCards[0].version, 1);
+  Assert.equal(creditCards[0].version, 2);
   Assert.notEqual(creditCards[0].timeCreated, undefined);
   Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated);
   Assert.equal(creditCards[0].timeLastUsed, 0);
   Assert.equal(creditCards[0].timesUsed, 0);
 
   // Empty string should be deleted before saving.
   await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
   let creditCard = profileStorage.creditCards._data[2];
@@ -417,16 +417,24 @@ add_task(async function test_update() {
   creditCard = profileStorage.creditCards._data[0];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
   await profileStorage.creditCards.update(profileStorage.creditCards._data[1].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, true);
   creditCard = profileStorage.creditCards._data[1];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
 
+  // Decryption failure of existing record should not prevent it from being updated.
+  creditCard = profileStorage.creditCards._data[0];
+  creditCard["cc-number-encrypted"] = "INVALID";
+  await profileStorage.creditCards.update(profileStorage.creditCards._data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, false);
+  creditCard = profileStorage.creditCards._data[0];
+  Assert.equal(creditCard["cc-number"],
+    CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
+
   await Assert.rejects(profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
     /No matching record\./
   );
 
   await Assert.rejects(profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_WITH_INVALID_FIELD),
     /"invalidField" is not a valid field\./
   );
 
@@ -650,23 +658,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();
-  });
-});
--- a/browser/extensions/formautofill/test/unit/test_migrateRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_migrateRecords.js
@@ -1,24 +1,27 @@
 /**
  * Tests the migration algorithm in profileStorage.
  */
 
 "use strict";
 
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+
 let FormAutofillStorage;
-
+let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {}));
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 const TEST_STORE_FILE_NAME = "test-profile.json";
 
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
 
 const ADDRESS_TESTCASES = [
   {
     description: "The record version is equal to the current version. The migration shouldn't be invoked.",
     record: {
       guid: "test-guid",
       version: ADDRESS_SCHEMA_VERSION,
       "given-name": "Timothy",
@@ -241,27 +244,98 @@ let do_check_record_matches = (expectedR
 };
 
 add_task(async function test_migrateAddressRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  await Promise.all(ADDRESS_TESTCASES.map(async testcase => {
+  for (let testcase of ADDRESS_TESTCASES) {
     info(testcase.description);
-    await profileStorage.addresses._migrateRecord(testcase.record);
-    do_check_record_matches(testcase.expectedResult, testcase.record);
-  }));
+    profileStorage._store.data.addresses = [testcase.record];
+    await profileStorage.addresses._migrateRecord(testcase.record, 0);
+    do_check_record_matches(testcase.expectedResult, profileStorage.addresses._data[0]);
+  }
 });
 
 add_task(async function test_migrateCreditCardRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  await Promise.all(CREDIT_CARD_TESTCASES.map(async testcase => {
+  for (let testcase of CREDIT_CARD_TESTCASES) {
     info(testcase.description);
-    await profileStorage.creditCards._migrateRecord(testcase.record);
-    do_check_record_matches(testcase.expectedResult, testcase.record);
-  }));
+    profileStorage._store.data.creditCards = [testcase.record];
+    await profileStorage.creditCards._migrateRecord(testcase.record, 0);
+    do_check_record_matches(testcase.expectedResult, profileStorage.creditCards._data[0]);
+  }
 });
+
+add_task(async function test_migrateEncryptedCreditCardNumber() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new FormAutofillStorage(path);
+  await profileStorage.initialize();
+
+  const ccNumber = "4111111111111111";
+  let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"]
+    .createInstance(Ci.nsILoginManagerCrypto);
+
+  info("Encrypted credit card should be migrated from v1 to v2");
+
+  let record = {
+    guid: "test-guid",
+    version: 1,
+    "cc-name": "Timothy",
+    "cc-number-encrypted": cryptoSDR.encrypt(ccNumber),
+  };
+
+  let expectedRecord = {
+    guid: "test-guid",
+    version: CREDIT_CARD_SCHEMA_VERSION,
+    "cc-name": "Timothy",
+    "cc-given-name": "Timothy",
+  };
+  profileStorage._store.data.creditCards = [record];
+  await profileStorage.creditCards._migrateRecord(record, 0);
+  record = profileStorage.creditCards._data[0];
+
+  Assert.equal(expectedRecord.guid, record.guid);
+  Assert.equal(expectedRecord.version, record.version);
+  Assert.equal(expectedRecord["cc-name"], record["cc-name"]);
+  Assert.equal(expectedRecord["cc-given-name"], record["cc-given-name"]);
+
+  // Ciphertext of OS Key Store is not stable, must compare decrypted text here.
+  Assert.equal(ccNumber, await OSKeyStore.decrypt(record["cc-number-encrypted"]));
+});
+
+add_task(async function test_migrateEncryptedCreditCardNumberWithMP() {
+  LoginTestUtils.masterPassword.enable();
+
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new FormAutofillStorage(path);
+  await profileStorage.initialize();
+
+  info("Encrypted credit card should be migrated a tombstone if MP is enabled");
+
+  let record = {
+    guid: "test-guid",
+    version: 1,
+    "cc-name": "Timothy",
+    "cc-number-encrypted": "(encrypted to be discarded)",
+  };
+
+  profileStorage._store.data.creditCards = [record];
+  await profileStorage.creditCards._migrateRecord(record, 0);
+  record = profileStorage.creditCards._data[0];
+
+  Assert.equal(record.guid, "test-guid");
+  Assert.equal(record.deleted, true);
+  Assert.equal(typeof record.version, "undefined");
+  Assert.equal(typeof record["cc-name"], "undefined");
+  Assert.equal(typeof record["cc-number-encrypted"], "undefined");
+
+  LoginTestUtils.masterPassword.disable();
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_osKeyStore.js
@@ -0,0 +1,144 @@
+/**
+ * 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(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/test_reconcile.js
+++ b/browser/extensions/formautofill/test/unit/test_reconcile.js
@@ -465,220 +465,220 @@ const ADDRESS_RECONCILE_TESTCASES = [
 ];
 
 const CREDIT_CARD_RECONCILE_TESTCASES = [
   {
     description: "Local change",
     parent: {
       // So when we last wrote the record to the server, it had these values.
       "guid": "2bbd2d8fbc6b",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current local record - by comparing against parent we can see that
       // only the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       // This is the incoming record. It has the same values as "parent", so
       // we can deduce the record hasn't actually been changed remotely so we
       // can safely ignore the incoming record and write our local changes.
       "guid": "2bbd2d8fbc6b",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "2bbd2d8fbc6b",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Remote change",
     parent: {
       "guid": "e3680e9f890d",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "e3680e9f890d",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "e3680e9f890d",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
 
   {
     description: "New local field",
     parent: {
       "guid": "0cba738b1be0",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "0cba738b1be0",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "0cba738b1be0",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "New remote field",
     parent: {
       "guid": "be3ef97f8285",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "be3ef97f8285",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "be3ef97f8285",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "Deleted field locally",
     parent: {
       "guid": "9627322248ec",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "9627322248ec",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "9627322248ec",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Deleted field remotely",
     parent: {
       "guid": "7d7509f3eeb2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "7d7509f3eeb2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "7d7509f3eeb2",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Local and remote changes to unrelated fields",
     parent: {
       // The last time we wrote this to the server, "cc-exp-month" was 12.
       "guid": "e087a06dfc57",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       // The current local record - so locally we've changed "cc-number".
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 12,
     }],
     remote: {
       // Remotely, we've changed "cc-exp-month" to 1.
       "guid": "e087a06dfc57",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 1,
     },
     reconciled: {
       "guid": "e087a06dfc57",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 1,
     },
   },
   {
     description: "Multiple local changes",
     parent: {
       "guid": "340a078c596f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
     }, {
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "340a078c596f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-year": 2000,
     },
     reconciled: {
       "guid": "340a078c596f",
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
@@ -687,54 +687,54 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local and remote diverged from the shared parent, but the values are the
     // same, so we shouldn't fork.
     description: "Same change to local and remote",
     parent: {
       "guid": "0b3a72a1bea2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "0b3a72a1bea2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "0b3a72a1bea2",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to single field",
     parent: {
       // This is what we last wrote to the sync server.
       "guid": "62068784d089",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current version of the local record - the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
     }],
     remote: {
       // An incoming record has a different cc-number than any of the above!
       "guid": "62068784d089",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     forked: {
       // So we've forked the local record to a new GUID (and the next sync is
       // going to write this as a new record)
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
@@ -745,29 +745,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to multiple fields",
     parent: {
       "guid": "244dbb692e94",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
     }],
     remote: {
       "guid": "244dbb692e94",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
@@ -778,28 +778,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field deleted locally, changed remotely",
     parent: {
       "guid": "6fc45e03d19a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "6fc45e03d19a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
@@ -809,29 +809,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field changed locally, deleted remotely",
     parent: {
       "guid": "fff9fa27fa18",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     }],
     remote: {
       "guid": "fff9fa27fa18",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
@@ -843,28 +843,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
   },
   {
     // Created, last modified should be synced; last used and times used should
     // be local. Remote created time older than local, remote modified time
     // newer than local.
     description: "Created, last modified time reconciliation without local changes",
     parent: {
       "guid": "5113f329c42f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [],
     remote: {
       "guid": "5113f329c42f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1200,
       "timeLastModified": 5700,
       "timeLastUsed": 5700,
       "timesUsed": 3,
     },
     reconciled: {
@@ -878,31 +878,31 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local changes, remote created time newer than local, remote modified time
     // older than local.
     description: "Created, last modified time reconciliation with local changes",
     parent: {
       "guid": "791e5608b80a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "791e5608b80a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1300,
       "timeLastModified": 5000,
       "timeLastUsed": 5000,
       "timesUsed": 3,
     },
     reconciled: {
@@ -917,17 +917,17 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
 ];
 
 add_task(async function test_reconcile_unknown_version() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
   // Cross-version reconciliation isn't supported yet. See bug 1377204.
   await Assert.rejects(profileStorage.addresses.reconcile({
     "guid": "31d83d2725ec",
-    "version": 2,
+    "version": 3,
     "given-name": "Mark",
     "family-name": "Hammond",
   }), /Got unknown record version/);
 });
 
 add_task(async function test_reconcile_idempotent() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
--- 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/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -60,16 +60,18 @@
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/NullPrincipal.h"
 #include <stdint.h>
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/StaticPtr.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
 #include "nsContentUtils.h"
 #include "nsJSUtils.h"
 #include "nsILoadInfo.h"
 #include "nsIDOMXULCommandDispatcher.h"
 #include "nsITreeSelection.h"
 
 // This should be probably defined on some other place... but I couldn't find it
 #define WEBAPPS_PERM_NAME "webapps-manage"
@@ -487,16 +489,25 @@ nsScriptSecurityManager::ContentSecurity
     nsCOMPtr<nsIContentSecurityPolicy> csp;
     nsresult rv = subjectPrincipal->GetCsp(getter_AddRefs(csp));
     NS_ASSERTION(NS_SUCCEEDED(rv), "CSP: Failed to get CSP from principal.");
 
     // don't do anything unless there's a CSP
     if (!csp)
         return true;
 
+    nsCOMPtr<nsICSPEventListener> cspEventListener;
+    if (!NS_IsMainThread()) {
+      WorkerPrivate* workerPrivate =
+        mozilla::dom::GetWorkerPrivateFromContext(cx);
+      if (workerPrivate) {
+        cspEventListener = workerPrivate->CSPEventListener();
+      }
+    }
+
     bool evalOK = true;
     bool reportViolation = false;
     rv = csp->GetAllowsEval(&reportViolation, &evalOK);
 
     if (NS_FAILED(rv))
     {
         NS_WARNING("CSP: failed to get allowsEval");
         return true; // fail open to not break sites.
@@ -524,16 +535,17 @@ nsScriptSecurityManager::ContentSecurity
             if (const char *file = scriptFilename.get()) {
                 CopyUTF8toUTF16(nsDependentCString(file), fileName);
             }
         } else {
             MOZ_ASSERT(!JS_IsExceptionPending(cx));
         }
         csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
                                  nullptr, // triggering element
+                                 cspEventListener,
                                  fileName,
                                  scriptSample,
                                  lineNum,
                                  columnNum,
                                  EmptyString(),
                                  EmptyString());
     }
 
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -12373,16 +12373,17 @@ nsIDocument::InlineScriptAllowedByCSP()
   nsresult rv = NodePrincipal()->GetCsp(getter_AddRefs(csp));
   NS_ENSURE_SUCCESS(rv, true);
   bool allowsInlineScript = true;
   if (csp) {
     nsresult rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
                                        EmptyString(), // aNonce
                                        true,          // aParserCreated
                                        nullptr,       // aTriggeringElement
+                                       nullptr,       // aCSPEventListener
                                        EmptyString(), // FIXME get script sample (bug 1314567)
                                        0,             // aLineNumber
                                        0,             // aColumnNumber
                                        &allowsInlineScript);
     NS_ENSURE_SUCCESS(rv, true);
   }
   return allowsInlineScript;
 }
--- a/dom/canvas/ImageBitmap.cpp
+++ b/dom/canvas/ImageBitmap.cpp
@@ -1392,17 +1392,17 @@ ImageBitmap::Create(nsIGlobalObject* aGl
 
   RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
 
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   if (aCropRect.isSome() && (aCropRect->Width() == 0 || aCropRect->Height() == 0)) {
-    aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+    aRv.Throw(NS_ERROR_RANGE_ERR);
     return promise.forget();
   }
 
   RefPtr<ImageBitmap> imageBitmap;
 
   if (aSrc.IsHTMLImageElement()) {
     MOZ_ASSERT(NS_IsMainThread(),
                "Creating ImageBitmap from HTMLImageElement off the main thread.");
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -850,16 +850,17 @@ EventListenerManager::SetEventHandler(ns
     }
 
     if (csp) {
       bool allowsInlineScript = true;
       rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
                                 EmptyString(), // aNonce
                                 true, // aParserCreated (true because attribute event handler)
                                 aElement,
+                                nullptr, // nsICSPEventListener
                                 aBody,
                                 lineNum,
                                 columnNum,
                                 &allowsInlineScript);
       NS_ENSURE_SUCCESS(rv, rv);
 
       // return early if CSP wants us to block inline scripts
       if (!allowsInlineScript) {
--- a/dom/fetch/Fetch.cpp
+++ b/dom/fetch/Fetch.cpp
@@ -398,27 +398,30 @@ private:
   }
 };
 
 class MainThreadFetchRunnable : public Runnable
 {
   RefPtr<WorkerFetchResolver> mResolver;
   const ClientInfo mClientInfo;
   const Maybe<ServiceWorkerDescriptor> mController;
+  nsCOMPtr<nsICSPEventListener> mCSPEventListener;
   RefPtr<InternalRequest> mRequest;
 
 public:
   MainThreadFetchRunnable(WorkerFetchResolver* aResolver,
                           const ClientInfo& aClientInfo,
                           const Maybe<ServiceWorkerDescriptor>& aController,
+                          nsICSPEventListener* aCSPEventListener,
                           InternalRequest* aRequest)
     : Runnable("dom::MainThreadFetchRunnable")
     , mResolver(aResolver)
     , mClientInfo(aClientInfo)
     , mController(aController)
+    , mCSPEventListener(aCSPEventListener)
     , mRequest(aRequest)
   {
     MOZ_ASSERT(mResolver);
   }
 
   NS_IMETHOD
   Run() override
   {
@@ -449,16 +452,17 @@ public:
       nsAutoCString spec;
       if (proxy->GetWorkerPrivate()->GetBaseURI()) {
         proxy->GetWorkerPrivate()->GetBaseURI()->GetAsciiSpec(spec);
       }
       fetch->SetWorkerScript(spec);
 
       fetch->SetClientInfo(mClientInfo);
       fetch->SetController(mController);
+      fetch->SetCSPEventListener(mCSPEventListener);
     }
 
     RefPtr<AbortSignalImpl> signalImpl =
       mResolver->GetAbortSignalForMainThread();
 
     // ...but release it before calling Fetch, because mResolver's callback can
     // be called synchronously and they want the mutex, too.
     return fetch->Fetch(signalImpl, mResolver);
@@ -581,17 +585,18 @@ FetchRequest(nsIGlobalObject* aGlobal, c
     Maybe<ClientInfo> clientInfo(worker->GetClientInfo());
     if (clientInfo.isNothing()) {
       aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
       return nullptr;
     }
 
     RefPtr<MainThreadFetchRunnable> run =
       new MainThreadFetchRunnable(resolver, clientInfo.ref(),
-                                  worker->GetController(), r);
+                                  worker->GetController(),
+                                  worker->CSPEventListener(), r);
     worker->DispatchToMainThread(run.forget());
   }
 
   return p.forget();
 }
 
 void
 MainThreadFetchResolver::OnResponseAvailableInternal(InternalResponse* aResponse)
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -575,16 +575,26 @@ FetchDriver::HttpFetch(const nsACString&
                        mPerformanceStorage,
                        mLoadGroup,
                        nullptr, /* aCallbacks */
                        loadFlags,
                        ios);
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (mCSPEventListener) {
+    nsCOMPtr<nsILoadInfo> loadInfo = chan->GetLoadInfo();
+    if (NS_WARN_IF(!loadInfo)) {
+      return NS_ERROR_UNEXPECTED;
+    }
+
+    rv = loadInfo->SetCspEventListener(mCSPEventListener);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   // Insert ourselves into the notification callbacks chain so we can set
   // headers on redirects.
 #ifdef DEBUG
   {
     nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks;
     chan->GetNotificationCallbacks(getter_AddRefs(notificationCallbacks));
     MOZ_ASSERT(!notificationCallbacks);
   }
@@ -1411,16 +1421,23 @@ void
 FetchDriver::SetDocument(nsIDocument* aDocument)
 {
   // Cannot set document after Fetch() has been called.
   MOZ_ASSERT(!mFetchCalled);
   mDocument = aDocument;
 }
 
 void
+FetchDriver::SetCSPEventListener(nsICSPEventListener* aCSPEventListener)
+{
+  MOZ_ASSERT(!mFetchCalled);
+  mCSPEventListener = aCSPEventListener;
+}
+
+void
 FetchDriver::SetClientInfo(const ClientInfo& aClientInfo)
 {
   MOZ_ASSERT(!mFetchCalled);
   mClientInfo.emplace(aClientInfo);
 }
 
 void
 FetchDriver::SetController(const Maybe<ServiceWorkerDescriptor>& aController)
--- a/dom/fetch/FetchDriver.h
+++ b/dom/fetch/FetchDriver.h
@@ -16,16 +16,17 @@
 #include "mozilla/dom/AbortSignal.h"
 #include "mozilla/dom/SRIMetadata.h"
 #include "mozilla/RefPtr.h"
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/net/ReferrerPolicy.h"
 
 class nsIConsoleReportCollector;
+class nsICSPEventListener;
 class nsIDocument;
 class nsIEventTarget;
 class nsIOutputStream;
 class nsILoadGroup;
 class nsIPrincipal;
 
 namespace mozilla {
 namespace dom {
@@ -115,16 +116,19 @@ public:
 
   nsresult Fetch(AbortSignalImpl* aSignalImpl,
                  FetchDriverObserver* aObserver);
 
   void
   SetDocument(nsIDocument* aDocument);
 
   void
+  SetCSPEventListener(nsICSPEventListener* aCSPEventListener);
+
+  void
   SetClientInfo(const ClientInfo& aClientInfo);
 
   void
   SetController(const Maybe<ServiceWorkerDescriptor>& aController);
 
   void
   SetWorkerScript(const nsACString& aWorkerScirpt)
   {
@@ -139,16 +143,17 @@ public:
 private:
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsCOMPtr<nsILoadGroup> mLoadGroup;
   RefPtr<InternalRequest> mRequest;
   RefPtr<InternalResponse> mResponse;
   nsCOMPtr<nsIOutputStream> mPipeOutputStream;
   RefPtr<FetchDriverObserver> mObserver;
   nsCOMPtr<nsIDocument> mDocument;
+  nsCOMPtr<nsICSPEventListener> mCSPEventListener;
   Maybe<ClientInfo> mClientInfo;
   Maybe<ServiceWorkerDescriptor> mController;
   nsCOMPtr<nsIChannel> mChannel;
   nsAutoPtr<SRICheckDataVerifier> mSRIDataVerifier;
   nsCOMPtr<nsIEventTarget> mMainThreadEventTarget;
 
   // This is set only when Fetch is used in workers.
   RefPtr<PerformanceStorage> mPerformanceStorage;
--- a/dom/html/HTMLFormElement.cpp
+++ b/dom/html/HTMLFormElement.cpp
@@ -1674,17 +1674,17 @@ HTMLFormElement::GetActionURL(nsIURI** a
   rv = NodePrincipal()->GetCsp(getter_AddRefs(csp));
   NS_ENSURE_SUCCESS(rv, rv);
   if (csp) {
     bool permitsFormAction = true;
 
     // form-action is only enforced if explicitly defined in the
     // policy - do *not* consult default-src, see:
     // http://www.w3.org/TR/CSP2/#directive-default-src
-    rv = csp->Permits(this, actionURL,
+    rv = csp->Permits(this, nullptr /* nsICSPEventListener */, actionURL,
                       nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE,
                       true, &permitsFormAction);
     NS_ENSURE_SUCCESS(rv, rv);
     if (!permitsFormAction) {
       return NS_ERROR_CSP_FORM_ACTION_VIOLATION;
     }
   }
 
--- a/dom/html/HTMLSharedElement.cpp
+++ b/dom/html/HTMLSharedElement.cpp
@@ -170,17 +170,18 @@ SetBaseURIUsingFirstBaseWithHref(nsIDocu
       if (NS_FAILED(rv)) {
         newBaseURI = nullptr;
       }
       if (csp && newBaseURI) {
         // base-uri is only enforced if explicitly defined in the
         // policy - do *not* consult default-src, see:
         // http://www.w3.org/TR/CSP2/#directive-default-src
         bool cspPermitsBaseURI = true;
-        rv = csp->Permits(child->AsElement(), newBaseURI,
+        rv = csp->Permits(child->AsElement(), nullptr /* nsICSPEventListener */,
+                          newBaseURI,
                           nsIContentSecurityPolicy::BASE_URI_DIRECTIVE,
                           true, &cspPermitsBaseURI);
         if (NS_FAILED(rv) || !cspPermitsBaseURI) {
           newBaseURI = nullptr;
         }
       }
       aDocument->SetBaseURI(newBaseURI);
       aDocument->SetChromeXHRDocBaseURI(nullptr);
--- a/dom/interfaces/security/nsIContentSecurityPolicy.idl
+++ b/dom/interfaces/security/nsIContentSecurityPolicy.idl
@@ -136,16 +136,17 @@ interface nsIContentSecurityPolicy : nsI
    * @return
    *     Whether or not the effects of the inline style should be allowed
    *     (block the rules if false).
    */
   boolean getAllowsInline(in nsContentPolicyType aContentPolicyType,
                           in AString aNonce,
                           in boolean aParserCreated,
                           in Element aTriggeringElement,
+                          in nsICSPEventListener aCSPEventListener,
                           in AString aContentOfPseudoScript,
                           in unsigned long aLineNumber,
                           in unsigned long aColumnNumber);
 
   /**
    * whether this policy allows eval and eval-like functions
    * such as setTimeout("code string", time).
    * @param shouldReportViolations
@@ -194,16 +195,17 @@ interface nsIContentSecurityPolicy : nsI
    * @param aContent
    *     (optional) If this is a hash violation, include contents of the inline
    *     resource in the question so we can recheck the hash in order to
    *     determine which policies were violated and send the appropriate
    *     reports.
    */
   void logViolationDetails(in unsigned short violationType,
                            in Element triggeringElement,
+                           in nsICSPEventListener aCSPEventListener,
                            in AString sourceFile,
                            in AString scriptSample,
                            in int32_t lineNum,
                            in int32_t columnNum,
                            [optional] in AString nonce,
                            [optional] in AString content);
 
   const unsigned short VIOLATION_TYPE_INLINE_SCRIPT          = 1;
@@ -221,23 +223,16 @@ interface nsIContentSecurityPolicy : nsI
    * context. Either use
    *  * aDocument (preferred), or if no document is available, then provide
    *  * aPrincipal
    */
   void setRequestContext(in Document aDocument,
                          in nsIPrincipal aPrincipal);
 
   /**
-   * Called after the CSP object is created to fill in event listener.
-   * @param aListener this listener will be notified when a CSP violation event
-   *                  must be dispatched.
-   */
-  void setEventListener(in nsICSPEventListener aListener);
-
-  /**
    *  Ensure we have a nsIEventTarget to use to label CSPReportSenderRunnable
    */
   [noscript] void ensureEventTarget(in nsIEventTarget aEventTarget);
 
   /*
    * Checks if a CSP requires Subresource Integrity (SRI)
    * for a given nsContentPolicyType.
    */
@@ -278,16 +273,17 @@ interface nsIContentSecurityPolicy : nsI
    *    "false" allows CSP to fall back to default-src.  This function
    *    behaves the same for both values of canUseDefault when querying
    *    directives that don't fall-back.
    * @return
    *    Whether or not the provided URI is allowed by CSP under the given
    *    directive. (block the pending operation if false).
    */
   boolean permits(in Element aTriggeringElement,
+                  in nsICSPEventListener aCSPEventListener,
                   in nsIURI aURI,
                   in CSPDirective aDir,
                   in boolean aSpecific);
 
   /**
    * Delegate method called by the service when sub-elements of the protected
    * document are being loaded.  Given a bit of information about the request,
    * decides whether or not the policy is satisfied.
@@ -295,16 +291,17 @@ interface nsIContentSecurityPolicy : nsI
    * Calls to this may trigger violation reports when queried, so
    * this value should not be cached.
    *
    * aOriginalURIIfRedirect must be passed only if this loading is the result
    * of a redirect. In this case, aOriginalURIIfRedirect must be the original
    * URL.
    */
   short shouldLoad(in nsContentPolicyType aContentType,
+                   in nsICSPEventListener aCSPEventListener,
                    in nsIURI          aContentLocation,
                    in nsIURI          aRequestOrigin,
                    in nsISupports     aContext,
                    in ACString        aMimeTypeGuess,
                    in nsIURI          aOriginalURIIfRedirect,
                    in bool            aSendViolationReports);
 
 %{ C++
--- a/dom/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/jsurl/nsJSProtocolHandler.cpp
@@ -176,16 +176,17 @@ nsresult nsJSThunk::EvaluateScript(nsICh
     rv = principal->GetCsp(getter_AddRefs(csp));
     NS_ENSURE_SUCCESS(rv, rv);
     if (csp) {
         bool allowsInlineScript = true;
         rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
                                   EmptyString(), // aNonce
                                   true,          // aParserCreated
                                   nullptr,       // aElement,
+                                  nullptr,       // nsICSPEventListener
                                   EmptyString(), // aContent
                                   0,             // aLineNumber
                                   0,             // aColumnNumber
                                   &allowsInlineScript);
 
         //return early if inline scripts are not allowed
         if (!allowsInlineScript) {
           return NS_ERROR_DOM_RETVAL_UNDEFINED;
--- a/dom/media/CubebUtils.cpp
+++ b/dom/media/CubebUtils.cpp
@@ -53,18 +53,20 @@
 #define PREF_AUDIOIPC_STACK_SIZE "media.audioipc.stack_size"
 
 #if (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)) || defined(XP_MACOSX)
 #define MOZ_CUBEB_REMOTING
 #endif
 
 extern "C" {
 
+// This must match AudioIpcInitParams in media/audioipc/client/src/lib.rs.
+// TODO: Generate this from the Rust definition rather than duplicating it.
 struct AudioIpcInitParams {
-  int mServerConnection;
+  mozilla::ipc::FileDescriptor::PlatformHandleType mServerConnection;
   size_t mPoolSize;
   size_t mStackSize;
   void (*mThreadCreateCallback)(const char*);
 };
 
 // These functions are provided by audioipc-server crate
 extern void* audioipc_server_start();
 extern mozilla::ipc::FileDescriptor::PlatformHandleType audioipc_server_new_client(void*);
@@ -406,19 +408,29 @@ void InitAudioIPCConnection()
                 });
 }
 #endif
 
 ipc::FileDescriptor CreateAudioIPCConnection()
 {
 #ifdef MOZ_CUBEB_REMOTING
   MOZ_ASSERT(sServerHandle);
-  int rawFD = audioipc_server_new_client(sServerHandle);
+  ipc::FileDescriptor::PlatformHandleType rawFD = audioipc_server_new_client(sServerHandle);
   ipc::FileDescriptor fd(rawFD);
+  if (!fd.IsValid()) {
+    MOZ_LOG(gCubebLog, LogLevel::Error, ("audioipc_server_new_client failed"));
+    return ipc::FileDescriptor();
+  }
+  // Close rawFD since FileDescriptor's ctor cloned it.
+  // TODO: Find cleaner cross-platform way to close rawFD.
+#ifdef XP_WIN
+  CloseHandle(rawFD);
+#else
   close(rawFD);
+#endif
   return fd;
 #else
   return ipc::FileDescriptor();
 #endif
 }
 
 cubeb* GetCubebContextUnlocked()
 {
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -1773,41 +1773,44 @@ PeerConnectionWrapper.prototype = {
 
   /**
    * Checks that we are getting the media streams we expect.
    *
    * @param {object} stats
    *        The stats to check from this PeerConnectionWrapper
    */
   checkStats : function(stats, twoMachines) {
+    // Allow for clock drift observed on Windows 7. (Bug 979649)
+    const isWin7 = navigator.userAgent.includes("Windows NT 6.1");
+    const clockDriftAllowance = isWin7 ? 1000 : 0;
+
     // Use spec way of enumerating stats
     var counters = {};
     for (let [key, res] of stats) {
       info("Checking stats for " + key + " : " + res);
       // validate stats
       ok(res.id == key, "Coherent stats id");
-      var nowish = Date.now();
+      var nowish = Date.now() + clockDriftAllowance;
       var minimum = this.whenCreated;
-      if (false) { // Bug 1325430 - timestamps aren't working properly in update 49
-      // if (!twoMachines) {
+      if (!twoMachines) {
         // Bug 1225729: On android, sometimes the first RTCP of the first
         // test run gets this value, likely because no RTP has been sent yet.
-        if (res.timestamp != 2085978496000) {
+        if (false) {
           ok(res.timestamp >= minimum,
              "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
                  res.timestamp + " >= " + minimum + " (" +
                  (res.timestamp - minimum) + " ms)");
-          ok(res.timestamp <= nowish,
-             "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
-                 res.timestamp + " <= " + nowish + " (" +
-                 (res.timestamp - nowish) + " ms)");
         } else {
-          info("Bug 1225729: Uninitialized timestamp (" + res.timestamp +
-                "), should be >=" + minimum + " and <= " + nowish);
+          info("FIXME bug 1495446: uncomment the timestamp test case " +
+               "above after RTCP epoch bug 1495446 is fixed.");
         }
+        ok(res.timestamp <= nowish,
+           "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
+               res.timestamp + " <= " + nowish + " (" +
+               (res.timestamp - nowish) + " ms)");
       }
       if (res.isRemote) {
         continue;
       }
       counters[res.type] = (counters[res.type] || 0) + 1;
 
       switch (res.type) {
         case "inbound-rtp":
@@ -1834,27 +1837,27 @@ PeerConnectionWrapper.prototype = {
             var rem = stats.get(res.remoteId);
             ok(rem.isRemote, "Remote is rtcp");
             ok(rem.remoteId == res.id, "Remote backlink match");
             if(res.type == "outbound-rtp") {
               ok(rem.type == "inbound-rtp", "Rtcp is inbound");
               ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived");
               ok(rem.packetsLost !== undefined, "Rtcp packetsLost");
               ok(rem.bytesReceived >= rem.packetsReceived, "Rtcp bytesReceived");
-	       if (false) { // Bug 1325430 if (!this.disableRtpCountChecking) {
-	       // no guarantee which one is newer!
-	       // Note: this must change when we add a timestamp field to remote RTCP reports
-	       // and make rem.timestamp be the reception time
-		if (res.timestamp >= rem.timestamp) {
-                 ok(rem.packetsReceived <= res.packetsSent, "No more than sent packets");
-		 } else {
+              if (!this.disableRtpCountChecking) {
+                // no guarantee which one is newer!
+                // Note: this must change when we add a timestamp field to remote RTCP reports
+                // and make rem.timestamp be the reception time
+                if (res.timestamp >= rem.timestamp) {
+                  ok(rem.packetsReceived <= res.packetsSent, "No more than sent packets");
+                } else {
                   info("REVERSED timestamps: rec:" +
-		     rem.packetsReceived + " time:" + rem.timestamp + " sent:" + res.packetsSent + " time:" + res.timestamp);
-		 }
-		// Else we may have received more than outdated Rtcp packetsSent
+                    rem.packetsReceived + " time:" + rem.timestamp + " sent:" + res.packetsSent + " time:" + res.timestamp);
+                }
+                // Else we may have received more than outdated Rtcp packetsSent
                 ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
               }
               ok(rem.jitter !== undefined, "Rtcp jitter");
               if (rem.roundTripTime) {
                 ok(rem.roundTripTime > 0,
                    "Rtcp rtt " + rem.roundTripTime + " >= 0");
                 ok(rem.roundTripTime < 60000,
                    "Rtcp rtt " + rem.roundTripTime + " < 1 min");
--- a/dom/script/ScriptLoader.cpp
+++ b/dom/script/ScriptLoader.cpp
@@ -1245,17 +1245,19 @@ CSPAllowsInlineScript(nsIScriptElement* 
   // query the nonce
   nsCOMPtr<Element> scriptContent = do_QueryInterface(aElement);
   nsAutoString nonce;
   scriptContent->GetAttr(kNameSpaceID_None, nsGkAtoms::nonce, nonce);
   bool parserCreated = aElement->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER;
 
   bool allowInlineScript = false;
   rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
-                            nonce, parserCreated, scriptContent, EmptyString(),
+                            nonce, parserCreated, scriptContent,
+                            nullptr /* nsICSPEventListener */,
+                            EmptyString(),
                             aElement->GetScriptLineNumber(),
                             aElement->GetScriptColumnNumber(),
                             &allowInlineScript);
   return allowInlineScript;
 }
 
 ScriptLoadRequest*
 ScriptLoader::CreateLoadRequest(ScriptKind aKind,
@@ -2937,16 +2939,17 @@ ScriptLoader::VerifySRI(ScriptLoadReques
       loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
       nsAutoCString violationURISpec;
       mDocument->GetDocumentURI()->GetAsciiSpec(violationURISpec);
       uint32_t lineNo = aRequest->Element() ? aRequest->Element()->GetScriptLineNumber() : 0;
       uint32_t columnNo = aRequest->Element() ? aRequest->Element()->GetScriptColumnNumber() : 0;
       csp->LogViolationDetails(
         nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT,
         nullptr, // triggering element
+        nullptr, // nsICSPEventListener
         NS_ConvertUTF8toUTF16(violationURISpec),
         EmptyString(), lineNo, columnNo, EmptyString(), EmptyString());
       rv = NS_ERROR_SRI_CORRUPT;
     }
   }
 
   return rv;
 }
--- a/dom/security/CSPEvalChecker.cpp
+++ b/dom/security/CSPEvalChecker.cpp
@@ -15,16 +15,17 @@
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 namespace {
 
 nsresult
 CheckInternal(nsIContentSecurityPolicy* aCSP,
+              nsICSPEventListener* aCSPEventListener,
               const nsAString& aExpression,
               const nsAString& aFileNameString,
               uint32_t aLineNum,
               uint32_t aColumnNum,
               bool* aAllowed)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aAllowed);
@@ -42,16 +43,17 @@ CheckInternal(nsIContentSecurityPolicy* 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     *aAllowed = false;
     return rv;
   }
 
   if (reportViolation) {
     aCSP->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
                               nullptr, // triggering element
+                              aCSPEventListener,
                               aFileNameString, aExpression, aLineNum,
                               aColumnNum, EmptyString(), EmptyString());
   }
 
   return NS_OK;
 }
 
 class WorkerCSPCheckRunnable final : public WorkerMainThreadRunnable
@@ -69,18 +71,19 @@ public:
     , mLineNum(aLineNum)
     , mColumnNum(aColumnNum)
     , mEvalAllowed(false)
   {}
 
   bool
   MainThreadRun() override
   {
-    mResult = CheckInternal(mWorkerPrivate->GetCSP(), mExpression,
-                            mFileNameString, mLineNum, mColumnNum,
+    mResult = CheckInternal(mWorkerPrivate->GetCSP(),
+                            mWorkerPrivate->CSPEventListener(),
+                            mExpression, mFileNameString, mLineNum, mColumnNum,
                             &mEvalAllowed);
     return true;
   }
 
   nsresult
   GetResult(bool* aAllowed)
   {
     MOZ_ASSERT(aAllowed);
@@ -130,17 +133,18 @@ CSPEvalChecker::CheckForWindow(JSContext
   uint32_t lineNum = 0;
   uint32_t columnNum = 0;
   nsAutoString fileNameString;
   if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
                                      &columnNum)) {
     fileNameString.AssignLiteral("unknown");
   }
 
-  rv = CheckInternal(csp, aExpression, fileNameString, lineNum, columnNum,
+  rv = CheckInternal(csp, nullptr /* no CSPEventListener for window */,
+                     aExpression, fileNameString, lineNum, columnNum,
                      aAllowEval);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     *aAllowEval = false;
     return rv;
   }
 
   return NS_OK;
 }
--- a/dom/security/nsCSPContext.cpp
+++ b/dom/security/nsCSPContext.cpp
@@ -113,16 +113,17 @@ BlockedContentSourceToString(nsCSPContex
       break;
   }
 }
 
 /* =====  nsIContentSecurityPolicy impl ====== */
 
 NS_IMETHODIMP
 nsCSPContext::ShouldLoad(nsContentPolicyType aContentType,
+                         nsICSPEventListener* aCSPEventListener,
                          nsIURI*             aContentLocation,
                          nsIURI*             aRequestOrigin,
                          nsISupports*        aRequestContext,
                          const nsACString&   aMimeTypeGuess,
                          nsIURI*             aOriginalURIIfRedirect,
                          bool                aSendViolationReports,
                          int16_t*            outDecision)
 {
@@ -173,16 +174,17 @@ nsCSPContext::ShouldLoad(nsContentPolicy
     nsCOMPtr<nsIScriptElement> script = do_QueryInterface(aRequestContext);
     if (script && script->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER) {
       parserCreated = true;
     }
   }
 
   bool permitted = permitsInternal(dir,
                                    nullptr, // aTriggeringElement
+                                   aCSPEventListener,
                                    aContentLocation,
                                    aOriginalURIIfRedirect,
                                    nonce,
                                    isPreload,
                                    false,     // allow fallback to default-src
                                    aSendViolationReports,
                                    true,     // send blocked URI in violation reports
                                    parserCreated);
@@ -197,16 +199,17 @@ nsCSPContext::ShouldLoad(nsContentPolicy
                    aContentLocation->GetSpecOrDefault().get()));
   }
   return NS_OK;
 }
 
 bool
 nsCSPContext::permitsInternal(CSPDirective aDir,
                               Element* aTriggeringElement,
+                              nsICSPEventListener* aCSPEventListener,
                               nsIURI* aContentLocation,
                               nsIURI* aOriginalURIIfRedirect,
                               const nsAString& aNonce,
                               bool aIsPreload,
                               bool aSpecific,
                               bool aSendViolationReports,
                               bool aSendContentLocationInViolationReports,
                               bool aParserCreated)
@@ -229,16 +232,17 @@ nsCSPContext::permitsInternal(CSPDirecti
         permits = false;
       }
 
       // Do not send a report or notify observers if this is a preload - the
       // decision may be wrong due to the inability to get the nonce, and will
       // incorrectly fail the unit tests.
       if (!aIsPreload && aSendViolationReports) {
         AsyncReportViolation(aTriggeringElement,
+                             aCSPEventListener,
                              (aSendContentLocationInViolationReports ?
                               aContentLocation : nullptr),
                              BlockedContentSource::eUnknown, /* a BlockedContentSource */
                              aOriginalURIIfRedirect,  /* in case of redirect originalURI is not null */
                              violatedDirective,
                              p,             /* policy index        */
                              EmptyString(), /* no observer subject */
                              EmptyString(), /* no source file      */
@@ -400,16 +404,17 @@ nsCSPContext::GetAllowsEval(bool* outSho
   }
   return NS_OK;
 }
 
 // Helper function to report inline violations
 void
 nsCSPContext::reportInlineViolation(nsContentPolicyType aContentType,
                                     Element* aTriggeringElement,
+                                    nsICSPEventListener* aCSPEventListener,
                                     const nsAString& aNonce,
                                     const nsAString& aContent,
                                     const nsAString& aViolatedDirective,
                                     uint32_t aViolatedPolicyIndex, // TODO, use report only flag for that
                                     uint32_t aLineNumber,
                                     uint32_t aColumnNumber)
 {
   nsString observerSubject;
@@ -429,33 +434,35 @@ nsCSPContext::reportInlineViolation(nsCo
 
   // use selfURI as the sourceFile
   nsAutoCString sourceFile;
   if (mSelfURI) {
     mSelfURI->GetSpec(sourceFile);
   }
 
   AsyncReportViolation(aTriggeringElement,
+                       aCSPEventListener,
                        nullptr,                            // aBlockedURI
-                       BlockedContentSource::eInline,      // aBloeckedSource
+                       BlockedContentSource::eInline,      // aBlockedSource
                        mSelfURI,                           // aOriginalURI
                        aViolatedDirective,                 // aViolatedDirective
                        aViolatedPolicyIndex,               // aViolatedPolicyIndex
                        observerSubject,                    // aObserverSubject
                        NS_ConvertUTF8toUTF16(sourceFile),  // aSourceFile
                        aContent,                           // aScriptSample
                        aLineNumber,                        // aLineNum
                        aColumnNumber);                     // aColumnNum
 }
 
 NS_IMETHODIMP
 nsCSPContext::GetAllowsInline(nsContentPolicyType aContentType,
                               const nsAString& aNonce,
                               bool aParserCreated,
                               Element* aTriggeringElement,
+                              nsICSPEventListener* aCSPEventListener,
                               const nsAString& aContentOfPseudoScript,
                               uint32_t aLineNumber,
                               uint32_t aColumnNumber,
                               bool* outAllowsInline)
 {
   *outAllowsInline = true;
 
   MOZ_ASSERT(aContentType == nsContentUtils::InternalContentPolicyTypeToExternal(aContentType),
@@ -506,16 +513,17 @@ nsCSPContext::GetAllowsInline(nsContentP
       }
       nsAutoString violatedDirective;
       bool reportSample = false;
       mPolicies[i]->getDirectiveStringAndReportSampleForContentType(aContentType,
                                                                     violatedDirective,
                                                                     &reportSample);
       reportInlineViolation(aContentType,
                             aTriggeringElement,
+                            aCSPEventListener,
                             aNonce,
                             reportSample ? content : EmptyString(),
                             violatedDirective,
                             i,
                             aLineNumber,
                             aColumnNumber);
     }
   }
@@ -558,17 +566,18 @@ nsCSPContext::GetAllowsInline(nsContentP
     if (!mPolicies[p]->allows(nsIContentPolicy::TYPE_ ## contentPolicyType,    \
                               keyword, nonceOrHash, false))                    \
     {                                                                          \
       nsAutoString violatedDirective;                                          \
       bool reportSample = false;                                               \
       mPolicies[p]->getDirectiveStringAndReportSampleForContentType(           \
                         nsIContentPolicy::TYPE_ ## contentPolicyType,          \
                         violatedDirective, &reportSample);                     \
-      AsyncReportViolation(aTriggeringElement, nullptr, blockedContentSource,  \
+      AsyncReportViolation(aTriggeringElement, aCSPEventListener,              \
+                           nullptr, blockedContentSource,                      \
                            nullptr, violatedDirective, p,                      \
                            NS_LITERAL_STRING(observerTopic),                   \
                            aSourceFile,                                        \
                            reportSample                                        \
                              ? aScriptSample : EmptyString(),                  \
                            aLineNum, aColumnNum);                              \
     }                                                                          \
     PR_END_MACRO;                                                              \
@@ -596,16 +605,17 @@ nsCSPContext::GetAllowsInline(nsContentP
  *     (optional) If this is a hash violation, include contents of the inline
  *     resource in the question so we can recheck the hash in order to
  *     determine which policies were violated and send the appropriate
  *     reports.
  */
 NS_IMETHODIMP
 nsCSPContext::LogViolationDetails(uint16_t aViolationType,
                                   Element* aTriggeringElement,
+                                  nsICSPEventListener* aCSPEventListener,
                                   const nsAString& aSourceFile,
                                   const nsAString& aScriptSample,
                                   int32_t aLineNum,
                                   int32_t aColumnNum,
                                   const nsAString& aNonce,
                                   const nsAString& aContent)
 {
   for (uint32_t p = 0; p < mPolicies.Length(); p++) {
@@ -687,23 +697,16 @@ nsCSPContext::SetRequestContext(nsIDocum
     mQueueUpMessages = false;
   }
 
   NS_ASSERTION(mSelfURI, "mSelfURI not available, can not translate 'self' into actual URI");
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsCSPContext::SetEventListener(nsICSPEventListener* aEventListener)
-{
-  mEventListener = aEventListener;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 nsCSPContext::EnsureEventTarget(nsIEventTarget* aEventTarget)
 {
   NS_ENSURE_ARG(aEventTarget);
   // Don't bother if we did have a valid event target (if the csp object is
   // tied to a document in SetRequestContext)
   if (mEventTarget) {
     return NS_OK;
   }
@@ -1130,22 +1133,23 @@ nsCSPContext::SendReports(
     }
   }
   return NS_OK;
 }
 
 nsresult
 nsCSPContext::FireViolationEvent(
   Element* aTriggeringElement,
+  nsICSPEventListener* aCSPEventListener,
   const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit)
 {
-  if (mEventListener) {
+  if (aCSPEventListener) {
     nsAutoString json;
     if (aViolationEventInit.ToJSON(json)) {
-      mEventListener->OnCSPViolationEvent(json);
+      aCSPEventListener->OnCSPViolationEvent(json);
     }
   }
 
   // 1. If target is not null, and global is a Window, and target’s
   // shadow-including root is not global’s associated Document, set target to
   // null.
   RefPtr<EventTarget> eventTarget = aTriggeringElement;
 
@@ -1180,30 +1184,32 @@ nsCSPContext::FireViolationEvent(
 
 /**
  * Dispatched from the main thread to send reports for one CSP violation.
  */
 class CSPReportSenderRunnable final : public Runnable
 {
   public:
     CSPReportSenderRunnable(Element* aTriggeringElement,
+                            nsICSPEventListener* aCSPEventListener,
                             nsIURI* aBlockedURI,
                             nsCSPContext::BlockedContentSource aBlockedContentSource,
                             nsIURI* aOriginalURI,
                             uint32_t aViolatedPolicyIndex,
                             bool aReportOnlyFlag,
                             const nsAString& aViolatedDirective,
                             const nsAString& aObserverSubject,
                             const nsAString& aSourceFile,
                             const nsAString& aScriptSample,
                             uint32_t aLineNum,
                             uint32_t aColumnNum,
                             nsCSPContext* aCSPContext)
       : mozilla::Runnable("CSPReportSenderRunnable")
       , mTriggeringElement(aTriggeringElement)
+      , mCSPEventListener(aCSPEventListener)
       , mBlockedURI(aBlockedURI)
       , mBlockedContentSource(aBlockedContentSource)
       , mOriginalURI(aOriginalURI)
       , mViolatedPolicyIndex(aViolatedPolicyIndex)
       , mReportOnlyFlag(aReportOnlyFlag)
       , mViolatedDirective(aViolatedDirective)
       , mSourceFile(aSourceFile)
       , mScriptSample(aScriptSample)
@@ -1287,23 +1293,24 @@ class CSPReportSenderRunnable final : pu
                                      blockedContentSource16.get() };
         mCSPContext->logToConsole(mReportOnlyFlag ? "CSPROViolationWithURI" :
                                                     "CSPViolationWithURI",
                                   params, ArrayLength(params), mSourceFile, mScriptSample,
                                   mLineNum, mColumnNum, nsIScriptError::errorFlag);
       }
 
       // 4) fire violation event
-      mCSPContext->FireViolationEvent(mTriggeringElement, init);
+      mCSPContext->FireViolationEvent(mTriggeringElement, mCSPEventListener, init);
 
       return NS_OK;
     }
 
   private:
     RefPtr<Element>         mTriggeringElement;
+    nsCOMPtr<nsICSPEventListener> mCSPEventListener;
     nsCOMPtr<nsIURI>        mBlockedURI;
     nsCSPContext::BlockedContentSource mBlockedContentSource;
     nsCOMPtr<nsIURI>        mOriginalURI;
     uint32_t                mViolatedPolicyIndex;
     bool                    mReportOnlyFlag;
     nsString                mViolatedDirective;
     nsCOMPtr<nsISupports>   mObserverSubject;
     nsString                mSourceFile;
@@ -1339,31 +1346,33 @@ class CSPReportSenderRunnable final : pu
  *        a sample of the violating inline script
  * @param aLineNum
  *        source line number of the violation (if available)
  * @param aColumnNum
  *        source column number of the violation (if available)
  */
 nsresult
 nsCSPContext::AsyncReportViolation(Element* aTriggeringElement,
+                                   nsICSPEventListener* aCSPEventListener,
                                    nsIURI* aBlockedURI,
                                    BlockedContentSource aBlockedContentSource,
                                    nsIURI* aOriginalURI,
                                    const nsAString& aViolatedDirective,
                                    uint32_t aViolatedPolicyIndex,
                                    const nsAString& aObserverSubject,
                                    const nsAString& aSourceFile,
                                    const nsAString& aScriptSample,
                                    uint32_t aLineNum,
                                    uint32_t aColumnNum)
 {
   NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
 
   nsCOMPtr<nsIRunnable> task =
     new CSPReportSenderRunnable(aTriggeringElement,
+                                aCSPEventListener,
                                 aBlockedURI,
                                 aBlockedContentSource,
                                 aOriginalURI,
                                 aViolatedPolicyIndex,
                                 mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(),
                                 aViolatedDirective,
                                 aObserverSubject,
                                 aSourceFile,
@@ -1484,17 +1493,18 @@ nsCSPContext::PermitsAncestry(nsIDocShel
                      ancestorsArray[a]->GetSpecOrDefault().get()));
     }
     // omit the ancestor URI in violation reports if cross-origin as per spec
     // (it is a violation of the same-origin policy).
     bool okToSendAncestor = NS_SecurityCompareURIs(ancestorsArray[a], mSelfURI, true);
 
 
     bool permits = permitsInternal(nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE,
-                                   nullptr,
+                                   nullptr, // triggering element
+                                   nullptr, // nsICSPEventListener
                                    ancestorsArray[a],
                                    nullptr, // no redirect here.
                                    EmptyString(), // no nonce
                                    false,   // not a preload.
                                    true,    // specific, do not use default-src
                                    true,    // send violation reports
                                    okToSendAncestor,
                                    false);  // not parser created
@@ -1502,28 +1512,30 @@ nsCSPContext::PermitsAncestry(nsIDocShel
       *outPermitsAncestry = false;
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::Permits(Element* aTriggeringElement,
+                      nsICSPEventListener* aCSPEventListener,
                       nsIURI* aURI,
                       CSPDirective aDir,
                       bool aSpecific,
                       bool* outPermits)
 {
   // Can't perform check without aURI
   if (aURI == nullptr) {
     return NS_ERROR_FAILURE;
   }
 
   *outPermits = permitsInternal(aDir,
                                 aTriggeringElement,
+                                aCSPEventListener,
                                 aURI,
                                 nullptr,  // no original (pre-redirect) URI
                                 EmptyString(),  // no nonce
                                 false,    // not a preload.
                                 aSpecific,
                                 true,     // send violation reports
                                 true,     // send blocked URI in violation reports
                                 false);   // not parser created
--- a/dom/security/nsCSPContext.h
+++ b/dom/security/nsCSPContext.h
@@ -100,27 +100,29 @@ class nsCSPContext : public nsIContentSe
       mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit);
 
     nsresult SendReports(
       const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit,
       uint32_t aViolatedPolicyIndex);
 
     nsresult FireViolationEvent(
       mozilla::dom::Element* aTriggeringElement,
+      nsICSPEventListener* aCSPEventListener,
       const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit);
 
     enum BlockedContentSource
     {
       eUnknown,
       eInline,
       eEval,
       eSelf,
     };
 
     nsresult AsyncReportViolation(mozilla::dom::Element* aTriggeringElement,
+                                  nsICSPEventListener* aCSPEventListener,
                                   nsIURI* aBlockedURI,
                                   BlockedContentSource aBlockedContentSource,
                                   nsIURI* aOriginalURI,
                                   const nsAString& aViolatedDirective,
                                   uint32_t aViolatedPolicyIndex,
                                   const nsAString& aObserverSubject,
                                   const nsAString& aSourceFile,
                                   const nsAString& aScriptSample,
@@ -143,28 +145,30 @@ class nsCSPContext : public nsIContentSe
       return std::max(
         mozilla::StaticPrefs::security_csp_reporting_script_sample_max_length(),
         0);
     }
 
   private:
     bool permitsInternal(CSPDirective aDir,
                          mozilla::dom::Element* aTriggeringElement,
+                         nsICSPEventListener* aCSPEventListener,
                          nsIURI* aContentLocation,
                          nsIURI* aOriginalURIIfRedirect,
                          const nsAString& aNonce,
                          bool aIsPreload,
                          bool aSpecific,
                          bool aSendViolationReports,
                          bool aSendContentLocationInViolationReports,
                          bool aParserCreated);
 
     // helper to report inline script/style violations
     void reportInlineViolation(nsContentPolicyType aContentType,
                                mozilla::dom::Element* aTriggeringElement,
+                               nsICSPEventListener* aCSPEventListener,
                                const nsAString& aNonce,
                                const nsAString& aContent,
                                const nsAString& aViolatedDirective,
                                uint32_t aViolatedPolicyIndex,
                                uint32_t aLineNumber,
                                uint32_t aColumnNumber);
 
     nsString                                   mReferrer;
@@ -172,17 +176,16 @@ class nsCSPContext : public nsIContentSe
     nsTArray<nsCSPPolicy*>                     mPolicies;
     nsCOMPtr<nsIURI>                           mSelfURI;
     nsCOMPtr<nsILoadGroup>                     mCallingChannelLoadGroup;
     nsWeakPtr                                  mLoadingContext;
     // The CSP hangs off the principal, so let's store a raw pointer of the principal
     // to avoid memory leaks. Within the destructor of the principal we explicitly
     // set mLoadingPrincipal to null.
     nsIPrincipal*                              mLoadingPrincipal;
-    nsCOMPtr<nsICSPEventListener>              mEventListener;
 
     // helper members used to queue up web console messages till
     // the windowID becomes available. see flushConsoleMessages()
     nsTArray<ConsoleMsgQueueElem>              mConsoleMsgQueue;
     bool                                       mQueueUpMessages;
     nsCOMPtr<nsIEventTarget>                   mEventTarget;
 };
 
--- a/dom/security/nsCSPService.cpp
+++ b/dom/security/nsCSPService.cpp
@@ -131,16 +131,20 @@ CSPService::ShouldLoad(nsIURI *aContentL
   nsCOMPtr<nsISupports> requestContext = aLoadInfo->GetLoadingContext();
   nsCOMPtr<nsIPrincipal> requestPrincipal = aLoadInfo->TriggeringPrincipal();
   nsCOMPtr<nsIURI> requestOrigin;
   nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->LoadingPrincipal();
   if (loadingPrincipal) {
     loadingPrincipal->GetURI(getter_AddRefs(requestOrigin));
   }
 
+  nsCOMPtr<nsICSPEventListener> cspEventListener;
+  nsresult rv = aLoadInfo->GetCspEventListener(getter_AddRefs(cspEventListener));
+  NS_ENSURE_SUCCESS(rv, rv);
+
   if (MOZ_LOG_TEST(gCspPRLog, LogLevel::Debug)) {
     MOZ_LOG(gCspPRLog, LogLevel::Debug,
            ("CSPService::ShouldLoad called for %s",
            aContentLocation->GetSpecOrDefault().get()));
   }
 
   // default decision, CSP can revise it if there's a policy to enforce
   *aDecision = nsIContentPolicy::ACCEPT;
@@ -166,29 +170,29 @@ CSPService::ShouldLoad(nsIURI *aContentL
     principal = requestPrincipal;
   } else  {
     principal = node->NodePrincipal();
   }
   if (!principal) {
     // if we can't query a principal, then there is nothing to do.
     return NS_OK;
   }
-  nsresult rv = NS_OK;
 
   // 1) Apply speculate CSP for preloads
   bool isPreload = nsContentUtils::IsPreloadType(contentType);
 
   if (isPreload) {
     nsCOMPtr<nsIContentSecurityPolicy> preloadCsp;
     rv = principal->GetPreloadCsp(getter_AddRefs(preloadCsp));
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (preloadCsp) {
       // obtain the enforcement decision
       rv = preloadCsp->ShouldLoad(contentType,
+                                  cspEventListener,
                                   aContentLocation,
                                   requestOrigin,
                                   requestContext,
                                   aMimeTypeGuess,
                                   nullptr, // no redirect, aOriginal URL is null.
                                   aLoadInfo->GetSendCSPViolationEvents(),
                                   aDecision);
       NS_ENSURE_SUCCESS(rv, rv);
@@ -204,16 +208,17 @@ CSPService::ShouldLoad(nsIURI *aContentL
   // 2) Apply actual CSP to all loads
   nsCOMPtr<nsIContentSecurityPolicy> csp;
   rv = principal->GetCsp(getter_AddRefs(csp));
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (csp) {
     // obtain the enforcement decision
     rv = csp->ShouldLoad(contentType,
+                         cspEventListener,
                          aContentLocation,
                          requestOrigin,
                          requestContext,
                          aMimeTypeGuess,
                          nullptr, // no redirect, aOriginal URL is null.
                          aLoadInfo->GetSendCSPViolationEvents(),
                          aDecision);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -267,16 +272,20 @@ CSPService::AsyncOnChannelRedirect(nsICh
   net::nsAsyncRedirectAutoCallback autoCallback(callback);
 
   nsCOMPtr<nsIURI> newUri;
   nsresult rv = newChannel->GetURI(getter_AddRefs(newUri));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsILoadInfo> loadInfo = oldChannel->GetLoadInfo();
 
+  nsCOMPtr<nsICSPEventListener> cspEventListener;
+  rv = loadInfo->GetCspEventListener(getter_AddRefs(cspEventListener));
+  NS_ENSURE_SUCCESS(rv, rv);
+
   // if no loadInfo on the channel, nothing for us to do
   if (!loadInfo) {
     return NS_OK;
   }
 
   // No need to continue processing if CSP is disabled or if the protocol
   // is *not* subject to CSP.
   // Please note, the correct way to opt-out of CSP using a custom
@@ -317,16 +326,17 @@ CSPService::AsyncOnChannelRedirect(nsICh
   // 1) Apply speculative CSP for preloads
   if (isPreload) {
     nsCOMPtr<nsIContentSecurityPolicy> preloadCsp;
     loadInfo->LoadingPrincipal()->GetPreloadCsp(getter_AddRefs(preloadCsp));
 
     if (preloadCsp) {
       // Pass  originalURI to indicate the redirect
       preloadCsp->ShouldLoad(policyType,     // load type per nsIContentPolicy (uint32_t)
+                             cspEventListener,
                              newUri,         // nsIURI
                              nullptr,        // nsIURI
                              requestContext, // nsISupports
                              EmptyCString(), // ACString - MIME guess
                              originalUri,    // Original nsIURI
                              true,           // aSendViolationReports
                              &aDecision);
 
@@ -342,16 +352,17 @@ CSPService::AsyncOnChannelRedirect(nsICh
 
   // 2) Apply actual CSP to all loads
   nsCOMPtr<nsIContentSecurityPolicy> csp;
   loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
 
   if (csp) {
     // Pass  originalURI to indicate the redirect
     csp->ShouldLoad(policyType,     // load type per nsIContentPolicy (uint32_t)
+                    cspEventListener,
                     newUri,         // nsIURI
                     nullptr,        // nsIURI
                     requestContext, // nsISupports
                     EmptyCString(), // ACString - MIME guess
                     originalUri,    // Original nsIURI
                     true,           // aSendViolationReports
                     &aDecision);
   }
--- a/dom/security/test/unit/test_csp_reports.js
+++ b/dom/security/test/unit/test_csp_reports.js
@@ -105,16 +105,17 @@ function run_test() {
   // test that inline script violations cause a report.
   makeTest(0, {"blocked-uri": "inline"}, false,
       function(csp) {
         let inlineOK = true;
         inlineOK = csp.getAllowsInline(Ci.nsIContentPolicy.TYPE_SCRIPT,
                                        "", // aNonce
                                        false, // aParserCreated
                                        null, // aTriggeringElement
+                                       null, // nsICSPEventListener
                                        "", // aContentOfPseudoScript
                                        0, // aLineNumber
                                        0); // aColumnNumber
 
         // this is not a report only policy, so it better block inline scripts
         Assert.ok(!inlineOK);
       });
 
@@ -132,42 +133,45 @@ function run_test() {
         Assert.ok(!evalOK);
         // ... and cause reports to go out
         Assert.ok(oReportViolation.value);
 
         if (oReportViolation.value) {
           // force the logging, since the getter doesn't.
           csp.logViolationDetails(Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL,
                                   null, // aTriggeringElement
+                                  null, // nsICSPEventListener
                                   selfuri.asciiSpec,
                                   // sending UTF-16 script sample to make sure
                                   // csp report in JSON is not cut-off, please
                                   // note that JSON is UTF8 encoded.
                                   "\u00a3\u00a5\u00b5\u5317\ud841\udf79",
                                   1, // line number
                                   2); // column number
         }
       });
 
   makeTest(2, {"blocked-uri": "http://blocked.test/foo.js"}, false,
       function(csp) {
         // shouldLoad creates and sends out the report here.
         csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT,
+                      null, // nsICSPEventListener
                       NetUtil.newURI("http://blocked.test/foo.js"),
                       null, null, null, null, true);
       });
 
   // test that inline script violations cause a report in report-only policy
   makeTest(3, {"blocked-uri": "inline"}, true,
       function(csp) {
         let inlineOK = true;
         inlineOK = csp.getAllowsInline(Ci.nsIContentPolicy.TYPE_SCRIPT,
                                        "", // aNonce
                                        false, // aParserCreated
                                        null, // aTriggeringElement
+                                       null, // nsICSPEventListener
                                        "", // aContentOfPseudoScript
                                        0, // aLineNumber
                                        0); // aColumnNumber
 
         // this is a report only policy, so it better allow inline scripts
         Assert.ok(inlineOK);
       });
 
@@ -181,56 +185,61 @@ function run_test() {
         Assert.ok(evalOK);
         // ... but still cause reports to go out
         Assert.ok(oReportViolation.value);
 
         if (oReportViolation.value) {
           // force the logging, since the getter doesn't.
           csp.logViolationDetails(Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_SCRIPT,
                                   null, // aTriggeringElement
+                                  null, // nsICSPEventListener
                                   selfuri.asciiSpec,
                                   "script sample",
                                   4, // line number
                                   5); // column number
         }
       });
 
   // test that only the uri's scheme is reported for globally unique identifiers
   makeTest(5, {"blocked-uri": "data"}, false,
     function(csp) {
       var base64data =
         "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
         "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
       // shouldLoad creates and sends out the report here.
       csp.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE,
+                     null, // nsICSPEventListener
                      NetUtil.newURI("data:image/png;base64," + base64data),
                      null, null, null, null, true);
       });
 
   // test that only the uri's scheme is reported for globally unique identifiers
   makeTest(6, {"blocked-uri": "intent"}, false,
     function(csp) {
       // shouldLoad creates and sends out the report here.
       csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+                     null, // nsICSPEventListener
                      NetUtil.newURI("intent://mymaps.com/maps?um=1&ie=UTF-8&fb=1&sll"),
                      null, null, null, null, true);
       });
 
   // test fragment removal
   var selfSpec = REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self/foo.js";
   makeTest(7, {"blocked-uri": selfSpec}, false,
     function(csp) {
       var uri = NetUtil
       // shouldLoad creates and sends out the report here.
       csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT,
+                     null, // nsICSPEventListener
                      NetUtil.newURI(selfSpec + "#bar"),
                      null, null, null, null, true);
       });
 
   // test scheme of ftp:
   makeTest(8, {"blocked-uri": "ftp://blocked.test/profile.png"}, false,
     function(csp) {
       // shouldLoad creates and sends out the report here.
       csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT,
+                     null, // nsICSPEventListener
                     NetUtil.newURI("ftp://blocked.test/profile.png"),
                     null, null, null, null, true);
     });
 }
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -2668,16 +2668,17 @@ LogViolationDetailsRunnable::MainThreadR
 {
   AssertIsOnMainThread();
 
   nsIContentSecurityPolicy* csp = mWorkerPrivate->GetCSP();
   if (csp) {
     if (mWorkerPrivate->GetReportCSPViolations()) {
       csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
                                nullptr, // triggering element
+                               mWorkerPrivate->CSPEventListener(),
                                mFileName, mScriptSample, mLineNum, mColumnNum,
                                EmptyString(), EmptyString());
     }
   }
 
   return true;
 }
 
--- a/dom/workers/ScriptLoader.cpp
+++ b/dom/workers/ScriptLoader.cpp
@@ -216,25 +216,28 @@ ChannelFromScriptURL(nsIPrincipal* princ
                        parentDoc,
                        secFlags,
                        contentPolicyType,
                        nullptr, // aPerformanceStorage
                        loadGroup,
                        nullptr, // aCallbacks
                        aLoadFlags,
                        ios);
+    NS_ENSURE_SUCCESS(rv, rv);
   } else {
     // We must have a loadGroup with a load context for the principal to
     // traverse the channel correctly.
     MOZ_ASSERT(loadGroup);
     MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal));
 
     RefPtr<PerformanceStorage> performanceStorage;
+    nsCOMPtr<nsICSPEventListener> cspEventListener;
     if (aWorkerPrivate && !aIsMainScript) {
       performanceStorage = aWorkerPrivate->GetPerformanceStorage();
+      cspEventListener = aWorkerPrivate->CSPEventListener();
     }
 
     if (aClientInfo.isSome()) {
       rv = NS_NewChannel(getter_AddRefs(channel),
                          uri,
                          principal,
                          aClientInfo.ref(),
                          aController,
@@ -252,20 +255,30 @@ ChannelFromScriptURL(nsIPrincipal* princ
                          secFlags,
                          contentPolicyType,
                          performanceStorage,
                          loadGroup,
                          nullptr, // aCallbacks
                          aLoadFlags,
                          ios);
     }
+
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (cspEventListener) {
+      nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo();
+      if (NS_WARN_IF(!loadInfo)) {
+        return NS_ERROR_UNEXPECTED;
+      }
+
+      rv = loadInfo->SetCspEventListener(cspEventListener);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
   }
 
-  NS_ENSURE_SUCCESS(rv, rv);
-
   if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) {
     mozilla::net::ReferrerPolicy referrerPolicy = parentDoc ?
       parentDoc->GetReferrerPolicy() : mozilla::net::RP_Unset;
     rv = nsContentUtils::SetFetchReferrerURIWithPolicy(principal, parentDoc,
                                                        httpChannel, referrerPolicy);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
@@ -1224,16 +1237,17 @@ private:
             ("Scriptloader::Load, SRI required but not supported in workers"));
       nsCOMPtr<nsIContentSecurityPolicy> wcsp;
       chanLoadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(wcsp));
       MOZ_ASSERT(wcsp, "We should have a CSP for the worker here");
       if (wcsp) {
         wcsp->LogViolationDetails(
             nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT,
             nullptr, // triggering element
+            mWorkerPrivate->CSPEventListener(),
             aLoadInfo.mURL, EmptyString(), 0, 0, EmptyString(), EmptyString());
       }
       return NS_ERROR_SRI_CORRUPT;
     }
 
     // Update the principal of the worker and its base URI if we just loaded the
     // worker's primary script.
     if (IsMainWorkerScript()) {
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -1468,17 +1468,16 @@ WorkerPrivate::SetCSP(nsIContentSecurity
 {
   AssertIsOnMainThread();
   if (!aCSP) {
     return;
   }
   aCSP->EnsureEventTarget(mMainThreadEventTarget);
 
   mLoadInfo.mCSP = aCSP;
-  EnsureCSPEventListener();
 }
 
 nsresult
 WorkerPrivate::SetCSPFromHeaderValues(const nsACString& aCSPHeaderValue,
                                       const nsACString& aCSPReportOnlyHeaderValue)
 {
   AssertIsOnMainThread();
   MOZ_DIAGNOSTIC_ASSERT(!mLoadInfo.mCSP);
@@ -1509,17 +1508,16 @@ WorkerPrivate::SetCSPFromHeaderValues(co
   bool evalAllowed = false;
   bool reportEvalViolations = false;
   rv = csp->GetAllowsEval(&reportEvalViolations, &evalAllowed);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mLoadInfo.mCSP = csp;
   mLoadInfo.mEvalAllowed = evalAllowed;
   mLoadInfo.mReportCSPViolations = reportEvalViolations;
-  EnsureCSPEventListener();
 
   return NS_OK;
 }
 
 void
 WorkerPrivate::SetReferrerPolicyFromHeaderValue(const nsACString& aReferrerPolicyHeaderValue)
 {
   NS_ConvertUTF8toUTF16 headerValue(aReferrerPolicyHeaderValue);
@@ -3434,24 +3432,26 @@ bool
 WorkerPrivate::EnsureCSPEventListener()
 {
   if (!mCSPEventListener) {
     mCSPEventListener = WorkerCSPEventListener::Create(this);
     if (NS_WARN_IF(!mCSPEventListener)) {
       return false;
     }
   }
-
-  if (mLoadInfo.mCSP) {
-    mLoadInfo.mCSP->SetEventListener(mCSPEventListener);
-  }
-
   return true;
 }
 
+nsICSPEventListener*
+WorkerPrivate::CSPEventListener() const
+{
+  MOZ_ASSERT(mCSPEventListener);
+  return mCSPEventListener;
+}
+
 void
 WorkerPrivate::EnsurePerformanceStorage()
 {
   AssertIsOnWorkerThread();
 
   if (!mPerformanceStorage) {
     mPerformanceStorage = PerformanceStorageWorker::Create(this);
   }
--- a/dom/workers/WorkerPrivate.h
+++ b/dom/workers/WorkerPrivate.h
@@ -407,16 +407,19 @@ public:
 
   WorkerDebuggerGlobalScope*
   DebuggerGlobalScope() const
   {
     AssertIsOnWorkerThread();
     return mDebuggerScope;
   }
 
+  nsICSPEventListener*
+  CSPEventListener() const;
+
   void
   SetThread(WorkerThread* aThread);
 
   bool
   IsOnWorkerThread() const;
 
   void
   AssertIsOnWorkerThread() const
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -2472,16 +2472,26 @@ XMLHttpRequestMainThread::CreateChannel(
                        nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST,
                        mPerformanceStorage, // aPerformanceStorage
                        loadGroup,
                        nullptr,   // aCallbacks
                        loadFlags);
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
+  if (mCSPEventListener) {
+    nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
+    if (NS_WARN_IF(!loadInfo)) {
+      return NS_ERROR_UNEXPECTED;
+    }
+
+    rv = loadInfo->SetCspEventListener(mCSPEventListener);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel));
   if (httpChannel) {
     rv = httpChannel->SetRequestMethod(mRequestMethod);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Set the initiator type
     nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel));
     if (timedChannel) {
--- a/dom/xhr/XMLHttpRequestMainThread.h
+++ b/dom/xhr/XMLHttpRequestMainThread.h
@@ -22,16 +22,17 @@
 #include "nsJSUtils.h"
 #include "nsTArray.h"
 #include "nsITimer.h"
 #include "nsIPrincipal.h"
 #include "nsIScriptObjectPrincipal.h"
 #include "nsISizeOfEventTarget.h"
 #include "nsIXPConnect.h"
 #include "nsIInputStream.h"
+#include "nsIContentSecurityPolicy.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/dom/MutableBlobStorage.h"
 #include "mozilla/dom/BodyExtractor.h"
@@ -198,24 +199,26 @@ public:
   };
 
   XMLHttpRequestMainThread();
 
   void Construct(nsIPrincipal* aPrincipal,
                  nsIGlobalObject* aGlobalObject,
                  nsIURI* aBaseURI = nullptr,
                  nsILoadGroup* aLoadGroup = nullptr,
-                 PerformanceStorage* aPerformanceStorage = nullptr)
+                 PerformanceStorage* aPerformanceStorage = nullptr,
+                 nsICSPEventListener* aCSPEventListener = nullptr)
   {
     MOZ_ASSERT(aPrincipal);
     mPrincipal = aPrincipal;
     BindToOwner(aGlobalObject);
     mBaseURI = aBaseURI;
     mLoadGroup = aLoadGroup;
     mPerformanceStorage = aPerformanceStorage;
+    mCSPEventListener = aCSPEventListener;
   }
 
   void InitParameters(bool aAnon, bool aSystem);
 
   void SetParameters(bool aAnon, bool aSystem)
   {
     mIsAnon = aAnon || aSystem;
     mIsSystem = aSystem;
@@ -541,16 +544,17 @@ protected:
   nsCOMPtr<nsIChannel> mChannel;
   nsCString mRequestMethod;
   nsCOMPtr<nsIURI> mRequestURL;
   nsCOMPtr<nsIDocument> mResponseXML;
 
   nsCOMPtr<nsIStreamListener> mXMLParserStreamListener;
 
   RefPtr<PerformanceStorage> mPerformanceStorage;
+  nsCOMPtr<nsICSPEventListener> mCSPEventListener;
 
   // used to implement getAllResponseHeaders()
   class nsHeaderVisitor : public nsIHttpHeaderVisitor
   {
     struct HeaderEntry final
     {
       nsCString mName;
       nsCString mValue;
--- a/dom/xhr/XMLHttpRequestWorker.cpp
+++ b/dom/xhr/XMLHttpRequestWorker.cpp
@@ -862,17 +862,18 @@ Proxy::Init()
     return false;
   }
 
   mXHR = new XMLHttpRequestMainThread();
   mXHR->Construct(mWorkerPrivate->GetPrincipal(),
                   ownerWindow ? ownerWindow->AsGlobal() : nullptr,
                   mWorkerPrivate->GetBaseURI(),
                   mWorkerPrivate->GetLoadGroup(),
-                  mWorkerPrivate->GetPerformanceStorage());
+                  mWorkerPrivate->GetPerformanceStorage(),
+                  mWorkerPrivate->CSPEventListener());
 
   mXHR->SetParameters(mMozAnon, mMozSystem);
   mXHR->SetClientInfoAndController(mClientInfo, mController);
 
   ErrorResult rv;
   mXHRUpload = mXHR->GetUpload(rv);
   if (NS_WARN_IF(rv.Failed())) {
     mXHR = nullptr;
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -330,19 +330,23 @@ RenderThread::RunEvent(wr::WindowId aWin
   aEvent->Run(*this, aWindowId);
   aEvent = nullptr;
 }
 
 static void
 NotifyDidRender(layers::CompositorBridgeParent* aBridge,
                 wr::WrPipelineInfo aInfo,
                 TimeStamp aStart,
-                TimeStamp aEnd)
+                TimeStamp aEnd,
+                bool aRender)
 {
-  if (aBridge->GetWrBridge()) {
+  if (aRender && aBridge->GetWrBridge()) {
+    // We call this here to mimic the behavior in LayerManagerComposite, as to
+    // not change what Talos measures. That is, we do not record an empty frame
+    // as a frame.
     aBridge->GetWrBridge()->RecordFrame();
   }
 
   for (uintptr_t i = 0; i < aInfo.epochs.length; i++) {
     aBridge->NotifyPipelineRendered(
         aInfo.epochs.data[i].pipeline_id,
         aInfo.epochs.data[i].epoch,
         aStart,
@@ -371,16 +375,18 @@ RenderThread::UpdateAndRender(wr::Window
 
   auto& renderer = it->second;
 
   if (aRender) {
     renderer->UpdateAndRender(aReadbackSize, aReadbackBuffer);
   } else {
     renderer->Update();
   }
+  // Check graphics reset status even when rendering is skipped.
+  renderer->CheckGraphicsResetStatus();
 
   TimeStamp end = TimeStamp::Now();
 
   auto info = renderer->FlushPipelineInfo();
   RefPtr<layers::AsyncImagePipelineManager> pipelineMgr =
       renderer->GetCompositorBridge()->GetAsyncImagePipelineManager();
   // pipelineMgr should always be non-null here because it is only nulled out
   // after the WebRenderAPI instance for the CompositorBridgeParent is
@@ -390,17 +396,18 @@ RenderThread::UpdateAndRender(wr::Window
   MOZ_ASSERT(pipelineMgr);
   pipelineMgr->NotifyPipelinesUpdated(info);
 
   layers::CompositorThreadHolder::Loop()->PostTask(NewRunnableFunction(
     "NotifyDidRenderRunnable",
     &NotifyDidRender,
     renderer->GetCompositorBridge(),
     info,
-    aStartTime, end
+    aStartTime, end,
+    aRender
   ));
 }
 
 void
 RenderThread::Pause(wr::WindowId aWindowId)
 {
   MOZ_ASSERT(IsInRenderThread());
 
--- a/gfx/webrender_bindings/RendererOGL.cpp
+++ b/gfx/webrender_bindings/RendererOGL.cpp
@@ -163,32 +163,40 @@ RendererOGL::UpdateAndRender(const Maybe
   if (mFrameStartTime) {
     uint32_t latencyMs = round((TimeStamp::Now() - mFrameStartTime).ToMilliseconds());
     printf_stderr("generate frame latencyMs latencyMs %d\n", latencyMs);
   }
   // Clear frame start time
   mFrameStartTime = TimeStamp();
 #endif
 
+  // TODO: Flush pending actions such as texture deletions/unlocks and
+  //       textureHosts recycling.
+
+  return true;
+}
+
+void
+RendererOGL::CheckGraphicsResetStatus()
+{
+  if (!mCompositor || !mCompositor->gl()) {
+    return;
+  }
+
   gl::GLContext* gl = mCompositor->gl();
   if (gl->IsSupported(gl::GLFeature::robustness)) {
     GLenum resetStatus = gl->fGetGraphicsResetStatus();
     if (resetStatus == LOCAL_GL_PURGED_CONTEXT_RESET_NV) {
       layers::CompositorThreadHolder::Loop()->PostTask(NewRunnableFunction(
         "DoNotifyWebRenderContextPurgeRunnable",
         &DoNotifyWebRenderContextPurge,
         mBridge
       ));
     }
   }
-
-  // TODO: Flush pending actions such as texture deletions/unlocks and
-  //       textureHosts recycling.
-
-  return true;
 }
 
 void
 RendererOGL::Pause()
 {
   mCompositor->Pause();
 }
 
--- a/gfx/webrender_bindings/RendererOGL.h
+++ b/gfx/webrender_bindings/RendererOGL.h
@@ -79,16 +79,19 @@ public:
               layers::CompositorBridgeParent* aBridge);
 
   /// This can be called on the render thread only.
   void Pause();
 
   /// This can be called on the render thread only.
   bool Resume();
 
+  /// This can be called on the render thread only.
+  void CheckGraphicsResetStatus();
+
   layers::SyncObjectHost* GetSyncObject() const;
 
   layers::CompositorBridgeParent* GetCompositorBridge() { return mBridge; }
 
   wr::WrPipelineInfo FlushPipelineInfo();
 
   RenderTextureHost* GetRenderTexture(wr::WrExternalImageId aExternalImageId);
 
--- a/layout/style/Loader.cpp
+++ b/layout/style/Loader.cpp
@@ -812,16 +812,17 @@ SheetLoadData::VerifySheetReadyToParse(n
       nsCOMPtr<nsIContentSecurityPolicy> csp;
       loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
       nsAutoCString spec;
       mLoader->mDocument->GetDocumentURI()->GetAsciiSpec(spec);
       // line number unknown. mRequestingNode doesn't bear this info.
       csp->LogViolationDetails(
         nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_STYLE,
         nullptr, // triggering element
+        nullptr, // nsICSPEventListener
         NS_ConvertUTF8toUTF16(spec), EmptyString(),
         0, 0, EmptyString(), EmptyString());
       return NS_OK;
     }
   } else {
     nsAutoCString sourceUri;
     if (mLoader->mDocument && mLoader->mDocument->GetDocumentURI()) {
       mLoader->mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri);
--- a/layout/style/nsStyleUtil.cpp
+++ b/layout/style/nsStyleUtil.cpp
@@ -478,17 +478,18 @@ nsStyleUtil::CSPAllowsInlineStyle(Elemen
   if (aElement && aElement->NodeInfo()->NameAtom() == nsGkAtoms::style) {
     aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::nonce, nonce);
   }
 
   bool allowInlineStyle = true;
   rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_STYLESHEET,
                             nonce,
                             false, // aParserCreated only applies to scripts
-                            aElement, aStyleText, aLineNumber, aColumnNumber,
+                            aElement, nullptr, // nsICSPEventListener
+                            aStyleText, aLineNumber, aColumnNumber,
                             &allowInlineStyle);
   NS_ENSURE_SUCCESS(rv, false);
 
   return allowInlineStyle;
 }
 
 void
 nsStyleUtil::AppendFontSlantStyle(const FontSlantStyle& aStyle, nsAString& aOut)
--- a/media/audioipc/README_MOZILLA
+++ b/media/audioipc/README_MOZILLA
@@ -1,8 +1,8 @@
 The source from this directory was copied from the audioipc-2
 git repository using the update.sh script.  The only changes
 made were those applied by update.sh and the addition of
 Makefile.in build files for the Mozilla build system.
 
 The audioipc-2 git repository is: https://github.com/djg/audioipc-2.git
 
-The git commit ID used was 3d716fe897ccb3ea43a2af0c794ea57c433400d7 (2018-07-30 08:51:04 +1200)
+The git commit ID used was 572d6a6a16501cde726dcc09604a0cbc895d93e3 (2018-10-23 16:43:12 +1300)
--- a/media/audioipc/audioipc/src/async.rs
+++ b/media/audioipc/audioipc/src/async.rs
@@ -1,19 +1,19 @@
 // Copyright © 2017 Mozilla Foundation
 //
 // This program is made available under an ISC-style license.  See the
 // accompanying file LICENSE for details
 
 //! Various async helpers modelled after futures-rs and tokio-io.
 
-use {RecvMsg, SendMsg};
 use bytes::{Buf, BufMut};
 use futures::{Async, Poll};
 use iovec::IoVec;
+use msg::{RecvMsg, SendMsg};
 use std::io;
 use tokio_io::{AsyncRead, AsyncWrite};
 use tokio_uds::UnixStream;
 
 pub trait AsyncRecvMsg: AsyncRead {
     /// Pull some bytes from this source into the specified `Buf`, returning
     /// how many bytes were read.
     ///
@@ -134,17 +134,17 @@ impl AsyncSendMsg for UnixStream {
         }
         let r = {
             // The `IoVec` type can't have a zero-length size, so create a dummy
             // version from a 1-length slice which we'll overwrite with the
             // `bytes_vec` method.
             static DUMMY: &[u8] = &[0];
             let nom = <&IoVec>::from(DUMMY);
             let mut bufs = [
-                nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom
+                nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom,
             ];
             let n = buf.bytes_vec(&mut bufs);
             self.send_msg(&bufs[..n], cmsg.bytes())
         };
         match r {
             Ok(n) => {
                 buf.advance(n);
                 Ok(Async::Ready(n))
--- a/media/audioipc/audioipc/src/cmsg.rs
+++ b/media/audioipc/audioipc/src/cmsg.rs
@@ -1,17 +1,17 @@
 // Copyright © 2017 Mozilla Foundation
 //
 // This program is made available under an ISC-style license.  See the
 // accompanying file LICENSE for details
 
 use bytes::{BufMut, Bytes, BytesMut};
 use libc::{self, cmsghdr};
+use std::os::unix::io::RawFd;
 use std::{convert, mem, ops, slice};
-use std::os::unix::io::RawFd;
 
 #[derive(Clone, Debug)]
 pub struct Fds {
     fds: Bytes,
 }
 
 impl convert::AsRef<[RawFd]> for Fds {
     fn as_ref(&self) -> &[RawFd] {
--- a/media/audioipc/audioipc/src/codec.rs
+++ b/media/audioipc/audioipc/src/codec.rs
@@ -159,17 +159,17 @@ where
             return Err(io::Error::new(
                 io::ErrorKind::InvalidInput,
                 "encoded message too big",
             ));
         }
 
         buf.reserve((encoded_len + 2) as usize);
 
-        buf.put_u16::<LittleEndian>(encoded_len as u16);
+        buf.put_u16_le(encoded_len as u16);
 
         if let Err(e) = bincode::config()
             .limit(encoded_len)
             .serialize_into::<_, Self::In>(&mut buf.writer(), &item)
         {
             match *e {
                 bincode::ErrorKind::Io(e) => return Err(e),
                 _ => return Err(io::Error::new(io::ErrorKind::Other, *e)),
--- a/media/audioipc/audioipc/src/core.rs
+++ b/media/audioipc/audioipc/src/core.rs
@@ -1,14 +1,19 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details.
+
 // Ease accessing reactor::Core handles.
 
+use futures::sync::oneshot;
 use futures::{Future, IntoFuture};
-use futures::sync::oneshot;
+use std::sync::mpsc;
 use std::{fmt, io, thread};
-use std::sync::mpsc;
 use tokio_core::reactor::{Core, Handle, Remote};
 
 scoped_thread_local! {
     static HANDLE: Handle
 }
 
 pub fn handle() -> Handle {
     HANDLE.with(|handle| handle.clone())
--- a/media/audioipc/audioipc/src/errors.rs
+++ b/media/audioipc/audioipc/src/errors.rs
@@ -1,8 +1,13 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details.
+
 use bincode;
 use cubeb;
 use std;
 
 error_chain! {
     // Maybe replace with chain_err to improve the error info.
     foreign_links {
         Bincode(bincode::Error);
--- a/media/audioipc/audioipc/src/fd_passing.rs
+++ b/media/audioipc/audioipc/src/fd_passing.rs
@@ -5,19 +5,19 @@
 
 use async::{AsyncRecvMsg, AsyncSendMsg};
 use bytes::{Bytes, BytesMut, IntoBuf};
 use cmsg;
 use codec::Codec;
 use futures::{AsyncSink, Poll, Sink, StartSend, Stream};
 use libc;
 use messages::AssocRawFd;
-use std::{fmt, io, mem};
 use std::collections::VecDeque;
 use std::os::unix::io::RawFd;
+use std::{fmt, io, mem};
 
 const INITIAL_CAPACITY: usize = 1024;
 const BACKPRESSURE_THRESHOLD: usize = 4 * INITIAL_CAPACITY;
 const FDS_CAPACITY: usize = 16;
 
 struct IncomingFds {
     cmsg: BytesMut,
     recv_fds: Option<cmsg::ControlMsgIter>,
@@ -29,17 +29,18 @@ impl IncomingFds {
         IncomingFds {
             cmsg: BytesMut::with_capacity(capacity),
             recv_fds: None,
         }
     }
 
     pub fn take_fds(&mut self) -> Option<[RawFd; 3]> {
         loop {
-            let fds = self.recv_fds
+            let fds = self
+                .recv_fds
                 .as_mut()
                 .and_then(|recv_fds| recv_fds.next())
                 .and_then(|fds| Some(clone_into_array(&fds)));
 
             if fds.is_some() {
                 return fds;
             }
 
@@ -288,20 +289,16 @@ pub fn framed_with_fds<A, C>(io: A, code
         frames: VecDeque::new(),
         write_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
         outgoing_fds: BytesMut::with_capacity(
             FDS_CAPACITY * cmsg::space(mem::size_of::<[RawFd; 3]>()),
         ),
     }
 }
 
-fn write_zero() -> io::Error {
-    io::Error::new(io::ErrorKind::WriteZero, "failed to write frame to io")
-}
-
 fn clone_into_array<A, T>(slice: &[T]) -> A
 where
     A: Sized + Default + AsMut<[T]>,
     T: Clone,
 {
     let mut a = Default::default();
     <A as AsMut<[T]>>::as_mut(&mut a).clone_from_slice(slice);
     a
--- a/media/audioipc/audioipc/src/frame.rs
+++ b/media/audioipc/audioipc/src/frame.rs
@@ -142,20 +142,16 @@ where
     }
 
     fn close(&mut self) -> Poll<(), Self::SinkError> {
         try_ready!(self.poll_complete());
         self.io.shutdown()
     }
 }
 
-fn write_zero() -> io::Error {
-    io::Error::new(io::ErrorKind::WriteZero, "failed to write frame to io")
-}
-
 pub fn framed<A, C>(io: A, codec: C) -> Framed<A, C> {
     Framed {
         io: io,
         codec: codec,
         read_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
         write_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
         frame: None,
         is_readable: false,
--- a/media/audioipc/audioipc/src/lib.rs
+++ b/media/audioipc/audioipc/src/lib.rs
@@ -1,13 +1,13 @@
 // Copyright © 2017 Mozilla Foundation
 //
 // This program is made available under an ISC-style license.  See the
 // accompanying file LICENSE for details
-#![allow(dead_code)] // TODO: Remove.
+
 #![recursion_limit = "1024"]
 #[macro_use]
 extern crate error_chain;
 
 #[macro_use]
 extern crate log;
 
 #[macro_use]
@@ -24,109 +24,37 @@ extern crate memmap;
 #[macro_use]
 extern crate scoped_tls;
 extern crate serde;
 extern crate tokio_core;
 #[macro_use]
 extern crate tokio_io;
 extern crate tokio_uds;
 
-pub mod async;
-pub mod cmsg;
+mod async;
+mod cmsg;
 pub mod codec;
+pub mod core;
 pub mod errors;
 pub mod fd_passing;
 pub mod frame;
-pub mod rpc;
-pub mod core;
 pub mod messages;
 mod msg;
+pub mod rpc;
 pub mod shm;
 
-use iovec::IoVec;
-#[cfg(target_os = "linux")]
-use libc::MSG_CMSG_CLOEXEC;
 pub use messages::{ClientMessage, ServerMessage};
 use std::env::temp_dir;
-use std::io;
-use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
 use std::path::PathBuf;
-#[cfg(not(target_os = "linux"))]
-const MSG_CMSG_CLOEXEC: libc::c_int = 0;
-
-// Extend sys::os::unix::net::UnixStream to support sending and receiving a single file desc.
-// We can extend UnixStream by using traits, eliminating the need to introduce a new wrapped
-// UnixStream type.
-pub trait RecvMsg {
-    fn recv_msg(
-        &mut self,
-        iov: &mut [&mut IoVec],
-        cmsg: &mut [u8],
-    ) -> io::Result<(usize, usize, i32)>;
-}
-
-pub trait SendMsg {
-    fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize>;
-}
-
-impl<T: AsRawFd> RecvMsg for T {
-    fn recv_msg(
-        &mut self,
-        iov: &mut [&mut IoVec],
-        cmsg: &mut [u8],
-    ) -> io::Result<(usize, usize, i32)> {
-        msg::recv_msg_with_flags(self.as_raw_fd(), iov, cmsg, MSG_CMSG_CLOEXEC)
-    }
-}
-
-impl<T: AsRawFd> SendMsg for T {
-    fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize> {
-        msg::send_msg_with_flags(self.as_raw_fd(), iov, cmsg, 0)
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////
 
-#[derive(Debug)]
-pub struct AutoCloseFd(RawFd);
-
-impl Drop for AutoCloseFd {
-    fn drop(&mut self) {
-        unsafe {
-            libc::close(self.0);
-        }
-    }
-}
-
-impl FromRawFd for AutoCloseFd {
-    unsafe fn from_raw_fd(fd: RawFd) -> Self {
-        AutoCloseFd(fd)
-    }
-}
-
-impl IntoRawFd for AutoCloseFd {
-    fn into_raw_fd(self) -> RawFd {
-        let fd = self.0;
-        ::std::mem::forget(self);
-        fd
-    }
-}
-
-impl AsRawFd for AutoCloseFd {
-    fn as_raw_fd(&self) -> RawFd {
-        self.0
-    }
-}
-
-impl<'a> AsRawFd for &'a AutoCloseFd {
-    fn as_raw_fd(&self) -> RawFd {
-        self.0
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////
+// This must match the definition of
+// ipc::FileDescriptor::PlatformHandleType in Gecko.
+#[cfg(target_os = "windows")]
+pub type PlatformHandleType = *mut std::os::raw::c_void;
+#[cfg(not(target_os = "windows"))]
+pub type PlatformHandleType = libc::c_int;
 
 pub fn get_shm_path(dir: &str) -> PathBuf {
     let pid = unsafe { libc::getpid() };
     let mut temp = temp_dir();
     temp.push(&format!("cubeb-shm-{}-{}", pid, dir));
     temp
 }
--- a/media/audioipc/audioipc/src/msg.rs
+++ b/media/audioipc/audioipc/src/msg.rs
@@ -1,13 +1,53 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details.
+
+use iovec::unix as iovec;
 use iovec::IoVec;
-use iovec::unix as iovec;
 use libc;
+use std::os::unix::io::{AsRawFd, RawFd};
 use std::{cmp, io, mem, ptr};
-use std::os::unix::io::RawFd;
+
+// Extend sys::os::unix::net::UnixStream to support sending and receiving a single file desc.
+// We can extend UnixStream by using traits, eliminating the need to introduce a new wrapped
+// UnixStream type.
+pub trait RecvMsg {
+    fn recv_msg(
+        &mut self,
+        iov: &mut [&mut IoVec],
+        cmsg: &mut [u8],
+    ) -> io::Result<(usize, usize, i32)>;
+}
+
+pub trait SendMsg {
+    fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize>;
+}
+
+impl<T: AsRawFd> RecvMsg for T {
+    fn recv_msg(
+        &mut self,
+        iov: &mut [&mut IoVec],
+        cmsg: &mut [u8],
+    ) -> io::Result<(usize, usize, i32)> {
+        #[cfg(target_os = "linux")]
+        let flags = libc::MSG_CMSG_CLOEXEC;
+        #[cfg(not(target_os = "linux"))]
+        let flags = 0;
+        recv_msg_with_flags(self.as_raw_fd(), iov, cmsg, flags)
+    }
+}
+
+impl<T: AsRawFd> SendMsg for T {
+    fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize> {
+        send_msg_with_flags(self.as_raw_fd(), iov, cmsg, 0)
+    }
+}
 
 fn cvt(r: libc::ssize_t) -> io::Result<usize> {
     if r == -1 {
         Err(io::Error::last_os_error())
     } else {
         Ok(r as usize)
     }
 }
--- a/media/audioipc/audioipc/src/rpc/client/mod.rs
+++ b/media/audioipc/audioipc/src/rpc/client/mod.rs
@@ -34,20 +34,20 @@
 // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
 // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
+use futures::sync::oneshot;
 use futures::{Async, Future, Poll, Sink, Stream};
-use futures::sync::oneshot;
+use rpc::driver::Driver;
 use rpc::Handler;
-use rpc::driver::Driver;
 use std::collections::VecDeque;
 use std::io;
 use tokio_core::reactor::Handle;
 
 mod proxy;
 
 pub use self::proxy::{ClientProxy, Response};
 
--- a/media/audioipc/audioipc/src/rpc/client/proxy.rs
+++ b/media/audioipc/audioipc/src/rpc/client/proxy.rs
@@ -38,18 +38,18 @@
 // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
 // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
+use futures::sync::{mpsc, oneshot};
 use futures::{Async, Future, Poll};
-use futures::sync::{mpsc, oneshot};
 use std::fmt;
 use std::io;
 
 /// Message used to dispatch requests to the task managing the
 /// client connection.
 pub type Request<R, Q> = (R, oneshot::Sender<Q>);
 
 /// Receive requests submitted to the client
--- a/media/audioipc/audioipc/src/rpc/mod.rs
+++ b/media/audioipc/audioipc/src/rpc/mod.rs
@@ -1,18 +1,18 @@
 // Copyright © 2017 Mozilla Foundation
 //
 // This program is made available under an ISC-style license.  See the
 // accompanying file LICENSE for details
 
 use futures::{Poll, Sink, Stream};
 use std::io;
 
+mod client;
 mod driver;
-mod client;
 mod server;
 
 pub use self::client::{bind_client, Client, ClientProxy, Response};
 pub use self::server::{bind_server, Server};
 
 pub trait Handler {
     /// Message type read from transport
     type In;
--- a/media/audioipc/audioipc/src/rpc/server.rs
+++ b/media/audioipc/audioipc/src/rpc/server.rs
@@ -35,18 +35,18 @@
 // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
 // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
 use futures::{Async, Future, Poll, Sink, Stream};
+use rpc::driver::Driver;
 use rpc::Handler;
-use rpc::driver::Driver;
 use std::collections::VecDeque;
 use std::io;
 use tokio_core::reactor::Handle;
 
 /// Bind an async I/O object `io` to the `server`.
 pub fn bind_server<S>(transport: S::Transport, server: S, handle: &Handle)
 where
     S: Server,
--- a/media/audioipc/audioipc/src/shm.rs
+++ b/media/audioipc/audioipc/src/shm.rs
@@ -1,8 +1,13 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details.
+
 use errors::*;
 use memmap::{Mmap, MmapViewSync, Protection};
 use std::fs::{remove_file, File, OpenOptions};
 use std::path::Path;
 use std::sync::atomic;
 
 pub struct SharedMemReader {
     mmap: Mmap,
--- a/media/audioipc/client/src/context.rs
+++ b/media/audioipc/client/src/context.rs
@@ -1,34 +1,36 @@
 // Copyright © 2017 Mozilla Foundation
 //
 // This program is made available under an ISC-style license.  See the
 // accompanying file LICENSE for details
 
-use {ClientStream, CPUPOOL_INIT_PARAMS, G_SERVER_FD};
 use assert_not_in_callback;
-use audioipc::{messages, ClientMessage, ServerMessage};
-use audioipc::{core, rpc};
 use audioipc::codec::LengthDelimitedCodec;
 use audioipc::fd_passing::{framed_with_fds, FramedWithFds};
-use cubeb_backend::{ffi, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceType, Error,
-                    Ops, Result, Stream, StreamParams, StreamParamsRef};
+use audioipc::{core, rpc};
+use audioipc::{messages, ClientMessage, ServerMessage};
+use cubeb_backend::{
+    ffi, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceType, Error, Ops, Result,
+    Stream, StreamParams, StreamParamsRef,
+};
 use futures::Future;
 use futures_cpupool::{self, CpuPool};
 use libc;
-use std::{fmt, io, mem, ptr};
 use std::ffi::{CStr, CString};
 use std::os::raw::c_void;
 use std::os::unix::io::FromRawFd;
 use std::os::unix::net;
 use std::sync::mpsc;
 use std::thread;
+use std::{fmt, io, mem, ptr};
 use stream;
 use tokio_core::reactor::{Handle, Remote};
 use tokio_uds::UnixStream;
+use {ClientStream, CPUPOOL_INIT_PARAMS, G_SERVER_FD};
 
 struct CubebClient;
 
 impl rpc::Client for CubebClient {
     type Request = ServerMessage;
     type Response = ClientMessage;
     type Transport = FramedWithFds<UnixStream, LengthDelimitedCodec<Self::Request, Self::Response>>;
 }
@@ -95,19 +97,17 @@ impl ContextOps for ClientContext {
             let _ = tx_rpc.send(rpc);
             Some(())
         }
 
         assert_not_in_callback();
 
         let (tx_rpc, rx_rpc) = mpsc::channel();
 
-        let params = CPUPOOL_INIT_PARAMS.with(|p| {
-            p.replace(None).unwrap()
-        });
+        let params = CPUPOOL_INIT_PARAMS.with(|p| p.replace(None).unwrap());
 
         let thread_create_callback = params.thread_create_callback;
 
         let register_thread = move || {
             if let Some(func) = thread_create_callback {
                 let thr = thread::current();
                 let name = CString::new(thr.name().unwrap()).unwrap();
                 func(name.as_ptr());
@@ -129,21 +129,21 @@ impl ContextOps for ClientContext {
                         "Failed to open stream and create rpc.",
                     )
                 })
         }));
 
         let rpc = t!(rx_rpc.recv());
 
         let cpupool = futures_cpupool::Builder::new()
-                .name_prefix("AudioIPC")
-                .after_start(register_thread)
-                .pool_size(params.pool_size)
-                .stack_size(params.stack_size)
-                .create();
+            .name_prefix("AudioIPC")
+            .after_start(register_thread)
+            .pool_size(params.pool_size)
+            .stack_size(params.stack_size)
+            .create();
 
         let ctx = Box::new(ClientContext {
             _ops: &CLIENT_OPS as *const _,
             rpc: rpc,
             core: core,
             cpu_pool: cpupool,
         });
         Ok(unsafe { Context::from_ptr(Box::into_raw(ctx) as *mut _) })
--- a/media/audioipc/client/src/lib.rs
+++ b/media/audioipc/client/src/lib.rs
@@ -15,45 +15,49 @@ extern crate log;
 extern crate tokio_core;
 extern crate tokio_uds;
 
 #[macro_use]
 mod send_recv;
 mod context;
 mod stream;
 
+use audioipc::PlatformHandleType;
 use context::ClientContext;
 use cubeb_backend::{capi, ffi};
 use std::os::raw::{c_char, c_int};
 use std::os::unix::io::RawFd;
 use stream::ClientStream;
 
 type InitParamsTls = std::cell::RefCell<Option<CpuPoolInitParams>>;
 
 thread_local!(static IN_CALLBACK: std::cell::RefCell<bool> = std::cell::RefCell::new(false));
 thread_local!(static CPUPOOL_INIT_PARAMS: InitParamsTls = std::cell::RefCell::new(None));
 
+// This must match the definition of AudioIpcInitParams in
+// dom/media/CubebUtils.cpp in Gecko.
 #[repr(C)]
 #[derive(Clone, Copy, Debug)]
 pub struct AudioIpcInitParams {
-    pub server_connection: c_int,
+    // Fields only need to be public for ipctest.
+    pub server_connection: PlatformHandleType,
     pub pool_size: usize,
     pub stack_size: usize,
     pub thread_create_callback: Option<extern "C" fn(*const ::std::os::raw::c_char)>,
 }
 
 #[derive(Clone, Copy, Debug)]
 struct CpuPoolInitParams {
-    pub pool_size: usize,
-    pub stack_size: usize,
-    pub thread_create_callback: Option<extern "C" fn(*const ::std::os::raw::c_char)>,
+    pool_size: usize,
+    stack_size: usize,
+    thread_create_callback: Option<extern "C" fn(*const ::std::os::raw::c_char)>,
 }
 
 impl CpuPoolInitParams {
-    pub fn init_with(params: &AudioIpcInitParams) -> Self {
+    fn init_with(params: &AudioIpcInitParams) -> Self {
         CpuPoolInitParams {
             pool_size: params.pool_size,
             stack_size: params.stack_size,
             thread_create_callback: params.thread_create_callback,
         }
     }
 }
 
--- a/media/audioipc/client/src/send_recv.rs
+++ b/media/audioipc/client/src/send_recv.rs
@@ -1,8 +1,13 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details.
+
 use cubeb_backend::Error;
 use std::os::raw::c_int;
 
 #[doc(hidden)]
 pub fn _err<E>(e: E) -> Error
 where
     E: Into<Option<c_int>>,
 {
--- a/media/audioipc/client/src/stream.rs
+++ b/media/audioipc/client/src/stream.rs
@@ -1,31 +1,31 @@
 // Copyright © 2017 Mozilla Foundation
 //
 // This program is made available under an ISC-style license.  See the
 // accompanying file LICENSE for details
 
-use {assert_not_in_callback, set_in_callback};
-use ClientContext;
 use audioipc::codec::LengthDelimitedCodec;
 use audioipc::frame::{framed, Framed};
 use audioipc::messages::{self, CallbackReq, CallbackResp, ClientMessage, ServerMessage};
 use audioipc::rpc;
 use audioipc::shm::{SharedMemMutSlice, SharedMemSlice};
 use cubeb_backend::{ffi, DeviceRef, Error, Result, Stream, StreamOps};
 use futures::Future;
 use futures_cpupool::{CpuFuture, CpuPool};
 use std::ffi::CString;
 use std::fs::File;
 use std::os::raw::c_void;
 use std::os::unix::io::FromRawFd;
 use std::os::unix::net;
 use std::ptr;
 use std::sync::mpsc;
 use tokio_uds::UnixStream;
+use ClientContext;
+use {assert_not_in_callback, set_in_callback};
 
 // TODO: Remove and let caller allocate based on cubeb backend requirements.
 const SHM_AREA_SIZE: usize = 2 * 1024 * 1024;
 
 pub struct Device(ffi::cubeb_device);
 
 impl Drop for Device {
     fn drop(&mut self) {
@@ -90,24 +90,24 @@ impl rpc::Server for CallbackServer {
 
                 self.cpu_pool.spawn_fn(move || {
                     // TODO: This is proof-of-concept. Make it better.
                     let input_ptr: *const u8 = match input_shm {
                         Some(shm) => shm
                             .get_slice(nframes as usize * frame_size)
                             .unwrap()
                             .as_ptr(),
-                        None => ptr::null()
+                        None => ptr::null(),
                     };
                     let output_ptr: *mut u8 = match output_shm {
                         Some(ref mut shm) => shm
                             .get_mut_slice(nframes as usize * frame_size)
                             .unwrap()
                             .as_mut_ptr(),
-                        None => ptr::null_mut()
+                        None => ptr::null_mut(),
                     };
 
                     set_in_callback(true);
                     let nframes = unsafe {
                         cb(
                             ptr::null_mut(),
                             user_ptr as *mut c_void,
                             input_ptr as *const _,
--- a/media/audioipc/server/src/lib.rs
+++ b/media/audioipc/server/src/lib.rs
@@ -1,451 +1,59 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details
+
 #[macro_use]
 extern crate error_chain;
 
 #[macro_use]
 extern crate log;
 
 extern crate audioipc;
 extern crate bytes;
 extern crate cubeb_core as cubeb;
 extern crate futures;
 extern crate lazycell;
 extern crate libc;
 extern crate slab;
 extern crate tokio_core;
 extern crate tokio_uds;
 
-use audioipc::codec::LengthDelimitedCodec;
 use audioipc::core;
-use audioipc::fd_passing::{framed_with_fds, FramedWithFds};
-use audioipc::frame::{framed, Framed};
-use audioipc::messages::{CallbackReq, CallbackResp, ClientMessage, Device, DeviceInfo,
-                         ServerMessage, StreamCreate, StreamInitParams, StreamParams};
+use audioipc::fd_passing::framed_with_fds;
 use audioipc::rpc;
-use audioipc::shm::{SharedMemReader, SharedMemWriter};
-use cubeb::ffi;
-use futures::future::{self, FutureResult};
+use audioipc::PlatformHandleType;
 use futures::sync::oneshot;
 use futures::Future;
-use std::cell::RefCell;
-use std::convert::From;
 use std::error::Error;
-use std::ffi::{CStr, CString};
-use std::mem::{size_of, ManuallyDrop};
-use std::os::raw::{c_long, c_void};
+use std::os::raw::c_void;
+use std::os::unix::io::IntoRawFd;
 use std::os::unix::net;
-use std::os::unix::prelude::*;
-use std::{panic, ptr, slice};
-use tokio_core::reactor::Remote;
+use std::ptr;
 use tokio_uds::UnixStream;
 
+mod server;
+
 pub mod errors {
     error_chain! {
         links {
             AudioIPC(::audioipc::errors::Error, ::audioipc::errors::ErrorKind);
         }
         foreign_links {
             Cubeb(::cubeb::Error);
             Io(::std::io::Error);
             Canceled(::futures::sync::oneshot::Canceled);
         }
     }
 }
 
 use errors::*;
 
-type ContextKey = RefCell<Option<cubeb::Result<cubeb::Context>>>;
-thread_local!(static CONTEXT_KEY:ContextKey = RefCell::new(None));
-
-fn with_local_context<T, F>(f: F) -> T
-where
-    F: FnOnce(&cubeb::Result<cubeb::Context>) -> T,
-{
-    CONTEXT_KEY.with(|k| {
-        let mut context = k.borrow_mut();
-        if context.is_none() {
-            let name = CString::new("AudioIPC Server").unwrap();
-            *context = Some(cubeb::Context::init(Some(name.as_c_str()), None));
-        }
-        f(context.as_ref().unwrap())
-    })
-}
-
-// TODO: Remove and let caller allocate based on cubeb backend requirements.
-const SHM_AREA_SIZE: usize = 2 * 1024 * 1024;
-
-// The size in which the stream slab is grown.
-const STREAM_CONN_CHUNK_SIZE: usize = 64;
-
-struct CallbackClient;
-
-impl rpc::Client for CallbackClient {
-    type Request = CallbackReq;
-    type Response = CallbackResp;
-    type Transport = Framed<UnixStream, LengthDelimitedCodec<Self::Request, Self::Response>>;
-}
-
-struct ServerStreamCallbacks {
-    /// Size of input frame in bytes
-    input_frame_size: u16,
-    /// Size of output frame in bytes
-    output_frame_size: u16,
-    /// Shared memory buffer for sending input data to client
-    input_shm: SharedMemWriter,
-    /// Shared memory buffer for receiving output data from client
-    output_shm: SharedMemReader,
-    /// RPC interface to callback server running in client
-    rpc: rpc::ClientProxy<CallbackReq, CallbackResp>,
-}
-
-impl ServerStreamCallbacks {
-    fn data_callback(&mut self, input: &[u8], output: &mut [u8]) -> isize {
-        trace!("Stream data callback: {} {}", input.len(), output.len());
-
-        // len is of input and output is frame len. Turn these into the real lengths.
-        let real_input = unsafe {
-            let nbytes = input.len() * self.input_frame_size as usize;
-            slice::from_raw_parts(input.as_ptr(), nbytes)
-        };
-
-        self.input_shm.write(real_input).unwrap();
-
-        let r = self.rpc
-            .call(CallbackReq::Data(
-                output.len() as isize,
-                self.output_frame_size as usize,
-            ))
-            .wait();
-
-        match r {
-            Ok(CallbackResp::Data(frames)) => {
-                if frames >= 0 {
-                    let nbytes = frames as usize * self.output_frame_size as usize;
-                    let real_output = unsafe {
-                        trace!("Resize output to {}", nbytes);
-                        slice::from_raw_parts_mut(output.as_mut_ptr(), nbytes)
-                    };
-                    self.output_shm.read(&mut real_output[..nbytes]).unwrap();
-                }
-                frames
-            }
-            _ => {
-                debug!("Unexpected message {:?} during data_callback", r);
-                -1
-            }
-        }
-    }
-
-    fn state_callback(&mut self, state: cubeb::State) {
-        trace!("Stream state callback: {:?}", state);
-        let r = self.rpc.call(CallbackReq::State(state.into())).wait();
-        match r {
-            Ok(CallbackResp::State) => {}
-            _ => {
-                debug!("Unexpected message {:?} during callback", r);
-            }
-        }
-    }
-}
-
-struct ServerStream {
-    stream: ManuallyDrop<cubeb::Stream>,
-    cbs: ManuallyDrop<Box<ServerStreamCallbacks>>,
-}
-
-impl Drop for ServerStream {
-    fn drop(&mut self) {
-        unsafe {
-            ManuallyDrop::drop(&mut self.stream);
-            ManuallyDrop::drop(&mut self.cbs);
-        }
-    }
-}
-
-type StreamSlab = slab::Slab<ServerStream, usize>;
-
-pub struct CubebServer {
-    cb_remote: Remote,
-    streams: StreamSlab,
-}
-
-impl rpc::Server for CubebServer {
-    type Request = ServerMessage;
-    type Response = ClientMessage;
-    type Future = FutureResult<Self::Response, ()>;
-    type Transport = FramedWithFds<UnixStream, LengthDelimitedCodec<Self::Response, Self::Request>>;
-
-    fn process(&mut self, req: Self::Request) -> Self::Future {
-        let resp = with_local_context(|context| match *context {
-            Err(_) => error(cubeb::Error::error()),
-            Ok(ref context) => self.process_msg(context, &req),
-        });
-        future::ok(resp)
-    }
-}
-
-impl CubebServer {
-    pub fn new(cb_remote: Remote) -> Self {
-        CubebServer {
-            cb_remote: cb_remote,
-            streams: StreamSlab::with_capacity(STREAM_CONN_CHUNK_SIZE),
-        }
-    }
-
-    // Process a request coming from the client.
-    fn process_msg(&mut self, context: &cubeb::Context, msg: &ServerMessage) -> ClientMessage {
-        let resp: ClientMessage = match *msg {
-            ServerMessage::ClientConnect => panic!("already connected"),
-
-            ServerMessage::ClientDisconnect => {
-                // TODO:
-                //self.connection.client_disconnect();
-                ClientMessage::ClientDisconnected
-            }
-
-            ServerMessage::ContextGetBackendId => ClientMessage::ContextBackendId(),
-
-            ServerMessage::ContextGetMaxChannelCount => context
-                .max_channel_count()
-                .map(ClientMessage::ContextMaxChannelCount)
-                .unwrap_or_else(error),
-
-            ServerMessage::ContextGetMinLatency(ref params) => {
-                let format = cubeb::SampleFormat::from(params.format);
-                let layout = cubeb::ChannelLayout::from(params.layout);
-
-                let params = cubeb::StreamParamsBuilder::new()
-                    .format(format)
-                    .rate(u32::from(params.rate))
-                    .channels(u32::from(params.channels))
-                    .layout(layout)
-                    .take();
-
-                context
-                    .min_latency(&params)
-                    .map(ClientMessage::ContextMinLatency)
-                    .unwrap_or_else(error)
-            }
-
-            ServerMessage::ContextGetPreferredSampleRate => context
-                .preferred_sample_rate()
-                .map(ClientMessage::ContextPreferredSampleRate)
-                .unwrap_or_else(error),
-
-            ServerMessage::ContextGetDeviceEnumeration(device_type) => context
-                .enumerate_devices(cubeb::DeviceType::from_bits_truncate(device_type))
-                .map(|devices| {
-                    let v: Vec<DeviceInfo> = devices.iter().map(|i| i.as_ref().into()).collect();
-                    ClientMessage::ContextEnumeratedDevices(v)
-                })
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamInit(ref params) => self.process_stream_init(context, params)
-                .unwrap_or_else(|_| error(cubeb::Error::error())),
-
-            ServerMessage::StreamDestroy(stm_tok) => {
-                self.streams.remove(stm_tok);
-                ClientMessage::StreamDestroyed
-            }
-
-            ServerMessage::StreamStart(stm_tok) => self.streams[stm_tok]
-                .stream
-                .start()
-                .map(|_| ClientMessage::StreamStarted)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamStop(stm_tok) => self.streams[stm_tok]
-                .stream
-                .stop()
-                .map(|_| ClientMessage::StreamStopped)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamResetDefaultDevice(stm_tok) => self.streams[stm_tok]
-                .stream
-                .reset_default_device()
-                .map(|_| ClientMessage::StreamDefaultDeviceReset)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamGetPosition(stm_tok) => self.streams[stm_tok]
-                .stream
-                .position()
-                .map(ClientMessage::StreamPosition)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamGetLatency(stm_tok) => self.streams[stm_tok]
-                .stream
-                .latency()
-                .map(ClientMessage::StreamLatency)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamSetVolume(stm_tok, volume) => self.streams[stm_tok]
-                .stream
-                .set_volume(volume)
-                .map(|_| ClientMessage::StreamVolumeSet)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamSetPanning(stm_tok, panning) => self.streams[stm_tok]
-                .stream
-                .set_panning(panning)
-                .map(|_| ClientMessage::StreamPanningSet)
-                .unwrap_or_else(error),
-
-            ServerMessage::StreamGetCurrentDevice(stm_tok) => self.streams[stm_tok]
-                .stream
-                .current_device()
-                .map(|device| ClientMessage::StreamCurrentDevice(Device::from(device)))
-                .unwrap_or_else(error),
-        };
-
-        trace!("process_msg: req={:?}, resp={:?}", msg, resp);
-
-        resp
-    }
-
-    // Stream init is special, so it's been separated from process_msg.
-    fn process_stream_init(
-        &mut self,
-        context: &cubeb::Context,
-        params: &StreamInitParams,
-    ) -> Result<ClientMessage> {
-        fn frame_size_in_bytes(params: Option<&StreamParams>) -> u16 {
-            params
-                .map(|p| {
-                    let format = p.format.into();
-                    let sample_size = match format {
-                        cubeb::SampleFormat::S16LE
-                        | cubeb::SampleFormat::S16BE
-                        | cubeb::SampleFormat::S16NE => 2,
-                        cubeb::SampleFormat::Float32LE
-                        | cubeb::SampleFormat::Float32BE
-                        | cubeb::SampleFormat::Float32NE => 4,
-                    };
-                    let channel_count = p.channels as u16;
-                    sample_size * channel_count
-                })
-                .unwrap_or(0u16)
-        }
-
-        // Create the callback handling struct which is attached the cubeb stream.
-        let input_frame_size = frame_size_in_bytes(params.input_stream_params.as_ref());
-        let output_frame_size = frame_size_in_bytes(params.output_stream_params.as_ref());
-
-        let (stm1, stm2) = net::UnixStream::pair()?;
-        debug!("Created callback pair: {:?}-{:?}", stm1, stm2);
-        let (input_shm, input_file) =
-            SharedMemWriter::new(&audioipc::get_shm_path("input"), SHM_AREA_SIZE)?;
-        let (output_shm, output_file) =
-            SharedMemReader::new(&audioipc::get_shm_path("output"), SHM_AREA_SIZE)?;
-
-        // This code is currently running on the Client/Server RPC
-        // handling thread.  We need to move the registration of the
-        // bind_client to the callback RPC handling thread.  This is
-        // done by spawning a future on cb_remote.
-
-        let id = core::handle().id();
-
-        let (tx, rx) = oneshot::channel();
-        self.cb_remote.spawn(move |handle| {
-            // Ensure we're running on a loop different to the one
-            // invoking spawn_fn.
-            assert_ne!(id, handle.id());
-            let stream = UnixStream::from_stream(stm2, handle).unwrap();
-            let transport = framed(stream, Default::default());
-            let rpc = rpc::bind_client::<CallbackClient>(transport, handle);
-            drop(tx.send(rpc));
-            Ok(())
-        });
-
-        let rpc: rpc::ClientProxy<CallbackReq, CallbackResp> = match rx.wait() {
-            Ok(rpc) => rpc,
-            Err(_) => bail!("Failed to create callback rpc."),
-        };
-
-        let cbs = Box::new(ServerStreamCallbacks {
-            input_frame_size,
-            output_frame_size,
-            input_shm,
-            output_shm,
-            rpc,
-        });
-
-        // Create cubeb stream from params
-        let stream_name = params
-            .stream_name
-            .as_ref()
-            .and_then(|name| CStr::from_bytes_with_nul(name).ok());
-
-        let input_device = params.input_device as *const _;
-        let input_stream_params = params.input_stream_params.as_ref().map(|isp| unsafe {
-            cubeb::StreamParamsRef::from_ptr(isp as *const StreamParams as *mut _)
-        });
-
-        let output_device = params.output_device as *const _;
-        let output_stream_params = params.output_stream_params.as_ref().map(|osp| unsafe {
-            cubeb::StreamParamsRef::from_ptr(osp as *const StreamParams as *mut _)
-        });
-
-        let latency = params.latency_frames;
-        assert!(size_of::<Box<ServerStreamCallbacks>>() == size_of::<usize>());
-        let user_ptr = cbs.as_ref() as *const ServerStreamCallbacks as *mut c_void;
-
-        unsafe {
-            context
-                .stream_init(
-                    stream_name,
-                    input_device,
-                    input_stream_params,
-                    output_device,
-                    output_stream_params,
-                    latency,
-                    Some(data_cb_c),
-                    Some(state_cb_c),
-                    user_ptr,
-                )
-                .and_then(|stream| {
-                    if !self.streams.has_available() {
-                        trace!(
-                            "server connection ran out of stream slots. reserving {} more.",
-                            STREAM_CONN_CHUNK_SIZE
-                        );
-                        self.streams.reserve_exact(STREAM_CONN_CHUNK_SIZE);
-                    }
-
-                    let stm_tok = match self.streams.vacant_entry() {
-                        Some(entry) => {
-                            debug!("Registering stream {:?}", entry.index(),);
-
-                            entry
-                                .insert(ServerStream {
-                                    stream: ManuallyDrop::new(stream),
-                                    cbs: ManuallyDrop::new(cbs),
-                                })
-                                .index()
-                        }
-                        None => {
-                            // TODO: Turn into error
-                            panic!("Failed to insert stream into slab. No entries")
-                        }
-                    };
-
-                    Ok(ClientMessage::StreamCreated(StreamCreate {
-                        token: stm_tok,
-                        fds: [
-                            stm1.into_raw_fd(),
-                            input_file.into_raw_fd(),
-                            output_file.into_raw_fd(),
-                        ],
-                    }))
-                })
-                .map_err(|e| e.into())
-        }
-    }
-}
-
 struct ServerWrapper {
     core_thread: core::CoreThread,
     callback_thread: core::CoreThread,
 }
 
 fn run() -> Result<ServerWrapper> {
     trace!("Starting up cubeb audio server event loop thread...");
 
@@ -482,17 +90,17 @@ fn run() -> Result<ServerWrapper> {
 pub extern "C" fn audioipc_server_start() -> *mut c_void {
     match run() {
         Ok(server) => Box::into_raw(Box::new(server)) as *mut _,
         Err(_) => ptr::null_mut() as *mut _,
     }
 }
 
 #[no_mangle]
-pub extern "C" fn audioipc_server_new_client(p: *mut c_void) -> libc::c_int {
+pub extern "C" fn audioipc_server_new_client(p: *mut c_void) -> PlatformHandleType {
     let (wait_tx, wait_rx) = oneshot::channel();
     let wrapper: &ServerWrapper = unsafe { &*(p as *mut _) };
 
     let cb_remote = wrapper.callback_thread.remote();
 
     // We create a pair of connected unix domain sockets. One socket is
     // registered with the reactor core, the other is returned to the
     // caller.
@@ -500,70 +108,26 @@ pub extern "C" fn audioipc_server_new_cl
         .and_then(|(sock1, sock2)| {
             // Spawn closure to run on same thread as reactor::Core
             // via remote handle.
             wrapper.core_thread.remote().spawn(|handle| {
                 trace!("Incoming connection");
                 UnixStream::from_stream(sock2, handle)
                     .and_then(|sock| {
                         let transport = framed_with_fds(sock, Default::default());
-                        rpc::bind_server(transport, CubebServer::new(cb_remote), handle);
+                        rpc::bind_server(transport, server::CubebServer::new(cb_remote), handle);
                         Ok(())
-                    })
-                    .map_err(|_| ())
+                    }).map_err(|_| ())
                     // Notify waiting thread that sock2 has been registered.
                     .and_then(|_| wait_tx.send(()))
             });
             // Wait for notification that sock2 has been registered
             // with reactor::Core.
             let _ = wait_rx.wait();
             Ok(sock1.into_raw_fd())
-        })
-        .unwrap_or(-1)
+        }).unwrap_or(-1)
 }
 
 #[no_mangle]
 pub extern "C" fn audioipc_server_stop(p: *mut c_void) {
     let wrapper = unsafe { Box::<ServerWrapper>::from_raw(p as *mut _) };
     drop(wrapper);
 }
-
-fn error(error: cubeb::Error) -> ClientMessage {
-    ClientMessage::Error(error.raw_code())
-}
-
-// C callable callbacks
-unsafe extern "C" fn data_cb_c(
-    _: *mut ffi::cubeb_stream,
-    user_ptr: *mut c_void,
-    input_buffer: *const c_void,
-    output_buffer: *mut c_void,
-    nframes: c_long,
-) -> c_long {
-    let ok = panic::catch_unwind(|| {
-        let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
-        let input = if input_buffer.is_null() {
-            &[]
-        } else {
-            slice::from_raw_parts(input_buffer as *const u8, nframes as usize)
-        };
-        let output: &mut [u8] = if output_buffer.is_null() {
-            &mut []
-        } else {
-            slice::from_raw_parts_mut(output_buffer as *mut u8, nframes as usize)
-        };
-        cbs.data_callback(input, output) as c_long
-    });
-    ok.unwrap_or(0)
-}
-
-unsafe extern "C" fn state_cb_c(
-    _: *mut ffi::cubeb_stream,
-    user_ptr: *mut c_void,
-    state: ffi::cubeb_state,
-) {
-    let ok = panic::catch_unwind(|| {
-        let state = cubeb::State::from(state);
-        let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
-        cbs.state_callback(state);
-    });
-    ok.expect("State callback panicked");
-}
new file mode 100644
--- /dev/null
+++ b/media/audioipc/server/src/server.rs
@@ -0,0 +1,460 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license.  See the
+// accompanying file LICENSE for details
+
+use audioipc;
+use audioipc::codec::LengthDelimitedCodec;
+use audioipc::core;
+use audioipc::fd_passing::FramedWithFds;
+use audioipc::frame::{framed, Framed};
+use audioipc::messages::{
+    CallbackReq, CallbackResp, ClientMessage, Device, DeviceInfo, ServerMessage, StreamCreate,
+    StreamInitParams, StreamParams,
+};
+use audioipc::rpc;
+use audioipc::shm::{SharedMemReader, SharedMemWriter};
+use cubeb;
+use cubeb::ffi;
+use futures::future::{self, FutureResult};
+use futures::sync::oneshot;
+use futures::Future;
+use slab;
+use std::cell::RefCell;
+use std::convert::From;
+use std::ffi::{CStr, CString};
+use std::mem::{size_of, ManuallyDrop};
+use std::os::raw::{c_long, c_void};
+use std::os::unix::io::IntoRawFd;
+use std::os::unix::net;
+use std::{panic, slice};
+use tokio_core::reactor::Remote;
+use tokio_uds::UnixStream;
+
+use errors::*;
+
+fn error(error: cubeb::Error) -> ClientMessage {
+    ClientMessage::Error(error.raw_code())
+}
+
+type ContextKey = RefCell<Option<cubeb::Result<cubeb::Context>>>;
+thread_local!(static CONTEXT_KEY:ContextKey = RefCell::new(None));
+
+fn with_local_context<T, F>(f: F) -> T
+where
+    F: FnOnce(&cubeb::Result<cubeb::Context>) -> T,
+{
+    CONTEXT_KEY.with(|k| {
+        let mut context = k.borrow_mut();
+        if context.is_none() {
+            let name = CString::new("AudioIPC Server").unwrap();
+            *context = Some(cubeb::Context::init(Some(name.as_c_str()), None));
+        }
+        f(context.as_ref().unwrap())
+    })
+}
+
+// TODO: Remove and let caller allocate based on cubeb backend requirements.
+const SHM_AREA_SIZE: usize = 2 * 1024 * 1024;
+
+// The size in which the stream slab is grown.
+const STREAM_CONN_CHUNK_SIZE: usize = 64;
+
+struct CallbackClient;
+
+impl rpc::Client for CallbackClient {
+    type Request = CallbackReq;
+    type Response = CallbackResp;
+    type Transport = Framed<UnixStream, LengthDelimitedCodec<Self::Request, Self::Response>>;
+}
+
+struct ServerStreamCallbacks {
+    /// Size of input frame in bytes
+    input_frame_size: u16,
+    /// Size of output frame in bytes
+    output_frame_size: u16,
+    /// Shared memory buffer for sending input data to client
+    input_shm: SharedMemWriter,
+    /// Shared memory buffer for receiving output data from client
+    output_shm: SharedMemReader,
+    /// RPC interface to callback server running in client
+    rpc: rpc::ClientProxy<CallbackReq, CallbackResp>,
+}
+
+impl ServerStreamCallbacks {
+    fn data_callback(&mut self, input: &[u8], output: &mut [u8]) -> isize {
+        trace!("Stream data callback: {} {}", input.len(), output.len());
+
+        // len is of input and output is frame len. Turn these into the real lengths.
+        let real_input = unsafe {
+            let nbytes = input.len() * self.input_frame_size as usize;
+            slice::from_raw_parts(input.as_ptr(), nbytes)
+        };
+
+        self.input_shm.write(real_input).unwrap();
+
+        let r = self
+            .rpc
+            .call(CallbackReq::Data(
+                output.len() as isize,
+                self.output_frame_size as usize,
+            )).wait();
+
+        match r {
+            Ok(CallbackResp::Data(frames)) => {
+                if frames >= 0 {
+                    let nbytes = frames as usize * self.output_frame_size as usize;
+                    let real_output = unsafe {
+                        trace!("Resize output to {}", nbytes);
+                        slice::from_raw_parts_mut(output.as_mut_ptr(), nbytes)
+                    };
+                    self.output_shm.read(&mut real_output[..nbytes]).unwrap();
+                }
+                frames
+            }
+            _ => {
+                debug!("Unexpected message {:?} during data_callback", r);
+                -1
+            }
+        }
+    }
+
+    fn state_callback(&mut self, state: cubeb::State) {
+        trace!("Stream state callback: {:?}", state);
+        let r = self.rpc.call(CallbackReq::State(state.into())).wait();
+        match r {
+            Ok(CallbackResp::State) => {}
+            _ => {
+                debug!("Unexpected message {:?} during callback", r);
+            }
+        }
+    }
+}
+
+struct ServerStream {
+    stream: ManuallyDrop<cubeb::Stream>,
+    cbs: ManuallyDrop<Box<ServerStreamCallbacks>>,
+}
+
+impl Drop for ServerStream {
+    fn drop(&mut self) {
+        unsafe {
+            ManuallyDrop::drop(&mut self.stream);
+            ManuallyDrop::drop(&mut self.cbs);
+        }
+    }
+}
+
+type StreamSlab = slab::Slab<ServerStream, usize>;
+
+pub struct CubebServer {
+    cb_remote: Remote,
+    streams: StreamSlab,
+}
+
+impl rpc::Server for CubebServer {
+    type Request = ServerMessage;
+    type Response = ClientMessage;
+    type Future = FutureResult<Self::Response, ()>;
+    type Transport = FramedWithFds<UnixStream, LengthDelimitedCodec<Self::Response, Self::Request>>;
+
+    fn process(&mut self, req: Self::Request) -> Self::Future {
+        let resp = with_local_context(|context| match *context {
+            Err(_) => error(cubeb::Error::error()),
+            Ok(ref context) => self.process_msg(context, &req),
+        });
+        future::ok(resp)
+    }
+}
+
+impl CubebServer {
+    pub fn new(cb_remote: Remote) -> Self {
+        CubebServer {
+            cb_remote: cb_remote,
+            streams: StreamSlab::with_capacity(STREAM_CONN_CHUNK_SIZE),
+        }
+    }
+
+    // Process a request coming from the client.
+    fn process_msg(&mut self, context: &cubeb::Context, msg: &ServerMessage) -> ClientMessage {
+        let resp: ClientMessage = match *msg {
+            ServerMessage::ClientConnect => panic!("already connected"),
+
+            ServerMessage::ClientDisconnect => {
+                // TODO:
+                //self.connection.client_disconnect();
+                ClientMessage::ClientDisconnected
+            }
+
+            ServerMessage::ContextGetBackendId => ClientMessage::ContextBackendId(),
+
+            ServerMessage::ContextGetMaxChannelCount => context
+                .max_channel_count()
+                .map(ClientMessage::ContextMaxChannelCount)
+                .unwrap_or_else(error),
+
+            ServerMessage::ContextGetMinLatency(ref params) => {
+                let format = cubeb::SampleFormat::from(params.format);
+                let layout = cubeb::ChannelLayout::from(params.layout);
+
+                let params = cubeb::StreamParamsBuilder::new()
+                    .format(format)
+                    .rate(u32::from(params.rate))
+                    .channels(u32::from(params.channels))
+                    .layout(layout)
+                    .take();
+
+                context
+                    .min_latency(&params)
+                    .map(ClientMessage::ContextMinLatency)
+                    .unwrap_or_else(error)
+            }
+
+            ServerMessage::ContextGetPreferredSampleRate => context
+                .preferred_sample_rate()
+                .map(ClientMessage::ContextPreferredSampleRate)
+                .unwrap_or_else(error),
+
+            ServerMessage::ContextGetDeviceEnumeration(device_type) => context
+                .enumerate_devices(cubeb::DeviceType::from_bits_truncate(device_type))
+                .map(|devices| {
+                    let v: Vec<DeviceInfo> = devices.iter().map(|i| i.as_ref().into()).collect();
+                    ClientMessage::ContextEnumeratedDevices(v)
+                }).unwrap_or_else(error),
+
+            ServerMessage::StreamInit(ref params) => self
+                .process_stream_init(context, params)
+                .unwrap_or_else(|_| error(cubeb::Error::error())),
+
+            ServerMessage::StreamDestroy(stm_tok) => {
+                self.streams.remove(stm_tok);
+                ClientMessage::StreamDestroyed
+            }
+
+            ServerMessage::StreamStart(stm_tok) => self.streams[stm_tok]
+                .stream
+                .start()
+                .map(|_| ClientMessage::StreamStarted)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamStop(stm_tok) => self.streams[stm_tok]
+                .stream
+                .stop()
+                .map(|_| ClientMessage::StreamStopped)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamResetDefaultDevice(stm_tok) => self.streams[stm_tok]
+                .stream
+                .reset_default_device()
+                .map(|_| ClientMessage::StreamDefaultDeviceReset)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamGetPosition(stm_tok) => self.streams[stm_tok]
+                .stream
+                .position()
+                .map(ClientMessage::StreamPosition)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamGetLatency(stm_tok) => self.streams[stm_tok]
+                .stream
+                .latency()
+                .map(ClientMessage::StreamLatency)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamSetVolume(stm_tok, volume) => self.streams[stm_tok]
+                .stream
+                .set_volume(volume)
+                .map(|_| ClientMessage::StreamVolumeSet)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamSetPanning(stm_tok, panning) => self.streams[stm_tok]
+                .stream
+                .set_panning(panning)
+                .map(|_| ClientMessage::StreamPanningSet)
+                .unwrap_or_else(error),
+
+            ServerMessage::StreamGetCurrentDevice(stm_tok) => self.streams[stm_tok]
+                .stream
+                .current_device()
+                .map(|device| ClientMessage::StreamCurrentDevice(Device::from(device)))
+                .unwrap_or_else(error),
+        };
+
+        trace!("process_msg: req={:?}, resp={:?}", msg, resp);
+
+        resp
+    }
+
+    // Stream init is special, so it's been separated from process_msg.
+    fn process_stream_init(
+        &mut self,
+        context: &cubeb::Context,
+        params: &StreamInitParams,
+    ) -> Result<ClientMessage> {
+        fn frame_size_in_bytes(params: Option<&StreamParams>) -> u16 {
+            params
+                .map(|p| {
+                    let format = p.format.into();
+                    let sample_size = match format {
+                        cubeb::SampleFormat::S16LE
+                        | cubeb::SampleFormat::S16BE
+                        | cubeb::SampleFormat::S16NE => 2,
+                        cubeb::SampleFormat::Float32LE
+                        | cubeb::SampleFormat::Float32BE
+                        | cubeb::SampleFormat::Float32NE => 4,
+                    };
+                    let channel_count = p.channels as u16;
+                    sample_size * channel_count
+                }).unwrap_or(0u16)
+        }
+
+        // Create the callback handling struct which is attached the cubeb stream.
+        let input_frame_size = frame_size_in_bytes(params.input_stream_params.as_ref());
+        let output_frame_size = frame_size_in_bytes(params.output_stream_params.as_ref());
+
+        let (stm1, stm2) = net::UnixStream::pair()?;
+        debug!("Created callback pair: {:?}-{:?}", stm1, stm2);
+        let (input_shm, input_file) =
+            SharedMemWriter::new(&audioipc::get_shm_path("input"), SHM_AREA_SIZE)?;
+        let (output_shm, output_file) =
+            SharedMemReader::new(&audioipc::get_shm_path("output"), SHM_AREA_SIZE)?;
+
+        // This code is currently running on the Client/Server RPC
+        // handling thread.  We need to move the registration of the
+        // bind_client to the callback RPC handling thread.  This is
+        // done by spawning a future on cb_remote.
+
+        let id = core::handle().id();
+
+        let (tx, rx) = oneshot::channel();
+        self.cb_remote.spawn(move |handle| {
+            // Ensure we're running on a loop different to the one
+            // invoking spawn_fn.
+            assert_ne!(id, handle.id());
+            let stream = UnixStream::from_stream(stm2, handle).unwrap();
+            let transport = framed(stream, Default::default());
+            let rpc = rpc::bind_client::<CallbackClient>(transport, handle);
+            drop(tx.send(rpc));
+            Ok(())
+        });
+
+        let rpc: rpc::ClientProxy<CallbackReq, CallbackResp> = match rx.wait() {
+            Ok(rpc) => rpc,
+            Err(_) => bail!("Failed to create callback rpc."),
+        };
+
+        let cbs = Box::new(ServerStreamCallbacks {
+            input_frame_size,
+            output_frame_size,
+            input_shm,
+            output_shm,
+            rpc,
+        });
+
+        // Create cubeb stream from params
+        let stream_name = params
+            .stream_name
+            .as_ref()
+            .and_then(|name| CStr::from_bytes_with_nul(name).ok());
+
+        let input_device = params.input_device as *const _;
+        let input_stream_params = params.input_stream_params.as_ref().map(|isp| unsafe {
+            cubeb::StreamParamsRef::from_ptr(isp as *const StreamParams as *mut _)
+        });
+
+        let output_device = params.output_device as *const _;
+        let output_stream_params = params.output_stream_params.as_ref().map(|osp| unsafe {
+            cubeb::StreamParamsRef::from_ptr(osp as *const StreamParams as *mut _)
+        });
+
+        let latency = params.latency_frames;
+        assert!(size_of::<Box<ServerStreamCallbacks>>() == size_of::<usize>());
+        let user_ptr = cbs.as_ref() as *const ServerStreamCallbacks as *mut c_void;
+
+        unsafe {
+            context
+                .stream_init(
+                    stream_name,
+                    input_device,
+                    input_stream_params,
+                    output_device,
+                    output_stream_params,
+                    latency,
+                    Some(data_cb_c),
+                    Some(state_cb_c),
+                    user_ptr,
+                ).and_then(|stream| {
+                    if !self.streams.has_available() {
+                        trace!(
+                            "server connection ran out of stream slots. reserving {} more.",
+                            STREAM_CONN_CHUNK_SIZE
+                        );
+                        self.streams.reserve_exact(STREAM_CONN_CHUNK_SIZE);
+                    }
+
+                    let stm_tok = match self.streams.vacant_entry() {
+                        Some(entry) => {
+                            debug!("Registering stream {:?}", entry.index(),);
+
+                            entry
+                                .insert(ServerStream {
+                                    stream: ManuallyDrop::new(stream),
+                                    cbs: ManuallyDrop::new(cbs),
+                                }).index()
+                        }
+                        None => {
+                            // TODO: Turn into error
+                            panic!("Failed to insert stream into slab. No entries")
+                        }
+                    };
+
+                    Ok(ClientMessage::StreamCreated(StreamCreate {
+                        token: stm_tok,
+                        fds: [
+                            stm1.into_raw_fd(),
+                            input_file.into_raw_fd(),
+                            output_file.into_raw_fd(),
+                        ],
+                    }))
+                }).map_err(|e| e.into())
+        }
+    }
+}
+
+// C callable callbacks
+unsafe extern "C" fn data_cb_c(
+    _: *mut ffi::cubeb_stream,
+    user_ptr: *mut c_void,
+    input_buffer: *const c_void,
+    output_buffer: *mut c_void,
+    nframes: c_long,
+) -> c_long {
+    let ok = panic::catch_unwind(|| {
+        let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
+        let input = if input_buffer.is_null() {
+            &[]
+        } else {
+            slice::from_raw_parts(input_buffer as *const u8, nframes as usize)
+        };
+        let output: &mut [u8] = if output_buffer.is_null() {
+            &mut []
+        } else {
+            slice::from_raw_parts_mut(output_buffer as *mut u8, nframes as usize)
+        };
+        cbs.data_callback(input, output) as c_long
+    });
+    ok.unwrap_or(0)
+}
+
+unsafe extern "C" fn state_cb_c(
+    _: *mut ffi::cubeb_stream,
+    user_ptr: *mut c_void,
+    state: ffi::cubeb_state,
+) {
+    let ok = panic::catch_unwind(|| {
+        let state = cubeb::State::from(state);
+        let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
+        cbs.state_callback(state);
+    });
+    ok.expect("State callback panicked");
+}
--- a/netwerk/base/LoadInfo.cpp
+++ b/netwerk/base/LoadInfo.cpp
@@ -1568,10 +1568,24 @@ LoadInfo::SetPerformanceStorage(Performa
 }
 
 PerformanceStorage*
 LoadInfo::GetPerformanceStorage()
 {
   return mPerformanceStorage;
 }
 
+NS_IMETHODIMP
+LoadInfo::GetCspEventListener(nsICSPEventListener** aCSPEventListener)
+{
+  NS_IF_ADDREF(*aCSPEventListener = mCSPEventListener);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadInfo::SetCspEventListener(nsICSPEventListener* aCSPEventListener)
+{
+  mCSPEventListener = aCSPEventListener;
+  return NS_OK;
+}
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/base/LoadInfo.h
+++ b/netwerk/base/LoadInfo.h
@@ -162,16 +162,17 @@ private:
   // it should be merged from parent channel through ParentLoadInfoForwarderArgs.
   nsCOMPtr<nsIPrincipal>           mLoadingPrincipal;
   nsCOMPtr<nsIPrincipal>           mTriggeringPrincipal;
   nsCOMPtr<nsIPrincipal>           mPrincipalToInherit;
   nsCOMPtr<nsIPrincipal>           mSandboxedLoadingPrincipal;
   nsCOMPtr<nsIPrincipal>           mTopLevelPrincipal;
   nsCOMPtr<nsIPrincipal>           mTopLevelStorageAreaPrincipal;
   nsCOMPtr<nsIURI>                 mResultPrincipalURI;
+  nsCOMPtr<nsICSPEventListener>    mCSPEventListener;
 
   Maybe<mozilla::dom::ClientInfo>               mClientInfo;
   UniquePtr<mozilla::dom::ClientSource>         mReservedClientSource;
   Maybe<mozilla::dom::ClientInfo>               mReservedClientInfo;
   Maybe<mozilla::dom::ClientInfo>               mInitialClientInfo;
   Maybe<mozilla::dom::ServiceWorkerDescriptor>  mController;
   RefPtr<mozilla::dom::PerformanceStorage>      mPerformanceStorage;
 
--- a/netwerk/base/nsILoadInfo.idl
+++ b/netwerk/base/nsILoadInfo.idl
@@ -3,16 +3,17 @@
  * 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/. */
 
 #include "nsISupports.idl"
 #include "nsIContentPolicy.idl"
 
 interface nsIChannel;
+interface nsICSPEventListener;
 interface nsINode;
 interface nsIPrincipal;
 interface nsIRedirectHistoryEntry;
 interface nsIURI;
 webidl Document;
 native LoadContextRef(already_AddRefed<nsISupports>);
 %{C++
 #include "nsTArray.h"
@@ -1067,9 +1068,16 @@ interface nsILoadInfo : nsISupports
     */
   [infallible] attribute boolean documentHasUserInteracted;
 
   /**
     * This attribute represents whether the document to which this
     * load belongs had finished loading when the load was initiated.
     */
   [infallible] attribute boolean documentHasLoaded;
+
+  /**
+    * The object in charged to receive CSP violation events. It can be null.
+    * This attribute will be merged into the CSP object eventually.
+    * See bug 1500908.
+    */
+  attribute nsICSPEventListener cspEventListener;
 };
--- 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/testing/web-platform/meta/2dcontext/imagebitmap/createImageBitmap-invalid-args.html.ini
+++ b/testing/web-platform/meta/2dcontext/imagebitmap/createImageBitmap-invalid-args.html.ini
@@ -1,139 +1,46 @@
 [createImageBitmap-invalid-args.html]
-  [createImageBitmap with a HTMLImageElement source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLImageElement source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLImageElement source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLVideoElement source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLVideoElement source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLVideoElement source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLCanvasElement source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLCanvasElement source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a HTMLCanvasElement source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
-    expected: FAIL
-
-  [createImageBitmap with a OffscreenCanvas source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a OffscreenCanvas source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a OffscreenCanvas source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
-    expected: FAIL
-
-  [createImageBitmap with a ImageData source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a ImageData source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a ImageData source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
-    expected: FAIL
-
-  [createImageBitmap with a ImageBitmap source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a ImageBitmap source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a ImageBitmap source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
-    expected: FAIL
-
-  [createImageBitmap with a Blob source and sw set to 0 rejects with a RangeError.]
-    expected: FAIL
-
-  [createImageBitmap with a Blob source and sh set to 0 rejects with a RangeError.]
-    expected: FAIL
-
   [createImageBitmap with an oversized canvas source rejects with a RangeError.]
     expected: FAIL
 
   [createImageBitmap with an invalid OffscreenCanvas source rejects with a RangeError.]
     expected: FAIL
 
   [createImageBitmap with a broken image source rejects with an InvalidStateError.]
     expected: FAIL
 
   [createImageBitmap with an available but undecodable image source rejects with an InvalidStateError.]
     expected: FAIL
 
-  [createImageBitmap with a an HTMLCanvasElement source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an HTMLCanvasElement source and sh set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an HTMLCanvasElement source and oversized (unallocatable) crop region]
-    expected: FAIL
-
-  [createImageBitmap with a an HTMLVideoElement source and sw set to 0]
+  [createImageBitmap with an HTMLCanvasElement source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a an HTMLVideoElement source and sh set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an HTMLVideoElement source and oversized (unallocatable) crop region]
+  [createImageBitmap with an HTMLVideoElement source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a an HTMLImageElement source and sw set to 0]
+  [createImageBitmap with an HTMLImageElement source and sw set to 0]
     expected: FAIL
 
-  [createImageBitmap with a an HTMLImageElement source and sh set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an HTMLImageElement source and oversized (unallocatable) crop region]
+  [createImageBitmap with an HTMLImageElement source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a an OffscreenCanvas source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an OffscreenCanvas source and sh set to 0]
+  [createImageBitmap with an OffscreenCanvas source and sw set to 0]
     expected: FAIL
 
-  [createImageBitmap with a an OffscreenCanvas source and oversized (unallocatable) crop region]
-    expected: FAIL
-
-  [createImageBitmap with a an ImageData source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an ImageData source and sh set to 0]
+  [createImageBitmap with an OffscreenCanvas source and sh set to 0]
     expected: FAIL
 
-  [createImageBitmap with a an ImageData source and oversized (unallocatable) crop region]
-    expected: FAIL
-
-  [createImageBitmap with a an ImageBitmap source and sw set to 0]
+  [createImageBitmap with an OffscreenCanvas source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a an ImageBitmap source and sh set to 0]
+  [createImageBitmap with an ImageData source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a an ImageBitmap source and oversized (unallocatable) crop region]
-    expected: FAIL
-
-  [createImageBitmap with a a Blob source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a a Blob source and sh set to 0]
+  [createImageBitmap with an ImageBitmap source and oversized (unallocatable) crop region]
     expected: FAIL
 
   [createImageBitmap with an oversized canvas source.]
     expected: FAIL
 
   [createImageBitmap with an invalid OffscreenCanvas source.]
     expected: FAIL
 
@@ -141,59 +48,41 @@
     expected: FAIL
 
   [createImageBitmap with an available but undecodable image source.]
     expected: FAIL
 
   [createImageBitmap with a closed ImageBitmap.]
     expected: FAIL
 
-  [createImageBitmap with a a bitmap HTMLImageElement source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a a bitmap HTMLImageElement source and sh set to 0]
+  [createImageBitmap with a bitmap HTMLImageElement source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a a bitmap HTMLImageElement source and oversized (unallocatable) crop region]
+  [createImageBitmap with a bitmap SVGImageElement source and sw set to 0]
     expected: FAIL
 
-  [createImageBitmap with a a vector HTMLImageElement source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a a vector HTMLImageElement source and sh set to 0]
+  [createImageBitmap with a bitmap SVGImageElement source and sh set to 0]
     expected: FAIL
 
-  [createImageBitmap with a a bitmap SVGImageElement source and sw set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a a bitmap SVGImageElement source and sh set to 0]
+  [createImageBitmap with a bitmap SVGImageElement source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a a bitmap SVGImageElement source and oversized (unallocatable) crop region]
+  [createImageBitmap with a vector SVGImageElement source and sw set to 0]
     expected: FAIL
 
-  [createImageBitmap with a a vector SVGImageElement source and sw set to 0]
+  [createImageBitmap with a vector SVGImageElement source and sh set to 0]
     expected: FAIL
 
-  [createImageBitmap with a a vector SVGImageElement source and sh set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a a vector SVGImageElement source and oversized (unallocatable) crop region]
+  [createImageBitmap with a vector SVGImageElement source and oversized (unallocatable) crop region]
     expected: FAIL
 
   [createImageBitmap with CanvasRenderingContext2D image source.]
     expected: FAIL
 
   [createImageBitmap with Uint8Array image source.]
     expected: FAIL
 
   [createImageBitmap with ArrayBuffer image source.]
     expected: FAIL
 
-  [createImageBitmap with a an HTMLVideoElement from a data URL source and sw set to 0]
+  [createImageBitmap with an HTMLVideoElement from a data URL source and oversized (unallocatable) crop region]
     expected: FAIL
 
-  [createImageBitmap with a an HTMLVideoElement from a data URL source and sh set to 0]
-    expected: FAIL
-
-  [createImageBitmap with a an HTMLVideoElement from a data URL source and oversized (unallocatable) crop region]
-    expected: FAIL
-
--- a/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-invalid-args.html
+++ b/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-invalid-args.html
@@ -44,38 +44,38 @@ function makeAvailableButBrokenImage(pat
     image.src = path;
     image.onload = () => resolve(image);
     image.onerror = reject;
   });
 }
 
 testCases = [
   {
-    description: 'createImageBitmap with a <sourceType> source and sw set to 0',
+    description: 'createImageBitmap with <sourceType> source and sw set to 0',
     promiseTestFunction:
       (source, t) => {
         return promise_rejects(t, new RangeError(),
             createImageBitmap(source, 0, 0, 0, 10));
       }
   },
   {
-    description: 'createImageBitmap with a <sourceType> source and sh set to 0',
+    description: 'createImageBitmap with <sourceType> source and sh set to 0',
     promiseTestFunction:
       (source, t) => {
         return promise_rejects(t, new RangeError(),
             createImageBitmap(source, 0, 0, 10, 0));
       }
   },
   {
     // This case is not explicitly documented in the specification for
     // createImageBitmap, but it is expected that internal failures cause
     // InvalidStateError.
     //
     // Note: https://bugs.chromium.org/p/chromium/issues/detail?id=799025
-    description: 'createImageBitmap with a <sourceType> source and oversized ' +
+    description: 'createImageBitmap with <sourceType> source and oversized ' +
         '(unallocatable) crop region',
     promiseTestFunction:
       (source, t) => {
         return promise_rejects(t, new DOMException('', 'InvalidStateError'),
             createImageBitmap(source, 0, 0, 100000000, 100000000));
       }
   },
 ];
--- a/toolkit/components/gfx/SanityTest.js
+++ b/toolkit/components/gfx/SanityTest.js
@@ -137,17 +137,21 @@ function testCompositor(test, win, ctx) 
   takeWindowSnapshot(win, ctx);
   var testPassed = true;
 
   if (!verifyLayersRendering(ctx)) {
     // Try disabling advanced layers if it was enabled. Also trigger
     // a device reset so the screen redraws.
     if (Services.prefs.getBoolPref(AL_ENABLED_PREF, false)) {
       Services.prefs.setBoolPref(AL_TEST_FAILED_PREF, true);
-      test.utils.triggerDeviceReset();
+      // Do not need to reset device when WebRender is used.
+      // When WebRender is used, advanced layers are not used.
+      if (test.utils.layerManagerType != "WebRender") {
+        test.utils.triggerDeviceReset();
+      }
     }
     reportResult(TEST_FAILED_RENDER);
     testPassed = false;
   } else {
     Services.prefs.setBoolPref(AL_TEST_FAILED_PREF, false);
     if (!verifyVideoRendering(ctx)) {
       reportResult(TEST_FAILED_VIDEO);
       Services.prefs.setBoolPref(DISABLE_VIDEO_PREF, true);
--- 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,23 @@ 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);
+        try {
+          label = await OSKeyStore.decrypt(this._encryptedNumber);
+        } catch (ex) {
+          // Quietly recover from decryption error.
+          label = this._number;
+          Cu.reportError(ex);
+        }
       } 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,42 @@
 /* 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.isLoggedIn = Object.getOwnPropertyDescriptor(OSKeyStore, "isLoggedIn").get;
+  OSKeyStore.__defineGetter__("isLoggedIn", () => gFakeLoggedIn);
+  registerCleanupFunction(async () => {
+    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.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}`);
 });