Bug 1253740 - Introduce extensionIdToCollectionId, r=bsilverberg,kmag
authorEthan Glasser-Camp <eglassercamp@mozilla.com>
Wed, 27 Jul 2016 16:16:09 -0400
changeset 352282 608fb9db68b9f154ddd0c78c597e48cbda50e9e7
parent 352281 b1405d5fda4628ccddb7bc2d05fad2b5aa017908
child 352283 d4b969307f46d9514fffad88c8c8e922b67dda2a
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbsilverberg, kmag
bugs1253740
milestone52.0a1
Bug 1253740 - Introduce extensionIdToCollectionId, r=bsilverberg,kmag MozReview-Commit-ID: 5nDVtleknyN
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
@@ -2,16 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // TODO:
 // * find out how the Chrome implementation deals with conflicts
 
 "use strict";
 
+/* exported extensionIdToCollectionId */
+
 this.EXPORTED_SYMBOLS = ["ExtensionStorageSync"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 const global = this;
 
@@ -37,16 +39,20 @@ const {
 } = Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils",
                                   "resource://gre/modules/AppsUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
                                   "resource://services-sync/record.js");
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
+                                  "resource://services-common/utils.js");
+XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
+                                  "resource://services-crypto/utils.js");
 XPCOMUtils.defineLazyModuleGetter(this, "EncryptionRemoteTransformer",
                                   "resource://services-sync/engines/extension-storage.js");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
                                   "resource://gre/modules/ExtensionStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
                                   "resource://gre/modules/FxAccounts.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "loadKinto",
                                   "resource://services-common/kinto-offline-client.js");
@@ -305,16 +311,38 @@ const openCollection = Task.async(functi
   const {kinto} = yield storageSyncInit;
   const coll = kinto.collection(collectionId, {
     idSchema: storageSyncIdSchema,
     remoteTransformers: [new CollectionKeyEncryptionRemoteTransformer(extension.id)],
   });
   return coll;
 });
 
+/**
+ * Hash an extension ID for a given user so that an attacker can't
+ * identify the extensions a user has installed.
+ *
+ * @param {User} user
+ *               The user for whom to choose a collection to sync
+ *               an extension to.
+ * @param {string} extensionId The extension ID to obfuscate.
+ * @returns {string} A collection ID suitable for use to sync to.
+ */
+function extensionIdToCollectionId(user, extensionId) {
+  const userFingerprint = CryptoUtils.hkdf(user.uid, undefined,
+                                           "identity.mozilla.com/picl/v1/chrome.storage.sync.collectionIds", 2 * 32);
+  let data = new TextEncoder().encode(userFingerprint + extensionId);
+  let hasher = Cc["@mozilla.org/security/hash;1"]
+                 .createInstance(Ci.nsICryptoHash);
+  hasher.init(hasher.SHA256);
+  hasher.update(data, data.length);
+
+  return CommonUtils.bytesAsHex(hasher.finish(false));
+}
+
 this.ExtensionStorageSync = {
   _fxaService: fxAccounts,
   listeners: new WeakMap(),
 
   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)}\n`);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -2,22 +2,22 @@
  * 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://gre/modules/ExtensionStorageSync.jsm");
 const {
   CollectionKeyEncryptionRemoteTransformer,
   cryptoCollection,
   idToKey,
+  extensionIdToCollectionId,
   keyToId,
 } = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
 Cu.import("resource://services-sync/engines/extension-storage.js");
 Cu.import("resource://services-sync/keys.js");
 
 /* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */
 /* globals KeyRingEncryptionRemoteTransformer */
 
@@ -328,17 +328,19 @@ function assertKeyRingKey(keyRing, exten
 
 // Tests using this ID will share keys in local storage, so be careful.
 const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
 // FIXME: need to access whatever mechanism we use in the syncing code
 const defaultCollectionId = defaultExtensionId;
 const defaultExtension = {id: defaultExtensionId};
 
 const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+const ANOTHER_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0";
 const loggedInUser = {
+  uid: "0123456789abcdef0123456789abcdef",
   kB: BORING_KB,
   oauthTokens: {
     "sync:addon-storage": {
       token: "some-access-token",
     },
   },
 };
 
@@ -367,16 +369,30 @@ add_task(function* test_key_to_id() {
   equal(idToKey("key-%"), null);
   equal(idToKey("key-_HI"), null);
   equal(idToKey("key-_HI_"), null);
   equal(idToKey("key-"), "");
   equal(idToKey("key-1"), "1");
   equal(idToKey("key-_2D_"), "-");
 });
 
+add_task(function* test_extension_id_to_collection_id() {
+  const newKBUser = Object.assign(loggedInUser, {kB: ANOTHER_KB});
+  const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}";
+  const extensionId2 = "{9419cce6-5435-11e6-84bf-54ee758d6343}";
+
+  // "random" 32-char hex userid
+  equal(extensionIdToCollectionId(loggedInUser, extensionId),
+        "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d");
+  equal(extensionIdToCollectionId(loggedInUser, extensionId),
+        extensionIdToCollectionId(newKBUser, extensionId));
+  equal(extensionIdToCollectionId(loggedInUser, extensionId2),
+        "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8");
+});
+
 add_task(function* ensureKeysFor_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]);