Bug 1426306 - Store only derived keys instead of storing kB/kA. r?markh, rfkelly, glasserc draft
authorEdouard Oger <eoger@fastmail.com>
Fri, 05 Jan 2018 15:56:00 -0500
changeset 722235 3218f2c91e3e20c046d7ddac4baa14835f88bfa4
parent 722181 0a543687fd36bc0dc4188c3d33d117b0a8174721
child 746575 f563640402c7f9bf795d9a6d5a699d991d8dab42
push id96106
push userbmo:eoger@fastmail.com
push dateThu, 18 Jan 2018 18:20:41 +0000
reviewersmarkh, rfkelly, glasserc
bugs1426306
milestone59.0a1
Bug 1426306 - Store only derived keys instead of storing kB/kA. r?markh, rfkelly, glasserc MozReview-Commit-ID: Hgv5hxSH4qX
browser/components/uitour/test/browser_fxa.js
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsCommon.js
services/fxaccounts/FxAccountsStorage.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
services/fxaccounts/tests/xpcshell/test_storage_manager.js
services/fxaccounts/tests/xpcshell/test_web_channel.js
services/sync/modules-testing/utils.js
services/sync/modules/browserid_identity.js
services/sync/modules/keys.js
services/sync/tests/unit/test_browserid_identity.js
services/sync/tests/unit/test_syncscheduler.js
toolkit/components/extensions/ExtensionStorageSync.jsm
toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
tools/lint/eslint/modules.json
--- a/browser/components/uitour/test/browser_fxa.js
+++ b/browser/components/uitour/test/browser_fxa.js
@@ -52,18 +52,20 @@ var tests = [
 // interfere with the tests.
 function setSignedInUser(data) {
   if (!data) {
     data = {
       email: "foo@example.com",
       uid: "1234@lcip.org",
       assertion: "foobar",
       sessionToken: "dead",
-      kA: "beef",
-      kB: "cafe",
+      kSync: "beef",
+      kXCS: "cafe",
+      kExtSync: "bacon",
+      kExtKbHash: "cheese",
       verified: true
     };
   }
  return fxAccounts.setSignedInUser(data);
 }
 
 function signOut() {
   // we always want a "localOnly" signout here...
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FxAccountsProfile.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource://services-sync/util.js");
 
 // All properties exposed by the public FxAccounts API.
 var publicProperties = [
   "accountStatus",
+  "canGetKeys",
   "checkVerificationStatus",
   "getAccountsClient",
   "getAssertion",
   "getDeviceId",
   "getDeviceList",
   "getKeys",
   "getOAuthToken",
   "getProfileCache",
@@ -514,18 +515,20 @@ FxAccountsInternal.prototype = {
    * Get the user currently signed in to Firefox Accounts.
    *
    * @return Promise
    *        The promise resolves to the credentials object of the signed-in user:
    *        {
    *          email: The user's email address
    *          uid: The user's unique id
    *          sessionToken: Session for the FxA server
-   *          kA: An encryption key from the FxA server
-   *          kB: An encryption key derived from the user's FxA password
+   *          kSync: An encryption key for Sync
+   *          kXCS: A key hash of kB for the X-Client-State header
+   *          kExtSync: An encryption key for WebExtensions syncing
+   *          kExtKbHash: A key hash of kB for WebExtensions syncing
    *          verified: email verification status
    *          authAt: The time (seconds since epoch) that this record was
    *                  authenticated
    *        }
    *        or null if no user is signed in.
    */
   getSignedInUser: function getSignedInUser() {
     let currentState = this.currentAccountState;
@@ -893,120 +896,209 @@ FxAccountsInternal.prototype = {
    *        the server.
    */
   async hasLocalSession() {
     let data = await this.getSignedInUser();
     return data && data.sessionToken;
   },
 
   /**
+   * Checks if we currently have encryption keys or if we have enough to
+   * be able to successfully fetch them for the signed-in-user.
+   */
+  async canGetKeys() {
+    let currentState = this.currentAccountState;
+    let userData = await currentState.getUserAccountData();
+    if (!userData) {
+      throw new Error("Can't possibly get keys; User is not signed in");
+    }
+    // - keyFetchToken means we can almost certainly grab them.
+    // - kSync, kXCS, kExtSync and kExtKbHash means we already have them.
+    // - kB is deprecated but |getKeys| will help us migrate to kSync and friends.
+    return userData && (userData.keyFetchToken ||
+                        DERIVED_KEYS_NAMES.every(k => userData[k]) ||
+                        userData.kB);
+  },
+
+  /**
    * Fetch encryption keys for the signed-in-user from the FxA API server.
    *
    * Not for user consumption.  Exists to cause the keys to be fetch.
    *
    * Returns user data so that it can be chained with other methods.
    *
    * @return Promise
    *        The promise resolves to the credentials object of the signed-in user:
    *        {
    *          email: The user's email address
    *          uid: The user's unique id
    *          sessionToken: Session for the FxA server
-   *          kA: An encryption key from the FxA server
-   *          kB: An encryption key derived from the user's FxA password
+   *          kSync: An encryption key for Sync
+   *          kXCS: A key hash of kB for the X-Client-State header
+   *          kExtSync: An encryption key for WebExtensions syncing
+   *          kExtKbHash: A key hash of kB for WebExtensions syncing
    *          verified: email verification status
    *        }
    *        or null if no user is signed in
    */
-  getKeys() {
+  async getKeys() {
     let currentState = this.currentAccountState;
-    return currentState.getUserAccountData().then((userData) => {
+    try {
+      let userData = await currentState.getUserAccountData();
       if (!userData) {
         throw new Error("Can't get keys; User is not signed in");
       }
-      if (userData.kA && userData.kB) {
-        return userData;
+      if (userData.kB) { // Bug 1426306 - Migrate from kB to derived keys.
+        log.info("Migrating kB to derived keys.");
+        const {uid, kB} = userData;
+        await this.updateUserAccountData({
+          uid,
+          ...this._deriveKeys(uid, CommonUtils.hexToBytes(kB)),
+          kA: null, // Remove kA and kB from storage.
+          kB: null
+        });
+        userData = await this.getUserAccountData();
+      }
+      if (DERIVED_KEYS_NAMES.every(k => userData[k])) {
+        return currentState.resolve(userData);
       }
       if (!currentState.whenKeysReadyDeferred) {
         currentState.whenKeysReadyDeferred = PromiseUtils.defer();
         if (userData.keyFetchToken) {
           this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
             (dataWithKeys) => {
-              if (!dataWithKeys.kA || !dataWithKeys.kB) {
+              if (DERIVED_KEYS_NAMES.some(k => !userData[k])) {
+                const missing = DERIVED_KEYS_NAMES.filter(k => !userData[k]);
                 currentState.whenKeysReadyDeferred.reject(
-                  new Error("user data missing kA or kB")
+                  new Error(`user data missing: ${missing.join(", ")}`)
                 );
                 return;
               }
               currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
             },
             (err) => {
               currentState.whenKeysReadyDeferred.reject(err);
             }
           );
         } else {
           currentState.whenKeysReadyDeferred.reject("No keyFetchToken");
         }
       }
-      return currentState.whenKeysReadyDeferred.promise;
-    }).catch(err =>
-      this._handleTokenError(err)
-    ).then(result => currentState.resolve(result));
-   },
+      return await currentState.resolve(currentState.whenKeysReadyDeferred.promise);
+    } catch (err) {
+      return currentState.resolve(this._handleTokenError(err));
+    }
+  },
 
   async fetchAndUnwrapKeys(keyFetchToken) {
     if (logPII) {
       log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
     }
     let currentState = this.currentAccountState;
     // Sign out if we don't have a key fetch token.
     if (!keyFetchToken) {
       log.warn("improper fetchAndUnwrapKeys() call: token missing");
       await this.signOut();
       return currentState.resolve(null);
     }
 
-    let {kA, wrapKB} = await this.fetchKeys(keyFetchToken);
+    let {wrapKB} = await this.fetchKeys(keyFetchToken);
 
     let data = await currentState.getUserAccountData();
 
     // Sanity check that the user hasn't changed out from under us
     if (data.keyFetchToken !== keyFetchToken) {
       throw new Error("Signed in user changed while fetching keys!");
     }
 
     // Next statements must be synchronous until we setUserAccountData
     // so that we don't risk getting into a weird state.
-    let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
-                                 wrapKB);
+    let kBbytes = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
+                                  wrapKB);
 
     if (logPII) {
-      log.debug("kB_hex: " + kB_hex);
+      log.debug("kBbytes: " + kBbytes);
     }
     let updateData = {
-      kA: CommonUtils.bytesAsHex(kA),
-      kB: CommonUtils.bytesAsHex(kB_hex),
+      ...this._deriveKeys(data.uid, kBbytes),
       keyFetchToken: null, // null values cause the item to be removed.
       unwrapBKey: null,
     };
 
-    log.debug("Keys Obtained: kA=" + !!updateData.kA + ", kB=" + !!updateData.kB);
+    log.debug("Keys Obtained:" +
+              DERIVED_KEYS_NAMES.map(k => `${k}=${!!updateData[k]}`).join(", "));
     if (logPII) {
-      log.debug("Keys Obtained: kA=" + updateData.kA + ", kB=" + updateData.kB);
+      log.debug("Keys Obtained:" +
+                DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", "));
     }
 
     await currentState.updateUserAccountData(updateData);
     // We are now ready for business. This should only be invoked once
     // per setSignedInUser(), regardless of whether we've rebooted since
     // setSignedInUser() was called.
     await this.notifyObservers(ONVERIFIED_NOTIFICATION);
     data = await currentState.getUserAccountData();
     return currentState.resolve(data);
   },
 
+  _deriveKeys(uid, kBbytes) {
+    return {
+      kSync: CommonUtils.bytesAsHex(this._deriveSyncKey(kBbytes)),
+      kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
+      kExtSync: CommonUtils.bytesAsHex(this._deriveWebExtSyncStoreKey(kBbytes)),
+      kExtKbHash: CommonUtils.bytesAsHex(this._deriveWebExtKbHash(uid, kBbytes)),
+    };
+  },
+
+  /**
+   * Derive the Sync Key given the byte string kB.
+   *
+   * @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)
+   */
+  _deriveSyncKey(kBbytes) {
+    return CryptoUtils.hkdf(kBbytes, undefined,
+                            "identity.mozilla.com/picl/v1/oldsync", 2 * 32);
+  },
+
+  /**
+   * Derive the WebExtensions Sync Storage Key given the byte string kB.
+   *
+   * @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)
+   */
+  _deriveWebExtSyncStoreKey(kBbytes) {
+    return CryptoUtils.hkdf(kBbytes, undefined,
+                            "identity.mozilla.com/picl/v1/chrome.storage.sync",
+                            2 * 32);
+  },
+
+  /**
+   * Derive the WebExtensions kbHash given the byte string kB.
+   *
+   * @returns SHA256(uid + kB)
+   */
+  _deriveWebExtKbHash(uid, kBbytes) {
+    return this._sha256(uid + kBbytes);
+  },
+
+  /**
+   * Derive the X-Client-State header given the byte string kB.
+   *
+   * @returns SHA256(kB)[:16]
+   */
+  _deriveXClientState(kBbytes) {
+    return this._sha256(kBbytes).slice(0, 16);
+  },
+
+  _sha256(bytes) {
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                    .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    return CryptoUtils.digestBytes(bytes, hasher);
+  },
+
   async getAssertionFromCert(data, keyPair, cert, audience) {
     log.debug("getAssertionFromCert");
     let options = {
       duration: ASSERTION_LIFETIME,
       localtimeOffsetMsec: this.localtimeOffsetMsec,
       now: this.now()
     };
     let currentState = this.currentAccountState;
@@ -1155,18 +1247,18 @@ FxAccountsInternal.prototype = {
     if (logPII) {
       log.debug("startVerifiedCheck with user data", data);
     }
 
     // Get us to the verified state, then get the keys. This returns a promise
     // that will fire when we are completely ready.
     //
     // Login is truly complete once keys have been fetched, so once getKeys()
-    // obtains and stores kA and kB, it will fire the onverified observer
-    // notification.
+    // obtains and stores kSync kXCS kExtSync and kExtKbHash, it will fire the
+    // onverified observer notification.
 
     // The callers of startVerifiedCheck never consume a returned promise (ie,
     // this is simply kicking off a background fetch) so we must add a rejection
     // handler to avoid runtime warnings about the rejection not being handled.
     this.whenVerified(data).then(
       () => this.getKeys(),
       err => log.info("startVerifiedCheck promise was rejected: " + err)
     );
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -200,30 +200,32 @@ exports.ERROR_INVALID_CONTENT_TYPE      
 exports.ERROR_NO_ACCOUNT                     = "NO_ACCOUNT";
 exports.ERROR_AUTH_ERROR                     = "AUTH_ERROR";
 exports.ERROR_INVALID_PARAMETER              = "INVALID_PARAMETER";
 
 // Status code errors
 exports.ERROR_CODE_METHOD_NOT_ALLOWED        = 405;
 exports.ERROR_MSG_METHOD_NOT_ALLOWED         = "METHOD_NOT_ALLOWED";
 
+exports.DERIVED_KEYS_NAMES = ["kSync", "kXCS", "kExtSync", "kExtKbHash"];
+
 // FxAccounts has the ability to "split" the credentials between a plain-text
 // JSON file in the profile dir and in the login manager.
 // In order to prevent new fields accidentally ending up in the "wrong" place,
 // all fields stored are listed here.
 
 // The fields we save in the plaintext JSON.
 // See bug 1013064 comments 23-25 for why the sessionToken is "safe"
 exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set(
   ["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile",
   "deviceId", "deviceRegistrationVersion", "profileCache"]);
 
 // Fields we store in secure storage if it exists.
 exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
-  ["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
+  [...exports.DERIVED_KEYS_NAMES, "keyFetchToken", "unwrapBKey", "assertion"]);
 
 // Fields we keep in memory and don't persist anywhere.
 exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
   ["cert", "keyPair"]);
 
 // A whitelist of fields that remain in storage when the user needs to
 // reauthenticate. All other fields will be removed.
 exports.FXA_PWDMGR_REAUTH_WHITELIST = new Set(
--- a/services/fxaccounts/FxAccountsStorage.jsm
+++ b/services/fxaccounts/FxAccountsStorage.jsm
@@ -209,31 +209,28 @@ this.FxAccountsStorageManager.prototype 
       throw new Error("No user is logged in");
     }
     if (!newFields || "uid" in newFields) {
       throw new Error("Can't change uid");
     }
     log.debug("_updateAccountData with items", Object.keys(newFields));
     // work out what bucket.
     for (let [name, value] of Object.entries(newFields)) {
-      if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
-        if (value == null) {
-          delete this.cachedMemory[name];
-        } else {
-          this.cachedMemory[name] = value;
-        }
+      if (value == null) {
+        delete this.cachedMemory[name];
+        delete this.cachedPlain[name];
+        // no need to do the "delete on null" thing for this.cachedSecure -
+        // we need to keep it until we have managed to read so we can nuke
+        // it on write.
+        this.cachedSecure[name] = null;
+      } else if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
+        this.cachedMemory[name] = value;
       } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
-        if (value == null) {
-          delete this.cachedPlain[name];
-        } else {
-          this.cachedPlain[name] = value;
-        }
+        this.cachedPlain[name] = value;
       } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
-        // don't do the "delete on null" thing here - we need to keep it until
-        // we have managed to read so we can nuke it on write.
         this.cachedSecure[name] = value;
       } else {
         // Throwing seems reasonable here as some client code has explicitly
         // specified the field name, so it's either confused or needs to update
         // how this field is to be treated.
         throw new Error("unexpected field '" + name + "'");
       }
     }
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -213,40 +213,48 @@ add_task(async function test_non_https_r
 add_task(async function test_get_signed_in_user_initially_unset() {
   _("Check getSignedInUser initially and after signout reports no user");
   let account = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
+    kSync: "beef",
+    kXCS: "cafe",
+    kExtSync: "bacon",
+    kExtKbHash: "cheese",
     verified: true
   };
   let result = await account.getSignedInUser();
   Assert.equal(result, null);
 
   await account.setSignedInUser(credentials);
   let histogram = Services.telemetry.getHistogramById("FXA_CONFIGURED");
   Assert.equal(histogram.snapshot().sum, 1);
   histogram.clear();
 
   result = await account.getSignedInUser();
   Assert.equal(result.email, credentials.email);
   Assert.equal(result.assertion, credentials.assertion);
-  Assert.equal(result.kB, credentials.kB);
+  Assert.equal(result.kSync, credentials.kSync);
+  Assert.equal(result.kXCS, credentials.kXCS);
+  Assert.equal(result.kExtSync, credentials.kExtSync);
+  Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
 
   // Delete the memory cache and force the user
   // to be read and parsed from storage (e.g. disk via JSONStorage).
   delete account.internal.signedInUser;
   result = await account.getSignedInUser();
   Assert.equal(result.email, credentials.email);
   Assert.equal(result.assertion, credentials.assertion);
-  Assert.equal(result.kB, credentials.kB);
+  Assert.equal(result.kSync, credentials.kSync);
+  Assert.equal(result.kXCS, credentials.kXCS);
+  Assert.equal(result.kExtSync, credentials.kExtSync);
+  Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
 
   // sign out
   let localOnly = true;
   await account.signOut(localOnly);
 
   // user should be undefined after sign out
   result = await account.getSignedInUser();
   Assert.equal(result, null);
@@ -256,18 +264,20 @@ add_task(async function test_set_signed_
   _("Check setSignedInUser tries to delete a previous registered device");
   let account = MakeFxAccounts();
   let deleteDeviceRegistrationCalled = false;
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
+    kSync: "beef",
+    kXCS: "cafe",
+    kExtSync: "bacon",
+    kExtKbHash: "cheese",
     verified: true
   };
   await account.setSignedInUser(credentials);
 
   account.internal.deleteDeviceRegistration = () => {
     deleteDeviceRegistrationCalled = true;
     return Promise.resolve(true);
   };
@@ -279,18 +289,20 @@ add_task(async function test_set_signed_
 add_task(async function test_update_account_data() {
   _("Check updateUserAccountData does the right thing.");
   let account = MakeFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
+    kSync: "beef",
+    kXCS: "cafe",
+    kExtSync: "bacon",
+    kExtKbHash: "cheese",
     verified: true
   };
   await account.setSignedInUser(credentials);
 
   let newCreds = {
     email: credentials.email,
     uid: credentials.uid,
     assertion: "new_assertion",
@@ -638,38 +650,64 @@ add_test(function test_getKeys() {
   let user = getTestUser("eusebius");
 
   // Once email has been verified, we will be able to get keys
   user.verified = true;
 
   fxa.setSignedInUser(user).then(() => {
     fxa.getSignedInUser().then((user2) => {
       // Before getKeys, we have no keys
-      Assert.equal(!!user2.kA, false);
-      Assert.equal(!!user2.kB, false);
+      Assert.equal(!!user2.kSync, false);
+      Assert.equal(!!user2.kXCS, false);
+      Assert.equal(!!user2.kExtSync, false);
+      Assert.equal(!!user2.kExtKbHash, false);
       // And we still have a key-fetch token and unwrapBKey to use
       Assert.equal(!!user2.keyFetchToken, true);
       Assert.equal(!!user2.unwrapBKey, true);
 
       fxa.internal.getKeys().then(() => {
         fxa.getSignedInUser().then((user3) => {
           // Now we should have keys
           Assert.equal(fxa.internal.isUserEmailVerified(user3), true);
           Assert.equal(!!user3.verified, true);
-          Assert.equal(user3.kA, expandHex("11"));
-          Assert.equal(user3.kB, expandHex("66"));
+          Assert.notEqual(null, user3.kSync);
+          Assert.notEqual(null, user3.kXCS);
+          Assert.notEqual(null, user3.kExtSync);
+          Assert.notEqual(null, user3.kExtKbHash);
           Assert.equal(user3.keyFetchToken, undefined);
           Assert.equal(user3.unwrapBKey, undefined);
           run_next_test();
         });
       });
     });
   });
 });
 
+add_task(async function test_getKeys_kb_migration() {
+  let fxa = new MockFxAccounts();
+  let user = getTestUser("eusebius");
+
+  user.verified = true;
+  // Set-up the deprecated set of keys.
+  user.kA = "e0245ab7f10e483470388e0a28f0a03379a3b417174fb2b42feab158b4ac2dbd";
+  user.kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9";
+
+  await fxa.setSignedInUser(user);
+  await fxa.internal.getKeys();
+  let newUser = await fxa.getSignedInUser();
+  Assert.equal(newUser.kA, null);
+  Assert.equal(newUser.kB, null);
+  Assert.equal(newUser.kSync, "0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4" +
+                              "dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e");
+  Assert.equal(newUser.kXCS, "22a42fe289dced5715135913424cb23b");
+  Assert.equal(newUser.kExtSync, "baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd882" +
+                                   "65cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9");
+  Assert.equal(newUser.kExtKbHash, "25ed0ab3ae2f1e5365d923c9402d4255770dbe6ce79b09ed49f516985c0aa0c1");
+});
+
 add_task(async function test_getKeys_nonexistent_account() {
   let fxa = new MockFxAccounts();
   let bismarck = getTestUser("bismarck");
 
   let client = fxa.internal.fxAccountsClient;
   client.accountStatus = () => Promise.resolve(false);
   client.accountKeys = () => {
     return Promise.reject({
@@ -796,18 +834,20 @@ add_test(function test_overlapping_signi
 add_task(async function test_getAssertion_invalid_token() {
   let fxa = new MockFxAccounts();
 
   let client = fxa.internal.fxAccountsClient;
   client.accountStatus = () => Promise.resolve(true);
 
   let creds = {
     sessionToken: "sessionToken",
-    kA: expandHex("11"),
-    kB: expandHex("66"),
+    kSync: expandHex("11"),
+    kXCS: expandHex("66"),
+    kExtSync: expandHex("88"),
+    kExtKbHash: expandHex("22"),
     verified: true,
     email: "sonia@example.com",
   };
   await fxa.setSignedInUser(creds);
   // we have what we still believe to be a valid session token, so we should
   // consider that we have a local session.
   Assert.ok(await fxa.hasLocalSession());
 
@@ -834,21 +874,23 @@ add_task(async function test_getAssertio
   let fxa = new MockFxAccounts();
 
   do_check_throws(async function() {
     await fxa.getAssertion("nonaudience");
   });
 
   let creds = {
     sessionToken: "sessionToken",
-    kA: expandHex("11"),
-    kB: expandHex("66"),
+    kSync: expandHex("11"),
+    kXCS: expandHex("66"),
+    kExtSync: expandHex("88"),
+    kExtKbHash: expandHex("22"),
     verified: true
   };
-  // By putting kA/kB/verified in "creds", we skip ahead
+  // By putting kSync/kXCS/kExtSync/kExtKbHash/verified in "creds", we skip ahead
   // to the "we're ready" stage.
   await fxa.setSignedInUser(creds);
 
   _("== ready to go\n");
   // Start with a nice arbitrary but realistic date.  Here we use a nice RFC
   // 1123 date string like we would get from an HTTP header. Over the course of
   // the test, we will update 'now', but leave 'start' where it is.
   let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
@@ -1546,16 +1588,33 @@ add_task(async function test_checkVerifi
 
   await fxa.checkVerificationStatus();
 
   user = await fxa.internal.getUserAccountData();
   Assert.equal(user.email, alice.email);
   Assert.equal(user.sessionToken, null);
 });
 
+add_test(function test_deriveKeys() {
+  let account = MakeFxAccounts();
+  let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
+  let kB = CommonUtils.hexToBytes(kBhex);
+  const uid = "1ad7f502-4cc7-4ec1-a209-071fd2fae348";
+
+  const {kSync, kXCS, kExtSync, kExtKbHash} = account.internal._deriveKeys(uid, kB);
+
+  Assert.equal(kSync, "ad501a50561be52b008878b2e0d8a73357778a712255f7722f497b5d4df14b05" +
+                      "dc06afb836e1521e882f521eb34691d172337accdbf6e2a5b968b05a7bbb9885");
+  Assert.equal(kXCS, "6ae94683571c7a7c54dab4700aa3995f");
+  Assert.equal(kExtSync, "f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" +
+                         "5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395");
+  Assert.equal(kExtKbHash, "6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273");
+  run_next_test();
+});
+
 /*
  * End of tests.
  * Utility functions follow.
  */
 
 function expandHex(two_hex) {
   // Return a 64-character hex string, encoding 32 identical bytes.
   let eight_hex = two_hex + two_hex + two_hex + two_hex;
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -58,58 +58,66 @@ function createFxAccounts() {
 
 add_task(async function test_simple() {
   let fxa = createFxAccounts();
 
   let creds = {
     uid: "abcd",
     email: "test@example.com",
     sessionToken: "sessionToken",
-    kA: "the kA value",
-    kB: "the kB value",
+    kSync: "the kSync value",
+    kXCS: "the kXCS value",
+    kExtSync: "the kExtSync value",
+    kExtKbHash: "the kExtKbHash value",
     verified: true
   };
   await fxa.setSignedInUser(creds);
 
   // This should have stored stuff in both the .json file in the profile
   // dir, and the login dir.
   let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
   let data = await CommonUtils.readJSON(path);
 
   Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
   Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
   Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
 
-  Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
-  Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
+  Assert.ok(!("kSync" in data.accountData), "kSync not stored in clear text");
+  Assert.ok(!("kXCS" in data.accountData), "kXCS not stored in clear text");
+  Assert.ok(!("kExtSync" in data.accountData), "kExtSync not stored in clear text");
+  Assert.ok(!("kExtKbHash" in data.accountData), "kExtKbHash not stored in clear text");
 
   let login = getLoginMgrData();
   Assert.strictEqual(login.username, creds.uid, "uid used for username");
   let loginData = JSON.parse(login.password);
   Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
-  Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
-  Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
+  Assert.strictEqual(loginData.accountData.kSync, creds.kSync, "correct kSync in the login mgr");
+  Assert.strictEqual(loginData.accountData.kXCS, creds.kXCS, "correct kXCS in the login mgr");
+  Assert.strictEqual(loginData.accountData.kExtSync, creds.kExtSync, "correct kExtSync in the login mgr");
+  Assert.strictEqual(loginData.accountData.kExtKbHash, creds.kExtKbHash, "correct kExtKbHash in the login mgr");
 
   Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
   Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
   Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
 
   await fxa.signOut(/* localOnly = */ true);
   Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
 });
 
 add_task(async function test_MPLocked() {
   let fxa = createFxAccounts();
 
   let creds = {
     uid: "abcd",
     email: "test@example.com",
     sessionToken: "sessionToken",
-    kA: "the kA value",
-    kB: "the kB value",
+    kSync: "the kSync value",
+    kXCS: "the kXCS value",
+    kExtSync: "the kExtSync value",
+    kExtKbHash: "the kExtKbHash value",
     verified: true
   };
 
   Assert.strictEqual(getLoginMgrData(), null, "no login mgr at the start");
   // tell the storage that the MP is locked.
   setLoginMgrLoggedInState(false);
   await fxa.setSignedInUser(creds);
 
@@ -117,84 +125,90 @@ add_task(async function test_MPLocked() 
   // will not exist.
   let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
   let data = await CommonUtils.readJSON(path);
 
   Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
   Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
   Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
 
-  Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
-  Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
+  Assert.ok(!("kSync" in data.accountData), "kSync not stored in clear text");
+  Assert.ok(!("kXCS" in data.accountData), "kXCS not stored in clear text");
+  Assert.ok(!("kExtSync" in data.accountData), "kExtSync not stored in clear text");
+  Assert.ok(!("kExtKbHash" in data.accountData), "kExtKbHash not stored in clear text");
 
   Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist");
   await fxa.signOut(/* localOnly = */ true);
 });
 
 
 add_task(async function test_consistentWithMPEdgeCases() {
   setLoginMgrLoggedInState(true);
 
   let fxa = createFxAccounts();
 
   let creds1 = {
     uid: "uid1",
     email: "test@example.com",
     sessionToken: "sessionToken",
-    kA: "the kA value",
-    kB: "the kB value",
+    kSync: "the kSync value",
+    kXCS: "the kXCS value",
+    kExtSync: "the kExtSync value",
+    kExtKbHash: "the kExtKbHash value",
     verified: true
   };
 
   let creds2 = {
     uid: "uid2",
     email: "test2@example.com",
     sessionToken: "sessionToken2",
-    kA: "the kA value2",
-    kB: "the kB value2",
+    kSync: "the kSync value2",
+    kXCS: "the kXCS value2",
+    kExtSync: "the kExtSync value2",
+    kExtKbHash: "the kExtKbHash value2",
     verified: false,
   };
 
   // Log a user in while MP is unlocked.
   await fxa.setSignedInUser(creds1);
 
   // tell the storage that the MP is locked - this will prevent logout from
   // being able to clear the data.
   setLoginMgrLoggedInState(false);
 
   // now set the second credentials.
   await fxa.setSignedInUser(creds2);
 
   // We should still have creds1 data in the login manager.
   let login = getLoginMgrData();
   Assert.strictEqual(login.username, creds1.uid);
-  // and that we do have the first kA in the login manager.
-  Assert.strictEqual(JSON.parse(login.password).accountData.kA, creds1.kA,
+  // and that we do have the first kSync in the login manager.
+  Assert.strictEqual(JSON.parse(login.password).accountData.kSync, creds1.kSync,
                      "stale data still in login mgr");
 
   // Make a new FxA instance (otherwise the values in memory will be used)
   // and we want the login manager to be unlocked.
   setLoginMgrLoggedInState(true);
   fxa = createFxAccounts();
 
   let accountData = await fxa.getSignedInUser();
   Assert.strictEqual(accountData.email, creds2.email);
-  // we should have no kA at all.
-  Assert.strictEqual(accountData.kA, undefined, "stale kA wasn't used");
+  // we should have no kSync at all.
+  Assert.strictEqual(accountData.kSync, undefined, "stale kSync wasn't used");
   await fxa.signOut(/* localOnly = */ true);
 });
 
 // A test for the fact we will accept either a UID or email when looking in
 // the login manager.
 add_task(async function test_uidMigration() {
   setLoginMgrLoggedInState(true);
   Assert.strictEqual(getLoginMgrData(), null, "expect no logins at the start");
 
   // create the login entry using email as a key.
-  let contents = {kA: "kA"};
+  let contents = {kSync: "kSync"};
 
   let loginInfo = new Components.Constructor(
    "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
   let login = new loginInfo(FXA_PWDMGR_HOST,
                             null, // aFormSubmitURL,
                             FXA_PWDMGR_REALM, // aHttpRealm,
                             "foo@bar.com", // aUsername
                             JSON.stringify(contents), // aPassword
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
@@ -106,18 +106,20 @@ function MockFxAccounts(device = {}) {
 
 async function createMockFxA() {
   let fxa = new MockFxAccounts();
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
+    kSync: "beef",
+    kXCS: "cafe",
+    kExtSync: "bacon",
+    kExtKbHash: "cheese",
     verified: true
   };
   await fxa.setSignedInUser(credentials);
   return fxa;
 }
 
 // The tests.
 
--- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
@@ -110,18 +110,20 @@ function MockFxAccounts(mockGrantClient)
 
 async function createMockFxA(mockGrantClient) {
   let fxa = new MockFxAccounts(mockGrantClient);
   let credentials = {
     email: "foo@example.com",
     uid: "1234@lcip.org",
     assertion: "foobar",
     sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
+    kSync: "beef",
+    kXCS: "cafe",
+    kExtSync: "bacon",
+    kExtKbHash: "cheese",
     verified: true
   };
 
   await fxa.setSignedInUser(credentials);
   return fxa;
 }
 
 // The tests.
--- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js
+++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js
@@ -87,48 +87,48 @@ function add_storage_task(testFunction) 
 
 // initialized without account data and there's nothing to read. Not logged in.
 add_storage_task(async function checkInitializedEmpty(sm) {
   if (sm.secureStorage) {
     sm.secureStorage = new MockedSecureStorage(null);
   }
   await sm.initialize();
   Assert.strictEqual((await sm.getAccountData()), null);
-  Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in");
+  Assert.rejects(sm.updateAccountData({kXCS: "kXCS"}), "No user is logged in");
 });
 
 // Initialized with account data (ie, simulating a new user being logged in).
 // Should reflect the initial data and be written to storage.
 add_storage_task(async function checkNewUser(sm) {
   let initialAccountData = {
     uid: "uid",
     email: "someone@somewhere.com",
-    kA: "kA",
+    kXCS: "kXCS",
     deviceId: "device id"
   };
   sm.plainStorage = new MockedPlainStorage();
   if (sm.secureStorage) {
     sm.secureStorage = new MockedSecureStorage(null);
   }
   await sm.initialize(initialAccountData);
   let accountData = await sm.getAccountData();
   Assert.equal(accountData.uid, initialAccountData.uid);
   Assert.equal(accountData.email, initialAccountData.email);
-  Assert.equal(accountData.kA, initialAccountData.kA);
+  Assert.equal(accountData.kXCS, initialAccountData.kXCS);
   Assert.equal(accountData.deviceId, initialAccountData.deviceId);
 
   // and it should have been written to storage.
   Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid);
   Assert.equal(sm.plainStorage.data.accountData.email, initialAccountData.email);
   Assert.equal(sm.plainStorage.data.accountData.deviceId, initialAccountData.deviceId);
   // check secure
   if (sm.secureStorage) {
-    Assert.equal(sm.secureStorage.data.accountData.kA, initialAccountData.kA);
+    Assert.equal(sm.secureStorage.data.accountData.kXCS, initialAccountData.kXCS);
   } else {
-    Assert.equal(sm.plainStorage.data.accountData.kA, initialAccountData.kA);
+    Assert.equal(sm.plainStorage.data.accountData.kXCS, initialAccountData.kXCS);
   }
 });
 
 // Initialized without account data but storage has it available.
 add_storage_task(async function checkEverythingRead(sm) {
   sm.plainStorage = new MockedPlainStorage({
     uid: "uid",
     email: "someone@somewhere.com",
@@ -144,73 +144,103 @@ add_storage_task(async function checkEve
   Assert.equal(accountData.uid, "uid");
   Assert.equal(accountData.email, "someone@somewhere.com");
   Assert.equal(accountData.deviceId, "wibble");
   Assert.equal(accountData.deviceRegistrationVersion, null);
   // Update the data - we should be able to fetch it back and it should appear
   // in our storage.
   await sm.updateAccountData({
     verified: true,
-    kA: "kA",
-    kB: "kB",
+    kSync: "kSync",
+    kXCS: "kXCS",
+    kExtSync: "kExtSync",
+    kExtKbHash: "kExtKbHash",
     deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION
   });
   accountData = await sm.getAccountData();
-  Assert.equal(accountData.kB, "kB");
-  Assert.equal(accountData.kA, "kA");
+  Assert.equal(accountData.kSync, "kSync");
+  Assert.equal(accountData.kXCS, "kXCS");
+  Assert.equal(accountData.kExtSync, "kExtSync");
+  Assert.equal(accountData.kExtKbHash, "kExtKbHash");
   Assert.equal(accountData.deviceId, "wibble");
   Assert.equal(accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
   // Check the new value was written to storage.
   await sm._promiseStorageComplete; // storage is written in the background.
   // "verified", "deviceId" and "deviceRegistrationVersion" are plain-text fields.
   Assert.equal(sm.plainStorage.data.accountData.verified, true);
   Assert.equal(sm.plainStorage.data.accountData.deviceId, "wibble");
   Assert.equal(sm.plainStorage.data.accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
-  // "kA" and "foo" are secure
+  // derive keys are secure
   if (sm.secureStorage) {
-    Assert.equal(sm.secureStorage.data.accountData.kA, "kA");
-    Assert.equal(sm.secureStorage.data.accountData.kB, "kB");
+    Assert.equal(sm.secureStorage.data.accountData.kExtKbHash, "kExtKbHash");
+    Assert.equal(sm.secureStorage.data.accountData.kExtSync, "kExtSync");
+    Assert.equal(sm.secureStorage.data.accountData.kXCS, "kXCS");
+    Assert.equal(sm.secureStorage.data.accountData.kSync, "kSync");
   } else {
-    Assert.equal(sm.plainStorage.data.accountData.kA, "kA");
-    Assert.equal(sm.plainStorage.data.accountData.kB, "kB");
+    Assert.equal(sm.plainStorage.data.accountData.kExtKbHash, "kExtKbHash");
+    Assert.equal(sm.plainStorage.data.accountData.kExtSync, "kExtSync");
+    Assert.equal(sm.plainStorage.data.accountData.kXCS, "kXCS");
+    Assert.equal(sm.plainStorage.data.accountData.kSync, "kSync");
   }
 });
 
 add_storage_task(function checkInvalidUpdates(sm) {
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
   if (sm.secureStorage) {
     sm.secureStorage = new MockedSecureStorage(null);
   }
   Assert.rejects(sm.updateAccountData({uid: "another"}), "Can't change");
   Assert.rejects(sm.updateAccountData({email: "someoneelse"}), "Can't change");
 });
 
 add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) {
   if (sm.secureStorage) {
     sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-    sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
+    sm.secureStorage = new MockedSecureStorage({kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+                                                kExtKbHash: "kExtKbHash"});
   } else {
     sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
-                                              kA: "kA", kB: "kB"});
+                                              kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+                                              kExtKbHash: "kExtKbHash"});
   }
   await sm.initialize();
 
-  await sm.updateAccountData({kA: null});
+  await sm.updateAccountData({kXCS: null});
+  let accountData = await sm.getAccountData();
+  Assert.ok(!accountData.kXCS);
+  Assert.equal(accountData.kSync, "kSync");
+});
+
+add_storage_task(async function checkNullRemovesUnlistedFields(sm) {
+  // kA and kB are not listed in FXA_PWDMGR_*_FIELDS, but we still want to
+  // be able to delete them (migration case).
+  if (sm.secureStorage) {
+    sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
+    sm.secureStorage = new MockedSecureStorage({kA: "kA", kb: "kB"});
+  } else {
+    sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
+                                              kA: "kA", kb: "kB"});
+  }
+  await sm.initialize();
+
+  await sm.updateAccountData({kA: null, kB: null});
   let accountData = await sm.getAccountData();
   Assert.ok(!accountData.kA);
-  Assert.equal(accountData.kB, "kB");
+  Assert.ok(!accountData.kB);
 });
 
 add_storage_task(async function checkDelete(sm) {
   if (sm.secureStorage) {
     sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-    sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
+    sm.secureStorage = new MockedSecureStorage({kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+      kExtKbHash: "kExtKbHash"});
   } else {
     sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
-                                              kA: "kA", kB: "kB"});
+                                              kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+                                              kExtKbHash: "kExtKbHash"});
   }
   await sm.initialize();
 
   await sm.deleteAccountData();
   // Storage should have been reset to null.
   Assert.equal(sm.plainStorage.data, null);
   if (sm.secureStorage) {
     Assert.equal(sm.secureStorage.data, null);
@@ -218,132 +248,133 @@ add_storage_task(async function checkDel
   // And everything should reflect no user.
   Assert.equal((await sm.getAccountData()), null);
 });
 
 // Some tests only for the secure storage manager.
 add_task(async function checkNullUpdatesRemovedLocked() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
+  sm.secureStorage = new MockedSecureStorage({kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+                                              kExtKbHash: "kExtKbHash"});
   sm.secureStorage.locked = true;
   await sm.initialize();
 
-  await sm.updateAccountData({kA: null});
+  await sm.updateAccountData({kSync: null});
   let accountData = await sm.getAccountData();
-  Assert.ok(!accountData.kA);
-  // still no kB as we are locked.
-  Assert.ok(!accountData.kB);
+  Assert.ok(!accountData.kSync);
+  // still no kXCS as we are locked.
+  Assert.ok(!accountData.kXCS);
 
-  // now unlock - should still be no kA but kB should appear.
+  // now unlock - should still be no kSync but kXCS should appear.
   sm.secureStorage.locked = false;
   accountData = await sm.getAccountData();
-  Assert.ok(!accountData.kA);
-  Assert.equal(accountData.kB, "kB");
+  Assert.ok(!accountData.kSync);
+  Assert.equal(accountData.kXCS, "kXCS");
   // And secure storage should have been written with our previously-cached
   // data.
-  Assert.strictEqual(sm.secureStorage.data.accountData.kA, undefined);
-  Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB");
+  Assert.strictEqual(sm.secureStorage.data.accountData.kSync, undefined);
+  Assert.strictEqual(sm.secureStorage.data.accountData.kXCS, "kXCS");
 });
 
 add_task(async function checkEverythingReadSecure() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
   await sm.initialize();
 
   let accountData = await sm.getAccountData();
   Assert.ok(accountData, "read account data");
   Assert.equal(accountData.uid, "uid");
   Assert.equal(accountData.email, "someone@somewhere.com");
-  Assert.equal(accountData.kA, "kA");
+  Assert.equal(accountData.kXCS, "kXCS");
 });
 
 add_task(async function checkMemoryFieldsNotReturnedByDefault() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
   await sm.initialize();
 
   // keyPair is a memory field.
   await sm.updateAccountData({keyPair: "the keypair value"});
   let accountData = await sm.getAccountData();
 
   // Requesting everything should *not* return in memory fields.
   Assert.strictEqual(accountData.keyPair, undefined);
   // But requesting them specifically does get them.
   accountData = await sm.getAccountData("keyPair");
   Assert.strictEqual(accountData.keyPair, "the keypair value");
 });
 
 add_task(async function checkExplicitGet() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
   await sm.initialize();
 
-  let accountData = await sm.getAccountData(["uid", "kA"]);
+  let accountData = await sm.getAccountData(["uid", "kXCS"]);
   Assert.ok(accountData, "read account data");
   Assert.equal(accountData.uid, "uid");
-  Assert.equal(accountData.kA, "kA");
+  Assert.equal(accountData.kXCS, "kXCS");
   // We didn't ask for email so shouldn't have got it.
   Assert.strictEqual(accountData.email, undefined);
 });
 
 add_task(async function checkExplicitGetNoSecureRead() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
   await sm.initialize();
 
   Assert.equal(sm.secureStorage.fetchCount, 0);
   // request 2 fields in secure storage - it should have caused a single fetch.
   let accountData = await sm.getAccountData(["email", "uid"]);
   Assert.ok(accountData, "read account data");
   Assert.equal(accountData.uid, "uid");
   Assert.equal(accountData.email, "someone@somewhere.com");
-  Assert.strictEqual(accountData.kA, undefined);
+  Assert.strictEqual(accountData.kXCS, undefined);
   Assert.equal(sm.secureStorage.fetchCount, 1);
 });
 
 add_task(async function checkLockedUpdates() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"});
+  sm.secureStorage = new MockedSecureStorage({kSync: "old-kSync", kXCS: "kXCS"});
   sm.secureStorage.locked = true;
   await sm.initialize();
 
   let accountData = await sm.getAccountData();
-  // requesting kA and kB will fail as storage is locked.
-  Assert.ok(!accountData.kA);
-  Assert.ok(!accountData.kB);
+  // requesting kSync and kXCS will fail as storage is locked.
+  Assert.ok(!accountData.kSync);
+  Assert.ok(!accountData.kXCS);
   // While locked we can still update it and see the updated value.
-  sm.updateAccountData({kA: "new-kA"});
+  sm.updateAccountData({kSync: "new-kSync"});
   accountData = await sm.getAccountData();
-  Assert.equal(accountData.kA, "new-kA");
+  Assert.equal(accountData.kSync, "new-kSync");
   // unlock.
   sm.secureStorage.locked = false;
   accountData = await sm.getAccountData();
   // should reflect the value we updated and the one we didn't.
-  Assert.equal(accountData.kA, "new-kA");
-  Assert.equal(accountData.kB, "kB");
+  Assert.equal(accountData.kSync, "new-kSync");
+  Assert.equal(accountData.kXCS, "kXCS");
   // And storage should also reflect it.
-  Assert.strictEqual(sm.secureStorage.data.accountData.kA, "new-kA");
-  Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB");
+  Assert.strictEqual(sm.secureStorage.data.accountData.kSync, "new-kSync");
+  Assert.strictEqual(sm.secureStorage.data.accountData.kXCS, "kXCS");
 });
 
 // Some tests for the "storage queue" functionality.
 
 // A helper for our queued tests. It creates a StorageManager and then queues
 // an unresolved promise. The tests then do additional setup and checks, then
 // resolves or rejects the blocked promise.
 async function setupStorageManagerForQueueTest() {
   let sm = new FxAccountsStorageManager();
   sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
-  sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+  sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
   sm.secureStorage.locked = true;
   await sm.initialize();
 
   let resolveBlocked, rejectBlocked;
   let blockedPromise = new Promise((resolve, reject) => {
     resolveBlocked = resolve;
     rejectBlocked = reject;
   });
--- a/services/fxaccounts/tests/xpcshell/test_web_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -473,18 +473,20 @@ add_test(function test_helpers_open_sync
 });
 
 add_task(async function test_helpers_getFxAStatus_extra_engines() {
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       getSignedInUser() {
         return Promise.resolve({
           email: "testuser@testuser.com",
-          kA: "kA",
-          kb: "kB",
+          kSync: "kSync",
+          kXCS: "kXCS",
+          kExtSync: "kExtSync",
+          kExtKbHash: "kExtKbHash",
           sessionToken: "sessionToken",
           uid: "uid",
           verified: true
         });
       }
     },
     privateBrowsingUtils: {
       isBrowserPrivate: () => true
@@ -508,18 +510,20 @@ add_task(async function test_helpers_get
   };
 
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       getSignedInUser() {
         wasCalled.getSignedInUser = true;
         return Promise.resolve({
           email: "testuser@testuser.com",
-          kA: "kA",
-          kb: "kB",
+          kSync: "kSync",
+          kXCS: "kXCS",
+          kExtSync: "kExtSync",
+          kExtKbHash: "kExtKbHash",
           sessionToken: "sessionToken",
           uid: "uid",
           verified: true
         });
       }
     }
   });
 
@@ -542,18 +546,20 @@ add_task(async function test_helpers_get
 
       Assert.equal(signedInUser.email, "testuser@testuser.com");
       Assert.equal(signedInUser.sessionToken, "sessionToken");
       Assert.equal(signedInUser.uid, "uid");
       Assert.ok(signedInUser.verified);
 
       // These properties are filtered and should not
       // be returned to the requester.
-      Assert.equal(false, "kA" in signedInUser);
-      Assert.equal(false, "kB" in signedInUser);
+      Assert.equal(false, "kSync" in signedInUser);
+      Assert.equal(false, "kXCS" in signedInUser);
+      Assert.equal(false, "kExtSync" in signedInUser);
+      Assert.equal(false, "kExtKbHash" in signedInUser);
     });
 });
 
 add_task(async function test_helpers_getFxaStatus_allowed_no_signedInUser() {
   let wasCalled = {
     getSignedInUser: false,
     shouldAllowFxaStatus: false
   };
@@ -764,17 +770,17 @@ add_task(async function test_helpers_cha
     updateDeviceRegistration: false
   };
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       updateUserAccountData(credentials) {
         return new Promise(resolve => {
           Assert.ok(credentials.hasOwnProperty("email"));
           Assert.ok(credentials.hasOwnProperty("uid"));
-          Assert.ok(credentials.hasOwnProperty("kA"));
+          Assert.ok(credentials.hasOwnProperty("unwrapBKey"));
           Assert.ok(credentials.hasOwnProperty("deviceId"));
           Assert.equal(null, credentials.deviceId);
           // "foo" isn't a field known by storage, so should be dropped.
           Assert.ok(!credentials.hasOwnProperty("foo"));
           wasCalled.updateUserAccountData = true;
 
           resolve();
         });
@@ -782,17 +788,17 @@ add_task(async function test_helpers_cha
 
       updateDeviceRegistration() {
         Assert.equal(arguments.length, 0);
         wasCalled.updateDeviceRegistration = true;
         return Promise.resolve();
       }
     }
   });
-  await helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
+  await helpers.changePassword({ email: "email", uid: "uid", unwrapBKey: "unwrapBKey", foo: "foo" });
   Assert.ok(wasCalled.updateUserAccountData);
   Assert.ok(wasCalled.updateDeviceRegistration);
 });
 
 add_task(async function test_helpers_change_password_with_error() {
   let wasCalled = {
     updateUserAccountData: false,
     updateDeviceRegistration: false
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -112,19 +112,21 @@ this.makeIdentityConfig = function(overr
   // first setup the defaults.
   let result = {
     // Username used in both fxaccount and sync identity configs.
     username: "foo",
     // fxaccount specific credentials.
     fxaccount: {
       user: {
         assertion: "assertion",
-        email: "email",
-        kA: "kA",
-        kB: "kB",
+        email: "foo",
+        kSync: "a".repeat(128),
+        kXCS: "a".repeat(32),
+        kExtSync: "a".repeat(128),
+        kExtKbHash: "a".repeat(32),
         sessionToken: "sessionToken",
         uid: "a".repeat(32),
         verified: true,
       },
       token: {
         endpoint: null,
         duration: 300,
         id: "id",
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -131,26 +131,16 @@ this.telemetryHelper = {
   },
 
   // hookable by tests.
   nowInMinutes() {
     return Math.floor(Date.now() / 1000 / 60);
   },
 };
 
-
-function deriveKeyBundle(kB) {
-  let out = CryptoUtils.hkdf(kB, undefined,
-                             "identity.mozilla.com/picl/v1/oldsync", 2 * 32);
-  let bundle = new BulkKeyBundle();
-  // [encryptionKey, hmacKey]
-  bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
-  return bundle;
-}
-
 /*
   General authentication error for abstracting authentication
   errors from multiple sources (e.g., from FxAccounts, TokenServer).
   details is additional details about the error - it might be a string, or
   some other error object (which should do the right thing when toString() is
   called on it)
 */
 function AuthenticationError(details, source) {
@@ -250,17 +240,17 @@ this.BrowserIDManager.prototype = {
     // After this is called, we can expect Service.identity != this.
     for (let topic of OBSERVER_TOPICS) {
       Services.obs.removeObserver(this, topic);
     }
     this.resetCredentials();
     this._signedInUser = null;
   },
 
-  initializeWithCurrentIdentity(isInitialSync = false) {
+  async initializeWithCurrentIdentity(isInitialSync = false) {
     // While this function returns a promise that resolves once we've started
     // the auth process, that process is complete when
     // this.whenReadyToAuthenticate.promise resolves.
     this._log.trace("initializeWithCurrentIdentity");
 
     // Reset the world before we do anything async.
     this.whenReadyToAuthenticate = PromiseUtils.defer();
     this.whenReadyToAuthenticate.promise.catch(err => {
@@ -269,66 +259,69 @@ this.BrowserIDManager.prototype = {
 
     // initializeWithCurrentIdentity() can be called after the
     // identity module was first initialized, e.g., after the
     // user completes a force authentication, so we should make
     // sure all credentials are reset before proceeding.
     this.resetCredentials();
     this._authFailureReason = null;
 
-    return this._fxaService.getSignedInUser().then(accountData => {
+    try {
+      let accountData = await this._fxaService.getSignedInUser();
       if (!accountData) {
         this._log.info("initializeWithCurrentIdentity has no user logged in");
         // and we are as ready as we can ever be for auth.
         this._shouldHaveSyncKeyBundle = true;
         this.whenReadyToAuthenticate.reject("no user is logged in");
         return;
       }
 
       this.username = accountData.email;
       this._updateSignedInUser(accountData);
       // The user must be verified before we can do anything at all; we kick
       // this and the rest of initialization off in the background (ie, we
       // don't return the promise)
-      this._log.info("Waiting for user to be verified.");
-      if (!accountData.verified) {
-        telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
-      }
-      this._fxaService.whenVerified(accountData).then(accountData => {
-        this._updateSignedInUser(accountData);
+      CommonUtils.nextTick(async () => {
+        try {
+          this._log.info("Waiting for user to be verified.");
+          if (!accountData.verified) {
+            telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
+          }
+          accountData = await this._fxaService.whenVerified(accountData);
+          this._updateSignedInUser(accountData);
 
-        this._log.info("Starting fetch for key bundle.");
-        return this._fetchTokenForUser();
-      }).then(token => {
-        this._token = token;
-        if (token) {
-          // We may not have a token if the master-password is locked - but we
-          // still treat this as "success" so we don't prompt for re-authentication.
-          this._hashedUID = token.hashed_fxa_uid; // see _ensureValidToken for why we do this...
+          this._log.info("Starting fetch for key bundle.");
+          let token = await this._fetchTokenForUser();
+          this._token = token;
+          if (token) {
+            // We may not have a token if the master-password is locked - but we
+            // still treat this as "success" so we don't prompt for re-authentication.
+            this._hashedUID = token.hashed_fxa_uid; // see _ensureValidToken for why we do this...
+          }
+          this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
+          this.whenReadyToAuthenticate.resolve();
+          this._log.info("Background fetch for key bundle done");
+          Weave.Status.login = LOGIN_SUCCEEDED;
+          if (isInitialSync) {
+            this._log.info("Doing initial sync actions");
+            Svc.Prefs.set("firstSync", "resetClient");
+            Services.obs.notifyObservers(null, "weave:service:setup-complete");
+            CommonUtils.nextTick(Weave.Service.sync, Weave.Service);
+          }
+        } catch (authErr) {
+          // report what failed...
+          this._log.error("Background fetch for key bundle failed", authErr);
+          this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
+          this.whenReadyToAuthenticate.reject(authErr);
         }
-        this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
-        this.whenReadyToAuthenticate.resolve();
-        this._log.info("Background fetch for key bundle done");
-        Weave.Status.login = LOGIN_SUCCEEDED;
-        if (isInitialSync) {
-          this._log.info("Doing initial sync actions");
-          Svc.Prefs.set("firstSync", "resetClient");
-          Services.obs.notifyObservers(null, "weave:service:setup-complete");
-          CommonUtils.nextTick(Weave.Service.sync, Weave.Service);
-        }
-      }).catch(authErr => {
-        // report what failed...
-        this._log.error("Background fetch for key bundle failed", authErr);
-        this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
-        this.whenReadyToAuthenticate.reject(authErr);
+        // and we are done - the fetch continues on in the background...
       });
-      // and we are done - the fetch continues on in the background...
-    }).catch(err => {
+    } catch (err) {
       this._log.error("Processing logged in account", err);
-    });
+    }
   },
 
   _updateSignedInUser(userData) {
     // This object should only ever be used for a single user.  It is an
     // error to update the data if the user changes (but updates are still
     // necessary, as each call may add more attributes to the user).
     // We start with no user, so an initial update is always ok.
     if (this._signedInUser && this._signedInUser.email != userData.email) {
@@ -397,35 +390,16 @@ this.BrowserIDManager.prototype = {
       this.resetCredentials();
       this._ensureValidToken().catch(err =>
         this._log.error("Error while fetching a new token", err));
       break;
     }
   },
 
   /**
-   * Compute the sha256 of the message bytes.  Return bytes.
-   */
-  _sha256(message) {
-    let hasher = Cc["@mozilla.org/security/hash;1"]
-                    .createInstance(Ci.nsICryptoHash);
-    hasher.init(hasher.SHA256);
-    return CryptoUtils.digestBytes(message, hasher);
-  },
-
-  /**
-   * Compute the X-Client-State header given the byte string kB.
-   *
-   * Return string: hex(first16Bytes(sha256(kBbytes)))
-   */
-  _computeXClientState(kBbytes) {
-    return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
-  },
-
-  /**
    * Provide override point for testing token expiration.
    */
   _now() {
     return this._fxaService.now();
   },
 
   get _localtimeOffsetMsec() {
     return this._fxaService.localtimeOffsetMsec;
@@ -532,60 +506,48 @@ this.BrowserIDManager.prototype = {
     // username seems to make things fail fast so that's good.
     if (!this.username) {
       return LOGIN_FAILED_NO_USERNAME;
     }
 
     return STATUS_OK;
   },
 
-  // Do we currently have keys, or do we have enough that we should be able
-  // to successfully fetch them?
-  _canFetchKeys() {
-    let userData = this._signedInUser;
-    // a keyFetchToken means we can almost certainly grab them.
-    // kA and kB means we already have them.
-    return userData && (userData.keyFetchToken || (userData.kA && userData.kB));
-  },
-
   /**
    * Verify the current auth state, unlocking the master-password if necessary.
    *
    * Returns a promise that resolves with the current auth state after
    * attempting to unlock.
    */
-  unlockAndVerifyAuthState() {
-    if (this._canFetchKeys()) {
+  async unlockAndVerifyAuthState() {
+    if ((await this._fxaService.canGetKeys())) {
       log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys");
-      return Promise.resolve(STATUS_OK);
+      return STATUS_OK;
     }
     // so no keys - ensure MP unlocked.
     if (!Utils.ensureMPUnlocked()) {
       // user declined to unlock, so we don't know if they are stored there.
       log.debug("unlockAndVerifyAuthState: user declined to unlock master-password");
-      return Promise.resolve(MASTER_PASSWORD_LOCKED);
+      return MASTER_PASSWORD_LOCKED;
     }
     // now we are unlocked we must re-fetch the user data as we may now have
     // the details that were previously locked away.
-    return this._fxaService.getSignedInUser().then(
-      accountData => {
-        this._updateSignedInUser(accountData);
-        // If we still can't get keys it probably means the user authenticated
-        // without unlocking the MP or cleared the saved logins, so we've now
-        // lost them - the user will need to reauth before continuing.
-        let result;
-        if (this._canFetchKeys()) {
-          result = STATUS_OK;
-        } else {
-          result = LOGIN_FAILED_LOGIN_REJECTED;
-        }
-        log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
-        return result;
-      }
-    );
+    const accountData = await this._fxaService.getSignedInUser();
+    this._updateSignedInUser(accountData);
+    // If we still can't get keys it probably means the user authenticated
+    // without unlocking the MP or cleared the saved logins, so we've now
+    // lost them - the user will need to reauth before continuing.
+    let result;
+    if ((await this._fxaService.canGetKeys())) {
+      result = STATUS_OK;
+    } else {
+      result = LOGIN_FAILED_LOGIN_REJECTED;
+    }
+    log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
+    return result;
   },
 
   /**
    * Do we have a non-null, not yet expired token for the user currently
    * signed in?
    */
   hasValidToken() {
     // If pref is set to ignore cached authentication credentials for debugging,
@@ -615,39 +577,34 @@ this.BrowserIDManager.prototype = {
       url = url.slice(0, -1);
     }
     return url;
   },
 
   // Refresh the sync token for our user. Returns a promise that resolves
   // with a token (which may be null in one sad edge-case), or rejects with an
   // error.
-  _fetchTokenForUser() {
+  async _fetchTokenForUser() {
     // tokenServerURI is mis-named - convention is uri means nsISomething...
     let tokenServerURI = this._tokenServerUrl;
     let log = this._log;
     let client = this._tokenServerClient;
     let fxa = this._fxaService;
     let userData = this._signedInUser;
 
-    // We need kA and kB for things to work.  If we don't have them, just
+    // We need keys for things to work.  If we don't have them, just
     // return null for the token - sync calling unlockAndVerifyAuthState()
     // before actually syncing will setup the error states if necessary.
-    if (!this._canFetchKeys()) {
+    if (!(await this._fxaService.canGetKeys())) {
       log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
       return Promise.resolve(null);
     }
 
     let maybeFetchKeys = () => {
-      // This is called at login time and every time we need a new token - in
-      // the latter case we already have kA and kB, so optimise that case.
-      if (userData.kA && userData.kB) {
-        return null;
-      }
-      log.info("Fetching new keys");
+      log.info("Getting keys");
       return this._fxaService.getKeys().then(
         newUserData => {
           userData = newUserData;
           this._updateSignedInUser(userData); // throws if the user changed.
         }
       );
     };
 
@@ -657,18 +614,17 @@ this.BrowserIDManager.prototype = {
       let cb = function(err, token) {
         if (err) {
           return deferred.reject(err);
         }
         log.debug("Successfully got a sync token");
         return deferred.resolve(token);
       };
 
-      let kBbytes = CommonUtils.hexToBytes(userData.kB);
-      let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
+      let headers = {"X-Client-State": userData.kXCS};
       client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
       return deferred.promise;
     };
 
     let getAssertion = () => {
       log.info("Getting an assertion from", tokenServerURI);
       let audience = Services.io.newURI(tokenServerURI).prePath;
       return fxa.getAssertion(audience);
@@ -692,18 +648,17 @@ this.BrowserIDManager.prototype = {
           .then(assertion => getToken(assertion));
       })
       .then(token => {
         // TODO: Make it be only 80% of the duration, so refresh the token
         // before it actually expires. This is to avoid sync storage errors
         // otherwise, we get a nasty notification bar briefly. Bug 966568.
         token.expiration = this._now() + (token.duration * 1000) * 0.80;
         if (!this._syncKeyBundle) {
-          // We are given kA/kB as hex.
-          this._syncKeyBundle = deriveKeyBundle(CommonUtils.hexToBytes(userData.kB));
+          this._syncKeyBundle = BulkKeyBundle.fromHexKey(userData.kSync);
         }
         telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
         return token;
       })
       .catch(err => {
         // TODO: unify these errors - we need to handle errors thrown by
         // both tokenserverclient and hawkclient.
         // A tokenserver error thrown based on a bad response.
@@ -868,17 +823,17 @@ BrowserIDClusterManager.prototype = {
     this.service.clusterURL = cluster;
 
     return true;
   },
 
   _findCluster() {
     let endPointFromIdentityToken = () => {
       // The only reason (in theory ;) that we can end up with a null token
-      // is when this.identity._canFetchKeys() returned false.  In turn, this
+      // is when this._fxaService.canGetKeys() returned false.  In turn, this
       // should only happen if the master-password is locked or the credentials
       // storage is screwed, and in those cases we shouldn't have started
       // syncing so shouldn't get here anyway.
       // But better safe than sorry! To keep things clearer, throw an explicit
       // exception - the message will appear in the logs and the error will be
       // treated as transient.
       if (!this.identity._token) {
         throw new Error("Can't get a cluster URL as we can't fetch keys.");
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -125,16 +125,23 @@ KeyBundle.prototype = {
  */
 this.BulkKeyBundle = function BulkKeyBundle(collection) {
   let log = Log.repository.getLogger("Sync.BulkKeyBundle");
   log.info("BulkKeyBundle being created for " + collection);
   KeyBundle.call(this);
 
   this._collection = collection;
 };
+BulkKeyBundle.fromHexKey = function(hexKey) {
+  let key = CommonUtils.hexToBytes(hexKey);
+  let bundle = new BulkKeyBundle();
+  // [encryptionKey, hmacKey]
+  bundle.keyPair = [key.slice(0, 32), key.slice(32, 64)];
+  return bundle;
+};
 
 BulkKeyBundle.prototype = {
   __proto__: KeyBundle.prototype,
 
   get collection() {
     return this._collection;
   },
 
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -124,27 +124,28 @@ add_task(async function test_initialiali
     Assert.ok(signCertificateCalled);
     Assert.ok(accountStatusCalled);
     Assert.ok(!browseridManager._token);
     Assert.ok(!browseridManager.hasValidToken());
     Assert.deepEqual(getLoginTelemetryScalar(), {REJECTED: 1});
 });
 
 add_task(async function test_initialializeWithNoKeys() {
-    _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
+    _("Verify start after initializeWithCurrentIdentity without kSync, kXCS, kExtSync, kExtKbHash or keyFetchToken");
     let identityConfig = makeIdentityConfig();
-    delete identityConfig.fxaccount.user.kA;
-    delete identityConfig.fxaccount.user.kB;
+    delete identityConfig.fxaccount.user.kSync;
+    delete identityConfig.fxaccount.user.kXCS;
+    delete identityConfig.fxaccount.user.kExtSync;
+    delete identityConfig.fxaccount.user.kExtKbHash;
     // there's no keyFetchToken by default, so the initialize should fail.
     configureFxAccountIdentity(globalBrowseridManager, identityConfig);
 
     await globalBrowseridManager.initializeWithCurrentIdentity();
     await globalBrowseridManager.whenReadyToAuthenticate.promise;
     Assert.equal(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
-    Assert.ok(!globalBrowseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys");
     Assert.equal(globalBrowseridManager._token, null, "we don't have a token");
 });
 
 add_test(function test_getResourceAuthenticator() {
     _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
     configureFxAccountIdentity(globalBrowseridManager);
     let authenticator = globalBrowseridManager.getResourceAuthenticator();
     Assert.ok(!!authenticator);
@@ -187,34 +188,23 @@ add_test(function test_resourceAuthentic
   // Sanity check
   Assert.equal(hawkClient.now(), now);
   Assert.equal(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
 
   // Properly picked up by the client
   Assert.equal(fxaClient.now(), now);
   Assert.equal(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
 
-  let fxa = new MockFxAccounts();
-  fxa.internal._now_is = now;
-  fxa.internal.fxAccountsClient = fxaClient;
-
-  // Picked up by the signed-in user module
-  Assert.equal(fxa.internal.now(), now);
-  Assert.equal(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec);
-
-  Assert.equal(fxa.now(), now);
-  Assert.equal(fxa.localtimeOffsetMsec, localtimeOffsetMsec);
+  let identityConfig = makeIdentityConfig();
+  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+  fxaInternal._now_is = now;
+  fxaInternal.fxAccountsClient = fxaClient;
 
   // Mocks within mocks...
-  configureFxAccountIdentity(browseridManager, globalIdentityConfig);
-
-  // Ensure the new FxAccounts mock has a signed-in user.
-  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
-
-  browseridManager._fxaService = fxa;
+  configureFxAccountIdentity(browseridManager, globalIdentityConfig, fxaInternal);
 
   Assert.equal(browseridManager._fxaService.internal.now(), now);
   Assert.equal(browseridManager._fxaService.internal.localtimeOffsetMsec,
       localtimeOffsetMsec);
 
   Assert.equal(browseridManager._fxaService.now(), now);
   Assert.equal(browseridManager._fxaService.localtimeOffsetMsec,
       localtimeOffsetMsec);
@@ -248,26 +238,23 @@ add_test(function test_RESTResourceAuthe
     return now;
   };
   // Imagine there's already been one fxa request and the hawk client has
   // already detected skew vs the fxa auth server.
   hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;
 
   let fxaClient = new MockFxAccountsClient();
   fxaClient.hawk = hawkClient;
-  let fxa = new MockFxAccounts();
-  fxa.internal._now_is = now;
-  fxa.internal.fxAccountsClient = fxaClient;
 
-  configureFxAccountIdentity(browseridManager, globalIdentityConfig);
+  let identityConfig = makeIdentityConfig();
+  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+  fxaInternal._now_is = now;
+  fxaInternal.fxAccountsClient = fxaClient;
 
-  // Ensure the new FxAccounts mock has a signed-in user.
-  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
-
-  browseridManager._fxaService = fxa;
+  configureFxAccountIdentity(browseridManager, globalIdentityConfig, fxaInternal);
 
   Assert.equal(browseridManager._fxaService.internal.now(), now);
 
   let request = new Resource("https://example.net/i/like/pie/");
   let authenticator = browseridManager.getResourceAuthenticator();
   let output = authenticator(request, "GET");
   dump("output" + JSON.stringify(output));
   let authHeader = output.headers.authorization;
@@ -337,54 +324,16 @@ add_test(function test_tokenExpiration()
     });
     Assert.ok(bimExp._token.expiration < bimExp._now());
     _("... means BrowserIDManager knows to re-fetch it on the next call.");
     Assert.ok(!bimExp.hasValidToken());
     run_next_test();
   }
 );
 
-add_test(function test_sha256() {
-  // Test vectors from http://www.bichlmeier.info/sha256test.html
-  let vectors = [
-    ["",
-     "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
-    ["abc",
-     "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
-    ["message digest",
-     "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"],
-    ["secure hash algorithm",
-     "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"],
-    ["SHA256 is considered to be safe",
-     "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"],
-    ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
-     "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"],
-    ["For this sample, this 63-byte string will be used as input data",
-     "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"],
-    ["This is exactly 64 bytes long, not counting the terminating byte",
-     "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"]
-  ];
-  let bidUser = new BrowserIDManager();
-  for (let [input, output] of vectors) {
-    Assert.equal(CommonUtils.bytesAsHex(bidUser._sha256(input)), output);
-  }
-  run_next_test();
-});
-
-add_test(function test_computeXClientStateHeader() {
-  let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
-  let kB = CommonUtils.hexToBytes(kBhex);
-
-  let bidUser = new BrowserIDManager();
-  let header = bidUser._computeXClientState(kB);
-
-  Assert.equal(header, "6ae94683571c7a7c54dab4700aa3995f");
-  run_next_test();
-});
-
 add_task(async function test_getTokenErrors() {
   _("BrowserIDManager correctly handles various failures to get a token.");
 
   _("Arrange for a 401 - Sync should reflect an auth error.");
   initializeIdentityWithTokenServerResponse({
     status: 401,
     headers: {"content-type": "application/json"},
     body: JSON.stringify({}),
@@ -524,19 +473,21 @@ add_task(async function test_getKeysErro
   _("Auth server (via hawk) sends an observer notification on backoff headers.");
 
   // Set Sync's backoffInterval to zero - after we simulated the backoff header
   // it should reflect the value we sent.
   Status.backoffInterval = 0;
   _("Arrange for a 503 with a X-Backoff header.");
 
   let config = makeIdentityConfig();
-  // We want no kA or kB so we attempt to fetch them.
-  delete config.fxaccount.user.kA;
-  delete config.fxaccount.user.kB;
+  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+  delete config.fxaccount.user.kSync;
+  delete config.fxaccount.user.kXCS;
+  delete config.fxaccount.user.kExtSync;
+  delete config.fxaccount.user.kExtKbHash;
   config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
     Assert.equal(method, "get");
     Assert.equal(uri, "http://mockedserver:9999/account/keys");
     return {
       status: 503,
       headers: {"content-type": "application/json",
                 "x-backoff": "100"},
@@ -558,19 +509,21 @@ add_task(async function test_getKeysErro
   _("Auth server (via hawk) sends an observer notification on retry headers.");
 
   // Set Sync's backoffInterval to zero - after we simulated the backoff header
   // it should reflect the value we sent.
   Status.backoffInterval = 0;
   _("Arrange for a 503 with a Retry-After header.");
 
   let config = makeIdentityConfig();
-  // We want no kA or kB so we attempt to fetch them.
-  delete config.fxaccount.user.kA;
-  delete config.fxaccount.user.kB;
+  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+  delete config.fxaccount.user.kSync;
+  delete config.fxaccount.user.kXCS;
+  delete config.fxaccount.user.kExtSync;
+  delete config.fxaccount.user.kExtKbHash;
   config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
     Assert.equal(method, "get");
     Assert.equal(uri, "http://mockedserver:9999/account/keys");
     return {
       status: 503,
       headers: {"content-type": "application/json",
                 "retry-after": "100"},
@@ -621,19 +574,21 @@ add_task(async function test_getHAWKErro
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
 });
 
 add_task(async function test_getGetKeysFailing401() {
   _("BrowserIDManager correctly handles 401 responses fetching keys.");
 
   _("Arrange for a 401 - Sync should reflect an auth error.");
   let config = makeIdentityConfig();
-  // We want no kA or kB so we attempt to fetch them.
-  delete config.fxaccount.user.kA;
-  delete config.fxaccount.user.kB;
+  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+  delete config.fxaccount.user.kSync;
+  delete config.fxaccount.user.kXCS;
+  delete config.fxaccount.user.kExtSync;
+  delete config.fxaccount.user.kExtKbHash;
   config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
     Assert.equal(method, "get");
     Assert.equal(uri, "http://mockedserver:9999/account/keys");
     return {
       status: 401,
       headers: {"content-type": "application/json"},
       body: "{}",
@@ -642,19 +597,21 @@ add_task(async function test_getGetKeysF
   Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
 });
 
 add_task(async function test_getGetKeysFailing503() {
   _("BrowserIDManager correctly handles 5XX responses fetching keys.");
 
   _("Arrange for a 503 - Sync should reflect a network error.");
   let config = makeIdentityConfig();
-  // We want no kA or kB so we attempt to fetch them.
-  delete config.fxaccount.user.kA;
-  delete config.fxaccount.user.kB;
+  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+  delete config.fxaccount.user.kSync;
+  delete config.fxaccount.user.kXCS;
+  delete config.fxaccount.user.kExtSync;
+  delete config.fxaccount.user.kExtKbHash;
   config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
     Assert.equal(method, "get");
     Assert.equal(uri, "http://mockedserver:9999/account/keys");
     return {
       status: 503,
       headers: {"content-type": "application/json"},
       body: "{}",
@@ -663,20 +620,22 @@ add_task(async function test_getGetKeysF
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error");
 });
 
 add_task(async function test_getKeysMissing() {
   _("BrowserIDManager correctly handles getKeys succeeding but not returning keys.");
 
   let browseridManager = new BrowserIDManager();
   let identityConfig = makeIdentityConfig();
-  // our mock identity config already has kA and kB - remove them or we never
+  // our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
   // try and fetch them.
-  delete identityConfig.fxaccount.user.kA;
-  delete identityConfig.fxaccount.user.kB;
+  delete identityConfig.fxaccount.user.kSync;
+  delete identityConfig.fxaccount.user.kXCS;
+  delete identityConfig.fxaccount.user.kExtSync;
+  delete identityConfig.fxaccount.user.kExtKbHash;
   identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
 
   configureFxAccountIdentity(browseridManager, identityConfig);
 
   // Mock a fxAccounts object that returns no keys
   let fxa = new FxAccounts({
     fetchAndUnwrapKeys() {
       return Promise.resolve({});
@@ -709,26 +668,28 @@ add_task(async function test_getKeysMiss
 
   let ex;
   try {
     await browseridManager.whenReadyToAuthenticate.promise;
   } catch (e) {
     ex = e;
   }
 
-  Assert.ok(ex.message.indexOf("missing kA or kB") >= 0);
+  Assert.equal(ex.message, "user data missing: kSync, kXCS, kExtSync, kExtKbHash");
 });
 
 add_task(async function test_signedInUserMissing() {
   _("BrowserIDManager detects getSignedInUser returning incomplete account data");
 
   let browseridManager = new BrowserIDManager();
   // Delete stored keys and the key fetch token.
-  delete globalIdentityConfig.fxaccount.user.kA;
-  delete globalIdentityConfig.fxaccount.user.kB;
+  delete globalIdentityConfig.fxaccount.user.kSync;
+  delete globalIdentityConfig.fxaccount.user.kXCS;
+  delete globalIdentityConfig.fxaccount.user.kExtSync;
+  delete globalIdentityConfig.fxaccount.user.kExtKbHash;
   delete globalIdentityConfig.fxaccount.user.keyFetchToken;
 
   configureFxAccountIdentity(browseridManager, globalIdentityConfig);
 
   let fxa = new FxAccounts({
     fetchAndUnwrapKeys() {
       return Promise.resolve({});
     },
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -1,11 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Cu.import("resource://gre/modules/FxAccounts.jsm");
 Cu.import("resource://services-sync/browserid_identity.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
@@ -521,33 +522,37 @@ add_task(async function test_autoconnect
   Utils.mpLocked = () => true;
 
 
   let origEnsureMPUnlocked = Utils.ensureMPUnlocked;
   Utils.ensureMPUnlocked = () => {
     _("Faking Master Password entry cancelation.");
     return false;
   };
-  let origCanFetchKeys = Service.identity._canFetchKeys;
-  Service.identity._canFetchKeys = () => false;
+  let origFxA = Service.identity._fxaService;
+  Service.identity._fxaService = new FxAccounts({
+    canGetKeys() {
+      return false;
+    }
+  });
 
   // A locked master password will still trigger a sync, but then we'll hit
   // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
   let promiseObserved = promiseOneObserver("weave:service:login:error");
 
   scheduler.delayedAutoConnect(0);
   await promiseObserved;
 
   await Async.promiseYield();
 
   Assert.equal(Status.login, MASTER_PASSWORD_LOCKED);
 
   Utils.mpLocked = origLocked;
   Utils.ensureMPUnlocked = origEnsureMPUnlocked;
-  Service.identity._canFetchKeys = origCanFetchKeys;
+  Service.identity._fxaService = origFxA;
 
   await cleanUpAndGo(server);
 });
 
 add_task(async function test_no_autoconnect_during_wizard() {
   let server = sync_httpd_setup();
   await setUp(server);
 
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -135,25 +135,21 @@ function ciphertextHMAC(keyBundle, id, I
  * @returns {string} sha256 of the user's kB as a hex string
  */
 const getKBHash = async function(fxaService) {
   const signedInUser = await fxaService.getSignedInUser();
   if (!signedInUser) {
     throw new Error("User isn't signed in!");
   }
 
-  if (!signedInUser.kB) {
-    throw new Error("User doesn't have kB??");
+  if (!signedInUser.kExtKbHash) {
+    throw new Error("User doesn't have KbHash??");
   }
 
-  let kBbytes = CommonUtils.hexToBytes(signedInUser.kB);
-  let hasher = Cc["@mozilla.org/security/hash;1"]
-      .createInstance(Ci.nsICryptoHash);
-  hasher.init(hasher.SHA256);
-  return CommonUtils.bytesAsHex(CryptoUtils.digestBytes(signedInUser.uid + kBbytes, hasher));
+  return signedInUser.kExtKbHash;
 };
 
 /**
  * A "remote transformer" that the Kinto library will use to
  * encrypt/decrypt records when syncing.
  *
  * This is an "abstract base class". Subclass this and override
  * getKeys() to use it.
@@ -265,29 +261,21 @@ class KeyRingEncryptionRemoteTransformer
     return (async function() {
       const user = await self._fxaService.getSignedInUser();
       // FIXME: we should permit this if the user is self-hosting
       // their storage
       if (!user) {
         throw new Error("user isn't signed in to FxA; can't sync");
       }
 
-      if (!user.kB) {
-        throw new Error("user doesn't have kB");
+      if (!user.kExtSync) {
+        throw new Error("user doesn't have kExtSync");
       }
 
-      let kB = CommonUtils.hexToBytes(user.kB);
-
-      let keyMaterial = CryptoUtils.hkdf(kB, undefined,
-                                         "identity.mozilla.com/picl/v1/chrome.storage.sync",
-                                         2 * 32);
-      let bundle = new BulkKeyBundle();
-      // [encryptionKey, hmacKey]
-      bundle.keyPair = [keyMaterial.slice(0, 32), keyMaterial.slice(32, 64)];
-      return bundle;
+      return BulkKeyBundle.fromHexKey(user.kExtSync);
     })();
   }
   // Pass through the kbHash field from the unencrypted record. If
   // encryption fails, we can use this to try to detect whether we are
   // being compromised or if the record here was encoded with a
   // different kB.
   async encode(record) {
     const encoded = await super.encode(record);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -512,20 +512,22 @@ const assertExtensionRecord = async func
         "decrypted data should have a key attribute corresponding to the extension data key");
   return decoded;
 };
 
 // Tests using this ID will share keys in local storage, so be careful.
 const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
 const defaultExtension = {id: defaultExtensionId};
 
-const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+const kExtSync = "63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b31";
+const KB_HASH = "2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc800";
 const loggedInUser = {
   uid: "0123456789abcdef0123456789abcdef",
-  kB: BORING_KB,
+  kExtSync,
+  kExtKbHash: KB_HASH,
   oauthTokens: {
     "sync:addon-storage": {
       token: "some-access-token",
     },
   },
 };
 
 function uuid() {
@@ -866,17 +868,17 @@ add_task(async function ensureCanSync_ha
       deepEqual(postBody.keys.collections[extensionId], extensionKey.keyPairB64,
                 `decrypted new post should have preserved the key for ${extensionId}`);
     });
   });
 });
 
 add_task(async function checkSyncKeyRing_reuploads_keys() {
   // Verify that when keys are present, they are reuploaded with the
-  // new kB when we call touchKeys().
+  // new kbHash when we call touchKeys().
   const extensionId = uuid();
   let extensionKey, extensionSalt;
   await withContextAndServer(async function(context, server) {
     await withSignedInUser(loggedInUser, async function(extensionStorageSync, fxaService) {
       server.installCollection("storage-sync-crypto");
       server.etag = 765;
 
       await extensionStorageSync.cryptoCollection._clear();
@@ -887,70 +889,72 @@ add_task(async function checkSyncKeyRing
          `ensureCanSync should return a keyring that has a key for ${extensionId}`);
       extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
       equal(server.getPosts().length, 1,
             "generating a key that doesn't exist on the server should post it");
       const body = await assertPostedEncryptedKeys(fxaService, server.getPosts()[0]);
       extensionSalt = body.salts[extensionId];
     });
 
-    // The user changes their password. This is their new kB, with
-    // the last f changed to an e.
-    const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
-    const newUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+    // The user changes their password. This is their new kbHash, with
+    // the last 0 changed to a 1.
+    const NEW_KB_HASH = "2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc801";
+    const NEW_KEXT = "63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b30";
+    const newUser = Object.assign({}, loggedInUser, {kExtKbHash: NEW_KB_HASH, kExtSync: NEW_KEXT});
     let postedKeys;
     await withSignedInUser(newUser, async function(extensionStorageSync, fxaService) {
       await extensionStorageSync.checkSyncKeyRing();
 
       let posts = server.getPosts();
       equal(posts.length, 2,
-            "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
+            "when kBHash changes, checkSyncKeyRing should post the keyring reencrypted with the new kBHash");
       postedKeys = posts[1];
       assertPostedUpdatedRecord(postedKeys, 765);
 
       let body = await assertPostedEncryptedKeys(fxaService, postedKeys);
       deepEqual(body.keys.collections[extensionId], extensionKey,
                 `the posted keyring should have the same key for ${extensionId} as the old one`);
       deepEqual(body.salts[extensionId], extensionSalt,
                 `the posted keyring should have the same salt for ${extensionId} as the old one`);
     });
 
-    // Verify that with the old kB, we can't decrypt the record.
+    // Verify that with the old kBHash, we can't decrypt the record.
     await withSignedInUser(loggedInUser, async function(extensionStorageSync, fxaService) {
       let error;
       try {
         await new KeyRingEncryptionRemoteTransformer(fxaService).decode(postedKeys.body.data);
       } catch (e) {
         error = e;
       }
-      ok(error, "decrypting the keyring with the old kB should fail");
+      ok(error, "decrypting the keyring with the old kBHash should fail");
       ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error),
-         "decrypting the keyring with the old kB should throw an HMAC mismatch");
+         "decrypting the keyring with the old kBHash should throw an HMAC mismatch");
     });
   });
 });
 
 add_task(async function checkSyncKeyRing_overwrites_on_conflict() {
   // If there is already a record on the server that was encrypted
-  // with a different kB, we wipe the server, clear sync state, and
+  // with a different kbHash, we wipe the server, clear sync state, and
   // overwrite it with our keys.
   const extensionId = uuid();
   let extensionKey;
   await withSyncContext(async function(context) {
     await withServer(async function(server) {
-      // The old device has this kB, which is very similar to the
-      // current kB but with the last f changed to an e.
-      const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
-      const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+      // The old device has this kbHash, which is very similar to the
+      // current kbHash but with the last 0 changed to a 1.
+      const NEW_KB_HASH = "2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc801";
+      const NEW_KEXT = "63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b30";
+      const oldUser = Object.assign({}, loggedInUser, {kExtKbHash: NEW_KB_HASH, kExtSync: NEW_KEXT});
       server.installDeleteBucket();
       await withSignedInUser(oldUser, async function(extensionStorageSync, fxaService) {
         await server.installKeyRing(fxaService, {}, {}, 765);
       });
 
-      // Now we have this new user with a different kB.
+      // Now we have this new user with a different kbHash.
       await withSignedInUser(loggedInUser, async function(extensionStorageSync, fxaService) {
         await extensionStorageSync.cryptoCollection._clear();
 
         // Do an `ensureCanSync` to generate some keys.
         // This will try to sync, notice that the record is
         // undecryptable, and clear the server.
         let collectionKeys = await extensionStorageSync.ensureCanSync([extensionId]);
         ok(collectionKeys.hasKeysFor([extensionId]),
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -72,17 +72,17 @@
   "forms.jsm": ["FormData"],
   "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
   "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
   "FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"],
   "FrameScriptManager.jsm": ["getNewLoaderID"],
   "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
   "fxaccounts.jsm": ["Authentication"],
   "FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
-  "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
+  "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
   "FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
   "FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
   "FxAccountsPush.js": ["FxAccountsPushService"],
   "FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"],
   "FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"],
   "gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
   "gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
   "Geometry.jsm": ["Point", "Rect"],