Bug 1333810: Add salts to the keys record, r?rnewman, kmag draft
authorEthan Glasser-Camp <eglassercamp@mozilla.com>
Mon, 30 Jan 2017 16:03:59 -0500
changeset 469339 39606f8551698756cbf7c7b54eb43d423c831cf6
parent 467550 9c76cde475ff5704a8eb4d71e6b1b5ba0a2d18b1
child 469340 8cd817fc06bcc79f8a78c89650e8e7b73cf2b3e5
child 481526 82fbc733ddd0da679831680e2040ce13ef0f9543
push id43701
push usereglassercamp@mozilla.com
push dateThu, 02 Feb 2017 03:52:35 +0000
reviewersrnewman, kmag
bugs1333810
milestone54.0a1
Bug 1333810: Add salts to the keys record, r?rnewman, kmag Because these need to be encrypted with kB like the keyring does, and wiped at the same time as the keyring does, just smush them into the keyring. Rename `ensureKeysFor` to `ensureCanSync`, but don't update it to return the salts -- for now, we don't need that. MozReview-Commit-ID: DOJxdx5ugKl
toolkit/components/extensions/ExtensionStorageSync.jsm
toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -21,16 +21,17 @@ Cu.import("resource://gre/modules/AppCon
 const KINTO_PROD_SERVER_URL = "https://webextensions.settings.services.mozilla.com/v1";
 const KINTO_DEFAULT_SERVER_URL = KINTO_PROD_SERVER_URL;
 
 const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled";
 const STORAGE_SYNC_SERVER_URL_PREF = "webextensions.storage.sync.serverURL";
 const STORAGE_SYNC_SCOPE = "sync:addon_storage";
 const STORAGE_SYNC_CRYPTO_COLLECTION_NAME = "storage-sync-crypto";
 const STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID = "keys";
+const STORAGE_SYNC_CRYPTO_SALT_LENGTH_BYTES = 32;
 const FXA_OAUTH_OPTIONS = {
   scope: STORAGE_SYNC_SCOPE,
 };
 // Default is 5sec, which seems a bit aggressive on the open internet
 const KINTO_REQUEST_TIMEOUT = 30000;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {
@@ -190,21 +191,41 @@ if (AppConstants.platform != "android") 
       const {kinto} = yield storageSyncInit;
       return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
         idSchema: cryptoCollectionIdSchema,
         remoteTransformers: [new KeyRingEncryptionRemoteTransformer()],
       });
     }),
 
     /**
+     * Generate a new salt for use in hashing extension and record
+     * IDs.
+     *
+     * @returns {string} A base64-encoded string of the salt
+     */
+    getNewSalt() {
+      return btoa(CryptoUtils.generateRandomBytes(STORAGE_SYNC_CRYPTO_SALT_LENGTH_BYTES));
+    },
+
+    /**
      * Retrieve the keyring record from the crypto collection.
      *
      * You can use this if you want to check metadata on the keyring
      * record rather than use the keyring itself.
      *
+     * The keyring record, if present, should have the structure:
+     *
+     * - kbHash: a hash of the user's kB. When this changes, we will
+     *   try to sync the collection.
+     * - uuid: a record identifier. This will only change when we wipe
+     *   the collection (due to kB getting reset).
+     * - keys: a "WBO" form of a CollectionKeyManager.
+     * - salts: a normal JS Object with keys being collection IDs and
+     *   values being base64-encoded salts to use when hashing IDs
+     *   for that collection.
      * @returns {Promise<Object>}
      */
     getKeyRingRecord: Task.async(function* () {
       const collection = yield this.getCollection();
       const cryptoKeyRecord = yield collection.getAny(STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID);
 
       let data = cryptoKeyRecord.data;
       if (!data) {
@@ -213,16 +234,21 @@ if (AppConstants.platform != "android") 
         // reupload everything.
         const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
         const uuid = uuidgen.generateUUID().toString();
         data = {uuid};
       }
       return data;
     }),
 
+    getSalts: Task.async(function* () {
+      const cryptoKeyRecord = yield this.getKeyRingRecord();
+      return cryptoKeyRecord && cryptoKeyRecord.salts;
+    }),
+
     /**
      * Retrieve the actual keyring from the crypto collection.
      *
      * @returns {Promise<CollectionKeyManager>}
      */
     getKeyRing: Task.async(function* () {
       const cryptoKeyRecord = yield this.getKeyRingRecord();
       const collectionKeys = new CollectionKeyManager();
@@ -370,17 +396,17 @@ function extensionIdToCollectionId(user,
 }
 
 /**
  * Verify that we were built on not-Android. Call this as a sanity
  * check before using cryptoCollection.
  */
 function ensureCryptoCollection() {
   if (!cryptoCollection) {
-    throw new Error("Call to ensureKeysFor, but no sync code; are you on Android?");
+    throw new Error("Call to ensureCanSync, but no sync code; are you on Android?");
   }
 }
 
 // FIXME: This is kind of ugly. Probably we should have
 // ExtensionStorageSync not be a singleton, but a constructed object,
 // and this should be a constructor argument.
 let _fxaService = null;
 if (AppConstants.platform != "android") {
@@ -394,17 +420,17 @@ this.ExtensionStorageSync = {
   syncAll: Task.async(function* () {
     const extensions = extensionContexts.keys();
     const extIds = Array.from(extensions, extension => extension.id);
     log.debug(`Syncing extension settings for ${JSON.stringify(extIds)}`);
     if (extIds.length == 0) {
       // No extensions to sync. Get out.
       return;
     }
-    yield this.ensureKeysFor(extIds);
+    yield this.ensureCanSync(extIds);
     yield this.checkSyncKeyRing();
     const promises = Array.from(extensionContexts.keys(), extension => {
       return openCollection(extension).then(coll => {
         return this.sync(extension, coll);
       });
     });
     yield Promise.all(promises);
   }),
@@ -519,49 +545,88 @@ this.ExtensionStorageSync = {
       const kintoHttp = new KintoHttpClient(prefStorageSyncServerURL, {
         headers: headers,
         timeout: KINTO_REQUEST_TIMEOUT,
       });
       return yield kintoHttp.deleteBucket("default");
     });
   }),
 
+  ensureSaltsFor: Task.async(function* (keysRecord, extIds) {
+    const newSalts = Object.assign({}, keysRecord.salts);
+    for (let collectionId of extIds) {
+      if (newSalts[collectionId]) {
+        continue;
+      }
+
+      newSalts[collectionId] = cryptoCollection.getNewSalt();
+    }
+
+    return newSalts;
+  }),
+
+  /**
+   * Check whether the keys record (provided) already has salts for
+   * all the extensions given in extIds.
+   *
+   * @param {Object} keysRecord A previously-retrieved keys record.
+   * @param {Array<string>} extIds The IDs of the extensions which
+   *                need salts.
+   * @returns {boolean}
+   */
+  hasSaltsFor(keysRecord, extIds) {
+    if (!keysRecord.salts) {
+      return false;
+    }
+
+    for (let collectionId of extIds) {
+      if (!keysRecord.salts[collectionId]) {
+        return false;
+      }
+    }
+
+    return true;
+  },
+
   /**
    * Recursive promise that terminates when our local collectionKeys,
    * as well as that on the server, have keys for all the extensions
    * in extIds.
    *
    * @param {Array<string>} extIds
    *                        The IDs of the extensions which need keys.
    * @returns {Promise<CollectionKeyManager>}
    */
-  ensureKeysFor: Task.async(function* (extIds) {
+  ensureCanSync: Task.async(function* (extIds) {
     ensureCryptoCollection();
 
+    const keysRecord = yield cryptoCollection.getKeyRingRecord();
     const collectionKeys = yield cryptoCollection.getKeyRing();
-    if (collectionKeys.hasKeysFor(extIds)) {
+    if (collectionKeys.hasKeysFor(extIds) && this.hasSaltsFor(keysRecord, extIds)) {
       return collectionKeys;
     }
 
     const kbHash = yield this.getKBHash();
     const newKeys = yield collectionKeys.ensureKeysFor(extIds);
+    const newSalts = yield this.ensureSaltsFor(keysRecord, extIds);
     const newRecord = {
       id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
       keys: newKeys.asWBO().cleartext,
+      salts: newSalts,
       uuid: collectionKeys.uuid,
       // Add a field for the current kB hash.
       kbHash: kbHash,
     };
     yield cryptoCollection.upsert(newRecord);
     const result = yield this._syncKeyRing(newRecord);
     if (result.resolved.length != 0) {
       // We had a conflict which was automatically resolved. We now
       // have a new keyring which might have keys for the
       // collections. Recurse.
-      return yield this.ensureKeysFor(extIds);
+      return yield this.ensureCanSync(extIds);
     }
 
     // No conflicts. We're good.
     return newKeys;
   }),
 
   /**
    * Get the current user's hashed kB.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -2,16 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 do_get_profile();   // so we can use FxAccounts
 
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
 const {
   CollectionKeyEncryptionRemoteTransformer,
   cryptoCollection,
   idToKey,
   extensionIdToCollectionId,
   keyToId,
 } = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm", {});
@@ -257,21 +258,22 @@ class KintoServer {
           last_modified: 1475161309026,
           id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME
         },
       }));
     });
   }
 
   // Utility function to install a keyring at the start of a test.
-  installKeyRing(keysData, etag, {conflict = false} = {}) {
+  installKeyRing(keysData, salts, etag, {conflict = false} = {}) {
     this.installCollection("storage-sync-crypto");
     const keysRecord = {
       "id": "keys",
       "keys": keysData,
+      "salts": salts,
       "last_modified": etag,
     };
     this.etag = etag;
     const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord";
     this[methodName](new KeyRingEncryptionRemoteTransformer(),
                      "storage-sync-crypto", keysRecord);
   }
 
@@ -409,16 +411,17 @@ function assertPostedUpdatedRecord(post,
 // Assert that this post was an encrypted keyring, and produce the
 // decrypted body. Sanity check the body while we're here.
 const assertPostedEncryptedKeys = Task.async(function* (post) {
   equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys");
 
   let body = yield new KeyRingEncryptionRemoteTransformer().decode(post.body.data);
   ok(body.keys, `keys object should be present in decoded body`);
   ok(body.keys.default, `keys object should have a default key`);
+  ok(body.salts, `salts object should be present in decoded body`);
   return body;
 });
 
 // assertEqual, but for keyring[extensionId] == key.
 function assertKeyRingKey(keyRing, extensionId, expectedKey, message) {
   if (!message) {
     message = `expected keyring's key for ${extensionId} to match ${expectedKey.keyPairB64}`;
   }
@@ -484,127 +487,183 @@ add_task(function* test_extension_id_to_
   equal(extensionIdToCollectionId(loggedInUser, extensionId),
         "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d");
   equal(extensionIdToCollectionId(loggedInUser, extensionId),
         extensionIdToCollectionId(newKBUser, extensionId));
   equal(extensionIdToCollectionId(loggedInUser, extensionId2),
         "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8");
 });
 
-add_task(function* ensureKeysFor_posts_new_keys() {
+add_task(function* ensureCanSync_posts_new_keys() {
   const extensionId = uuid();
   yield* withContextAndServer(function* (context, server) {
     yield* withSignedInUser(loggedInUser, function* () {
       server.installCollection("storage-sync-crypto");
       server.etag = 1000;
 
-      let newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+      let newKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
       ok(newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}`);
 
       let posts = server.getPosts();
       equal(posts.length, 1);
       const post = posts[0];
       assertPostedNewRecord(post);
       const body = yield assertPostedEncryptedKeys(post);
+      const oldSalt = body.salts[extensionId];
       ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
+      ok(oldSalt, `salts object should have a salt for ${extensionId}`);
 
       // Try adding another key to make sure that the first post was
       // OK, even on a new profile.
       yield cryptoCollection._clear();
       server.clearPosts();
       // Restore the first posted keyring, but add a last_modified date
       const firstPostedKeyring = Object.assign({}, post.body.data, {last_modified: server.etag});
       server.addRecordInPast("storage-sync-crypto", firstPostedKeyring);
       const extensionId2 = uuid();
-      newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId2]);
+      newKeys = yield ExtensionStorageSync.ensureCanSync([extensionId2]);
       ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`);
       ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`);
 
       posts = server.getPosts();
       equal(posts.length, 1);
       const newPost = posts[posts.length - 1];
       const newBody = yield assertPostedEncryptedKeys(newPost);
       ok(newBody.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
       ok(newBody.keys.collections[extensionId2], `keys object should have a key for ${extensionId2}`);
+      ok(newBody.salts[extensionId], `salts object should have a key for ${extensionId}`);
+      ok(newBody.salts[extensionId2], `salts object should have a key for ${extensionId2}`);
+      equal(oldSalt, newBody.salts[extensionId], `old salt should be preserved in post`);
     });
   });
 });
 
-add_task(function* ensureKeysFor_pulls_key() {
-  // ensureKeysFor is implemented by adding a key to our local record
+add_task(function* ensureCanSync_pulls_key() {
+  // ensureCanSync is implemented by adding a key to our local record
   // and doing a sync. This means that if the same key exists
   // remotely, we get a "conflict". Ensure that we handle this
   // correctly -- we keep the server key (since presumably it's
   // already been used to encrypt records) and we don't wipe out other
   // collections' keys.
   const extensionId = uuid();
   const extensionId2 = uuid();
+  const extensionOnlyKey = uuid();
+  const extensionOnlySalt = uuid();
   const DEFAULT_KEY = new BulkKeyBundle("[default]");
   DEFAULT_KEY.generateRandom();
   const RANDOM_KEY = new BulkKeyBundle(extensionId);
   RANDOM_KEY.generateRandom();
+  const RANDOM_SALT = cryptoCollection.getNewSalt();
   yield* withContextAndServer(function* (context, server) {
     yield* withSignedInUser(loggedInUser, function* () {
       const keysData = {
         "default": DEFAULT_KEY.keyPairB64,
         "collections": {
           [extensionId]: RANDOM_KEY.keyPairB64,
         },
       };
-      server.installKeyRing(keysData, 999);
+      const saltData = {
+        [extensionId]: RANDOM_SALT,
+      };
+      server.installKeyRing(keysData, saltData, 999);
 
-      let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+      let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
       assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY);
 
       let posts = server.getPosts();
       equal(posts.length, 0,
-            "ensureKeysFor shouldn't push when the server keyring has the right key");
+            "ensureCanSync shouldn't push when the server keyring has the right key");
 
       // Another client generates a key for extensionId2
       const newKey = new BulkKeyBundle(extensionId2);
       newKey.generateRandom();
       keysData.collections[extensionId2] = newKey.keyPairB64;
+      saltData[extensionId2] = cryptoCollection.getNewSalt();
       server.clearCollection("storage-sync-crypto");
-      server.installKeyRing(keysData, 1000);
+      server.installKeyRing(keysData, saltData, 1000);
 
-      let newCollectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId, extensionId2]);
+      let newCollectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId, extensionId2]);
       assertKeyRingKey(newCollectionKeys, extensionId2, newKey);
       assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY,
-                       `ensureKeysFor shouldn't lose the old key for ${extensionId}`);
+                       `ensureCanSync shouldn't lose the old key for ${extensionId}`);
+
+      posts = server.getPosts();
+      equal(posts.length, 0, "ensureCanSync shouldn't push when updating keys");
+
+      // Another client generates a key, but not a salt, for extensionOnlyKey
+      const onlyKey = new BulkKeyBundle(extensionOnlyKey);
+      onlyKey.generateRandom();
+      keysData.collections[extensionOnlyKey] = onlyKey.keyPairB64;
+      server.clearCollection("storage-sync-crypto");
+      server.installKeyRing(keysData, saltData, 1001);
+
+      let withNewKey = yield ExtensionStorageSync.ensureCanSync([extensionId, extensionOnlyKey]);
+      dump(`got ${JSON.stringify(withNewKey.asWBO().cleartext)}\n`);
+      assertKeyRingKey(withNewKey, extensionOnlyKey, onlyKey);
+      assertKeyRingKey(withNewKey, extensionId, RANDOM_KEY,
+                       `ensureCanSync shouldn't lose the old key for ${extensionId}`);
 
       posts = server.getPosts();
-      equal(posts.length, 0, "ensureKeysFor shouldn't push when updating keys");
+      equal(posts.length, 1, "ensureCanSync should push when generating a new salt");
+      const withNewKeyRecord = yield assertPostedEncryptedKeys(posts[0]);
+      // We don't a priori know what the new salt is
+      dump(`${JSON.stringify(withNewKeyRecord)}\n`);
+      ok(withNewKeyRecord.salts[extensionOnlyKey],
+         `ensureCanSync should generate a salt for an extension that only had a key`);
+
+      // Another client generates a key, but not a salt, for extensionOnlyKey
+      const newSalt = cryptoCollection.getNewSalt();
+      saltData[extensionOnlySalt] = newSalt;
+      server.clearCollection("storage-sync-crypto");
+      server.installKeyRing(keysData, saltData, 1002);
+
+      let withOnlySaltKey = yield ExtensionStorageSync.ensureCanSync([extensionId, extensionOnlySalt]);
+      assertKeyRingKey(withOnlySaltKey, extensionId, RANDOM_KEY,
+                       `ensureCanSync shouldn't lose the old key for ${extensionId}`);
+      // We don't a priori know what the new key is
+      ok(withOnlySaltKey.hasKeysFor([extensionOnlySalt]),
+         `ensureCanSync generated a key for an extension that only had a salt`);
+
+      posts = server.getPosts();
+      equal(posts.length, 2, "ensureCanSync should push when generating a new key");
+      const withNewSaltRecord = yield assertPostedEncryptedKeys(posts[1]);
+      equal(withNewSaltRecord.salts[extensionOnlySalt], newSalt,
+            "ensureCanSync should keep the existing salt when generating only a key");
     });
   });
 });
 
-add_task(function* ensureKeysFor_handles_conflicts() {
+add_task(function* ensureCanSync_handles_conflicts() {
   // Syncing is done through a pull followed by a push of any merged
   // changes. Accordingly, the only way to have a "true" conflict --
   // i.e. with the server rejecting a change -- is if
   // someone pushes changes between our pull and our push. Ensure that
   // if this happens, we still behave sensibly (keep the remote key).
   const extensionId = uuid();
   const DEFAULT_KEY = new BulkKeyBundle("[default]");
   DEFAULT_KEY.generateRandom();
   const RANDOM_KEY = new BulkKeyBundle(extensionId);
   RANDOM_KEY.generateRandom();
+  const RANDOM_SALT = cryptoCollection.getNewSalt();
   yield* withContextAndServer(function* (context, server) {
     yield* withSignedInUser(loggedInUser, function* () {
       const keysData = {
         "default": DEFAULT_KEY.keyPairB64,
         "collections": {
           [extensionId]: RANDOM_KEY.keyPairB64,
         },
       };
-      server.installKeyRing(keysData, 765, {conflict: true});
+      const saltData = {
+        [extensionId]: RANDOM_SALT,
+      };
+      server.installKeyRing(keysData, saltData, 765, {conflict: true});
 
       yield cryptoCollection._clear();
 
-      let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+      let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
       assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY,
                        `syncing keyring should keep the server key for ${extensionId}`);
 
       let posts = server.getPosts();
       equal(posts.length, 1,
             "syncing keyring should have tried to post a keyring");
       const failedPost = posts[0];
       assertPostedNewRecord(failedPost);
@@ -618,31 +677,33 @@ add_task(function* ensureKeysFor_handles
     });
   });
 });
 
 add_task(function* checkSyncKeyRing_reuploads_keys() {
   // Verify that when keys are present, they are reuploaded with the
   // new kB when we call touchKeys().
   const extensionId = uuid();
-  let extensionKey;
+  let extensionKey, extensionSalt;
   yield* withContextAndServer(function* (context, server) {
     yield* withSignedInUser(loggedInUser, function* () {
       server.installCollection("storage-sync-crypto");
       server.etag = 765;
 
       yield cryptoCollection._clear();
 
-      // Do an `ensureKeysFor` to generate some keys.
-      let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+      // Do an `ensureCanSync` to generate some keys.
+      let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
       ok(collectionKeys.hasKeysFor([extensionId]),
-         `ensureKeysFor should return a keyring that has a key for ${extensionId}`);
+         `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 = yield assertPostedEncryptedKeys(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});
     let postedKeys;
     yield* withSignedInUser(newUser, function* () {
@@ -652,16 +713,18 @@ add_task(function* checkSyncKeyRing_reup
       equal(posts.length, 2,
             "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
       postedKeys = posts[1];
       assertPostedUpdatedRecord(postedKeys, 765);
 
       let body = yield assertPostedEncryptedKeys(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.
     yield* withSignedInUser(loggedInUser, function* () {
       let error;
       try {
         yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
       } catch (e) {
@@ -689,32 +752,33 @@ add_task(function* checkSyncKeyRing_over
       const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
       server.installCollection("storage-sync-crypto");
       server.installDeleteBucket();
       server.etag = 765;
       yield* withSignedInUser(oldUser, function* () {
         const FAKE_KEYRING = {
           id: "keys",
           keys: {},
+          salts: {},
           uuid: "abcd",
           kbHash: "abcd",
         };
         yield server.encryptAndAddRecord(transformer, "storage-sync-crypto", FAKE_KEYRING);
       });
 
       // Now we have this new user with a different kB.
       yield* withSignedInUser(loggedInUser, function* () {
         yield cryptoCollection._clear();
 
-        // Do an `ensureKeysFor` to generate some keys.
+        // Do an `ensureCanSync` to generate some keys.
         // This will try to sync, notice that the record is
         // undecryptable, and clear the server.
-        let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+        let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
         ok(collectionKeys.hasKeysFor([extensionId]),
-           `ensureKeysFor should always return a keyring with a key for ${extensionId}`);
+           `ensureCanSync should always return a keyring with a key for ${extensionId}`);
         extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
 
         deepEqual(server.getDeletedBuckets(), ["default"],
                   "Kinto server should have been wiped when keyring was thrown away");
 
         let posts = server.getPosts();
         equal(posts.length, 1,
              "new keyring should have been uploaded");
@@ -732,17 +796,17 @@ add_task(function* checkSyncKeyRing_over
         equal(typeof body.uuid, "string", "keyring UUIDs should be strings");
         notEqual(body.uuid, "abcd",
                  "new keyring should not have the same UUID as previous keyring");
         ok(body.keys,
            "new keyring should have a keys attribute");
         ok(body.keys.default, "new keyring should have a default key");
         // We should keep the extension key that was in our uploaded version.
         deepEqual(extensionKey, body.keys.collections[extensionId],
-                  "ensureKeysFor should have returned keyring with the same key that was uploaded");
+                  "ensureCanSync should have returned keyring with the same key that was uploaded");
 
         // This should be a no-op; the keys were uploaded as part of ensurekeysfor
         yield ExtensionStorageSync.checkSyncKeyRing();
         equal(server.getPosts().length, 1,
               "checkSyncKeyRing should not need to post keys after they were reuploaded");
       });
     });
   });
@@ -759,20 +823,20 @@ add_task(function* checkSyncKeyRing_flus
   yield* withSyncContext(function* (context) {
     yield* withServer(function* (server) {
       server.installCollection("storage-sync-crypto");
       server.installCollection(collectionId);
       server.installDeleteBucket();
       yield* withSignedInUser(loggedInUser, function* () {
         yield cryptoCollection._clear();
 
-        // Do an `ensureKeysFor` to get access to keys.
-        let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+        // Do an `ensureCanSync` to get access to keys.
+        let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
         ok(collectionKeys.hasKeysFor([extensionId]),
-           `ensureKeysFor should always return a keyring that has a key for ${extensionId}`);
+           `ensureCanSync should always return a keyring that has a key for ${extensionId}`);
         const extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
 
         // Set something to make sure that it gets re-uploaded when
         // uuid changes.
         yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
         yield ExtensionStorageSync.syncAll();
 
         let posts = server.getPosts();
@@ -784,16 +848,19 @@ add_task(function* checkSyncKeyRing_flus
 
         let body = yield transformer.decode(postedKeys.body.data);
         ok(body.uuid,
            "keyring should have a UUID");
         ok(body.keys,
            "keyring should have a keys attribute");
         ok(body.keys.default,
            "keyring should have a default key");
+        ok(body.salts[extensionId],
+           `keyring should have a salt for ${extensionId}`);
+        const extensionSalt = body.salts[extensionId];
         deepEqual(extensionKey, body.keys.collections[extensionId],
                   "new keyring should have the same key that we uploaded");
 
         // Another client comes along and replaces the UUID.
         // In real life, this would mean changing the keys too, but
         // this test verifies that just changing the UUID is enough.
         const newKeyRingData = Object.assign({}, body, {
           uuid: "abcd",
@@ -804,38 +871,40 @@ add_task(function* checkSyncKeyRing_flus
         });
         server.clearCollection("storage-sync-crypto");
         server.etag = 765;
         yield server.encryptAndAddRecordOnlyOnce(transformer, "storage-sync-crypto", newKeyRingData);
 
         // Fake adding another extension just so that the keyring will
         // really get synced.
         const newExtension = uuid();
-        const newKeyRing = yield ExtensionStorageSync.ensureKeysFor([newExtension]);
+        const newKeyRing = yield ExtensionStorageSync.ensureCanSync([newExtension]);
 
         // This should have detected the UUID change and flushed everything.
         // The keyring should, however, be the same, since we just
         // changed the UUID of the previously POSTed one.
         deepEqual(newKeyRing.keyForCollection(extensionId).keyPairB64, extensionKey,
-                  "ensureKeysFor should have pulled down a new keyring with the same keys");
+                  "ensureCanSync should have pulled down a new keyring with the same keys");
 
         // Syncing should reupload the data for the extension.
         yield ExtensionStorageSync.syncAll();
         posts = server.getPosts();
         equal(posts.length, 4,
               "should have posted keyring for new extension and reuploaded extension data");
 
         const finalKeyRingPost = posts[2];
         const reuploadedPost = posts[3];
 
         equal(finalKeyRingPost.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
               "keyring for new extension should have been posted to /keys");
         let finalKeyRing = yield transformer.decode(finalKeyRingPost.body.data);
         equal(finalKeyRing.uuid, "abcd",
               "newly uploaded keyring should preserve UUID from replacement keyring");
+        deepEqual(finalKeyRing.salts[extensionId], extensionSalt,
+                  "newly uploaded keyring should preserve salts from existing salts");
 
         // Confirm that the data got reuploaded
         equal(reuploadedPost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
               "extension data should be posted to path corresponding to its key");
         let reuploadedData = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(reuploadedPost.body.data);
         equal(reuploadedData.key, "my-key",
               "extension data should have a key attribute corresponding to the extension data key");
         equal(reuploadedData.data, 5,
@@ -855,17 +924,17 @@ add_task(function* test_storage_sync_pul
       server.installCollection(collectionId);
       server.installCollection("storage-sync-crypto");
 
       let calls = [];
       yield ExtensionStorageSync.addOnChangedListener(extension, function() {
         calls.push(arguments);
       }, context);
 
-      yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+      yield ExtensionStorageSync.ensureCanSync([extensionId]);
       yield server.encryptAndAddRecord(transformer, collectionId, {
         "id": "key-remote_2D_key",
         "key": "remote-key",
         "data": 6,
       });
 
       yield ExtensionStorageSync.syncAll();
       const remoteValue = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];