Bug 787273 - Part 5: Remove the CollectionKeys singleton; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Fri, 14 Sep 2012 16:02:33 -0700
changeset 111081 5a9e2b0e6ba1abccad91fd74602591b142fd0ece
parent 111080 6117227b889d83db35f64a605b1adcb16f95f38b
child 111082 c258bd2d95e4e09d5fd89dd9b3cc8d0f35a80110
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersrnewman
bugs787273
milestone18.0a1
Bug 787273 - Part 5: Remove the CollectionKeys singleton; r=rnewman CollectionKeys is gone. Instead, we export CollectionKeyManager (the underlying type) and an instance is available on the Service singleton.
services/sync/modules/engines.js
services/sync/modules/record.js
services/sync/modules/service.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/test_addons_engine.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_bookmark_record.js
services/sync/tests/unit/test_bookmark_smart_bookmarks.js
services/sync/tests/unit/test_clients_engine.js
services/sync/tests/unit/test_corrupt_keys.js
services/sync/tests/unit/test_engine_abort.js
services/sync/tests/unit/test_errorhandler.js
services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
services/sync/tests/unit/test_history_engine.js
services/sync/tests/unit/test_hmac_error.js
services/sync/tests/unit/test_interval_triggers.js
services/sync/tests/unit/test_keys.js
services/sync/tests/unit/test_records_crypto.js
services/sync/tests/unit/test_service_detect_upgrade.js
services/sync/tests/unit/test_service_sync_remoteSetup.js
services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
services/sync/tests/unit/test_service_wipeClient.js
services/sync/tests/unit/test_syncengine_sync.js
services/sync/tests/unit/test_syncscheduler.js
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -802,51 +802,55 @@ SyncEngine.prototype = {
       if (failed.length) {
         this.previousFailed = Utils.arrayUnion(failed, this.previousFailed);
         count.failed += failed.length;
         this._log.debug("Records that failed to apply: " + failed);
         failed = [];
       }
     }
 
+    let key = this.service.collectionKeys.keyForCollection(this.name);
+
     // Not binding this method to 'this' for performance reasons. It gets
     // called for every incoming record.
     let self = this;
+
     newitems.recordHandler = function(item) {
       if (aborting) {
         return;
       }
 
       // Grab a later last modified if possible
       if (self.lastModified == null || item.modified > self.lastModified)
         self.lastModified = item.modified;
 
       // Track the collection for the WBO.
       item.collection = self.name;
-      
+
       // Remember which records were processed
       handled.push(item.id);
 
       try {
         try {
-          item.decrypt();
+          item.decrypt(key);
         } catch (ex if Utils.isHMACMismatch(ex)) {
           let strategy = self.handleHMACMismatch(item, true);
           if (strategy == SyncEngine.kRecoveryStrategy.retry) {
             // You only get one retry.
             try {
               // Try decrypting again, typically because we've got new keys.
               self._log.info("Trying decrypt again...");
-              item.decrypt();
+              key = self.service.collectionKeys.keyForCollection(self.name);
+              item.decrypt(key);
               strategy = null;
             } catch (ex if Utils.isHMACMismatch(ex)) {
               strategy = self.handleHMACMismatch(item, false);
             }
           }
-          
+
           switch (strategy) {
             case null:
               // Retry succeeded! No further handling.
               break;
             case SyncEngine.kRecoveryStrategy.retry:
               self._log.debug("Ignoring second retry suggestion.");
               // Fall through to error case.
             case SyncEngine.kRecoveryStrategy.error:
@@ -1237,17 +1241,17 @@ SyncEngine.prototype = {
       });
 
       for each (let id in modifiedIDs) {
         try {
           let out = this._createRecord(id);
           if (this._log.level <= Log4Moz.Level.Trace)
             this._log.trace("Outgoing: " + out);
 
-          out.encrypt();
+          out.encrypt(this.service.collectionKeys.keyForCollection(this.name));
           up.pushData(out);
         }
         catch(ex) {
           this._log.warn("Error creating record: " + Utils.exceptionStr(ex));
         }
 
         // Partial upload
         if ((++count % MAX_UPLOAD_RECORDS) == 0)
@@ -1319,20 +1323,22 @@ SyncEngine.prototype = {
     // Report failure even if there's nothing to decrypt
     let canDecrypt = false;
 
     // Fetch the most recently uploaded record and try to decrypt it
     let test = new Collection(this.engineURL, this._recordObj, this.service);
     test.limit = 1;
     test.sort = "newest";
     test.full = true;
-    test.recordHandler = function(record) {
-      record.decrypt();
+
+    let key = this.service.collectionKeys.keyForCollection(this.name);
+    test.recordHandler = function recordHandler(record) {
+      record.decrypt(key);
       canDecrypt = true;
-    };
+    }.bind(this);
 
     // Any failure fetching/decrypting will just result in false
     try {
       this._log.trace("Trying to decrypt a record from the server..");
       test.get();
     }
     catch(ex) {
       this._log.debug("Failed test decrypt: " + Utils.exceptionStr(ex));
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const EXPORTED_SYMBOLS = [
   "WBORecord",
   "RecordManager",
   "CryptoWrapper",
-  "CollectionKeys",
+  "CollectionKeyManager",
   "Collection",
 ];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
@@ -191,37 +191,35 @@ CryptoWrapper.prototype = {
    * collection, which is decrypted with the sync key.
    *
    * Cache those keys; invalidate the cache if the time on the keys collection
    * changes, or other auth events occur.
    *
    * Optional key bundle overrides the collection key lookup.
    */
   encrypt: function encrypt(keyBundle) {
-    keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
     if (!keyBundle) {
-      throw new Error("Key bundle is null for " + this.uri.spec);
+      throw new Error("A key bundle must be supplied to encrypt.");
     }
 
     this.IV = Svc.Crypto.generateRandomIV();
     this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext),
                                          keyBundle.encryptionKeyB64, this.IV);
     this.hmac = this.ciphertextHMAC(keyBundle);
     this.cleartext = null;
   },
 
   // Optional key bundle.
   decrypt: function decrypt(keyBundle) {
     if (!this.ciphertext) {
       throw "No ciphertext: nothing to decrypt?";
     }
 
-    keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
     if (!keyBundle) {
-      throw new Error("Key bundle is null for " + this.collection + "/" + this.id);
+      throw new Error("A key bundle must be supplied to decrypt.");
     }
 
     // Authenticate the encrypted blob with the expected HMAC
     let computedHMAC = this.ciphertextHMAC(keyBundle);
 
     if (computedHMAC != this.hmac) {
       Utils.throwHMACMismatch(this.hmac, computedHMAC);
     }
@@ -266,33 +264,29 @@ CryptoWrapper.prototype = {
     WBORecord.prototype.__lookupSetter__("id").call(this, val);
     return this.cleartext.id = val;
   },
 };
 
 Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]);
 Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
 
-XPCOMUtils.defineLazyGetter(this, "CollectionKeys", function () {
-  return new CollectionKeyManager();
-});
-
 
 /**
  * Keeps track of mappings between collection names ('tabs') and KeyBundles.
  *
  * You can update this thing simply by giving it /info/collections. It'll
  * use the last modified time to bring itself up to date.
  */
 function CollectionKeyManager() {
   this.lastModified = 0;
   this._collections = {};
   this._default = null;
 
-  this._log = Log4Moz.repository.getLogger("Sync.CollectionKeys");
+  this._log = Log4Moz.repository.getLogger("Sync.CollectionKeyManager");
 }
 
 // TODO: persist this locally as an Identity. Bug 610913.
 // Note that the last modified time needs to be preserved.
 CollectionKeyManager.prototype = {
 
   // Return information about old vs new keys:
   // * same: true if two collections are equal
@@ -321,17 +315,17 @@ CollectionKeyManager.prototype = {
             changed: changed};
   },
 
   get isClear() {
    return !this._default;
   },
 
   clear: function clear() {
-    this._log.info("Clearing CollectionKeys...");
+    this._log.info("Clearing collection keys...");
     this.lastModified = 0;
     this._collections = {};
     this._default = null;
   },
 
   keyForCollection: function(collection) {
     if (collection && this._collections[collection])
       return this._collections[collection];
@@ -424,26 +418,26 @@ CollectionKeyManager.prototype = {
   //
   setContents: function setContents(payload, modified) {
 
     if (!modified)
       throw "No modified time provided to setContents.";
 
     let self = this;
 
-    this._log.info("Setting CollectionKeys contents. Our last modified: " +
+    this._log.info("Setting collection keys contents. Our last modified: " +
                    this.lastModified + ", input modified: " + modified + ".");
 
     if (!payload)
-      throw "No payload in CollectionKeys.setContents().";
+      throw "No payload in CollectionKeyManager.setContents().";
 
     if (!payload.default) {
       this._log.warn("No downloaded default key: this should not occur.");
       this._log.warn("Not clearing local keys.");
-      throw "No default key in CollectionKeys.setContents(). Cannot proceed.";
+      throw "No default key in CollectionKeyManager.setContents(). Cannot proceed.";
     }
 
     // Process the incoming default key.
     let b = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
     b.keyPairB64 = payload.default;
     let newDefault = b;
 
     // Process the incoming collections.
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -232,17 +232,17 @@ WeaveSvc.prototype = {
     this.lastHMACEvent = now;
 
     // Fetch keys.
     let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
     try {
       let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
 
       // Save out the ciphertext for when we reupload. If there's a bug in
-      // CollectionKeys, this will prevent us from uploading junk.
+      // CollectionKeyManager, this will prevent us from uploading junk.
       let cipherText = cryptoKeys.ciphertext;
 
       if (!cryptoResp.success) {
         this._log.warn("Failed to download keys.");
         return false;
       }
 
       let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
@@ -270,20 +270,18 @@ WeaveSvc.prototype = {
       this._log.warn("Got exception \"" + ex + "\" fetching and handling " +
                      "crypto keys. Will try again later.");
       return false;
     }
   },
 
   handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) {
     // Don't want to wipe if we're just starting up!
-    // This is largely relevant because we don't persist
-    // CollectionKeys yet: Bug 610913.
-    let wasBlank = CollectionKeys.isClear;
-    let keysChanged = CollectionKeys.updateContents(syncKey, cryptoKeys);
+    let wasBlank = this.collectionKeys.isClear;
+    let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys);
 
     if (keysChanged && !wasBlank) {
       this._log.debug("Keys changed: " + JSON.stringify(keysChanged));
 
       if (!skipReset) {
         this._log.info("Resetting client to reflect key change.");
 
         if (keysChanged.length) {
@@ -313,16 +311,18 @@ WeaveSvc.prototype = {
     // needs to be a singleton. Ideally, the longer-lived object would spawn
     // this service instance.
     if (!Status || !Status._authManager) {
       throw new Error("Status or Status._authManager not initialized.");
     }
 
     this.identity = Status._authManager;
 
+    this.collectionKeys = new CollectionKeyManager();
+
     this.errorHandler = new ErrorHandler(this);
 
     this._log = Log4Moz.repository.getLogger("Sync.Service");
     this._log.level =
       Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")];
 
     this._log.info("Loading Weave " + WEAVE_VERSION);
 
@@ -555,35 +555,35 @@ WeaveSvc.prototype = {
         this.errorHandler.checkServerError(infoResponse);
         return false;
       }
 
       let infoCollections = infoResponse.obj;
 
       this._log.info("Testing info/collections: " + JSON.stringify(infoCollections));
 
-      if (CollectionKeys.updateNeeded(infoCollections)) {
-        this._log.info("CollectionKeys reports that a key update is needed.");
+      if (this.collectionKeys.updateNeeded(infoCollections)) {
+        this._log.info("collection keys reports that a key update is needed.");
 
         // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this.
 
         // Fetch storage/crypto/keys.
         let cryptoKeys;
 
         if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) {
           try {
             cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
             let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
 
             if (cryptoResp.success) {
               let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys);
               return true;
             }
             else if (cryptoResp.status == 404) {
-              // On failure, ask CollectionKeys to generate new keys and upload them.
+              // On failure, ask to generate new keys and upload them.
               // Fall through to the behavior below.
               this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating.");
               cryptoKeys = null;
             }
             else {
               // Some other problem.
               Status.login = LOGIN_FAILED_SERVER_ERROR;
               this.errorHandler.checkServerError(cryptoResp);
@@ -725,17 +725,17 @@ WeaveSvc.prototype = {
       Status.login = LOGIN_FAILED_NETWORK_ERROR;
       this.errorHandler.checkServerError(ex);
       return false;
     }
   },
 
   generateNewSymmetricKeys: function generateNewSymmetricKeys() {
     this._log.info("Generating new keys WBO...");
-    let wbo = CollectionKeys.generateNewKeysWBO();
+    let wbo = this.collectionKeys.generateNewKeysWBO();
     this._log.info("Encrypting new key bundle.");
     wbo.encrypt(this.identity.syncKeyBundle);
 
     this._log.info("Uploading...");
     let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL));
     if (uploadRes.status != 200) {
       this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!");
       this.errorHandler.checkServerError(uploadRes);
@@ -813,17 +813,17 @@ WeaveSvc.prototype = {
       this.logout();
 
       /* Set this so UI is updated on next run. */
       this.identity.syncKey = newphrase;
       this.persistLogin();
 
       /* We need to re-encrypt everything, so reset. */
       this.resetClient();
-      CollectionKeys.clear();
+      this.collectionKeys.clear();
 
       /* Login and sync. This also generates new keys. */
       this.sync();
 
       Svc.Obs.notify("weave:service:change-passphrase", true);
 
       return true;
     })();
@@ -854,17 +854,17 @@ WeaveSvc.prototype = {
         }
       }
     } else {
       this._log.debug("Skipping client data removal: no cluster URL.");
     }
 
     // Reset all engines and clear keys.
     this.resetClient();
-    CollectionKeys.clear();
+    this.collectionKeys.clear();
     Status.resetBackoff();
 
     // Reset Weave prefs.
     this._ignorePrefObserver = true;
     Svc.Prefs.resetBranch("");
     this._ignorePrefObserver = false;
 
     Svc.Prefs.set("lastversion", WEAVE_VERSION);
@@ -1069,17 +1069,17 @@ WeaveSvc.prototype = {
       Status.sync = VERSION_OUT_OF_DATE;
       this._log.warn("Upgrade required to access newer storage version.");
       return false;
     }
     else if (meta.payload.syncID != this.syncID) {
 
       this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID);
       this.resetClient();
-      CollectionKeys.clear();
+      this.collectionKeys.clear();
       this.syncID = meta.payload.syncID;
       this._log.debug("Clear cached values and take syncId: " + this.syncID);
 
       if (!this.upgradeSyncKey(meta.payload.syncID)) {
         this._log.warn("Failed to upgrade sync key. Failing remote setup.");
         return false;
       }
 
@@ -1231,17 +1231,17 @@ WeaveSvc.prototype = {
     this.persistLogin();
     this._log.info("Done saving.");
     return true;
   },
 
   _freshStart: function _freshStart() {
     this._log.info("Fresh start. Resetting client and considering key upgrade.");
     this.resetClient();
-    CollectionKeys.clear();
+    this.collectionKeys.clear();
     this.upgradeSyncKey(this.syncID);
 
     // Wipe the server.
     let wipeTimestamp = this.wipeServer();
 
     // Upload a new meta/global record.
     let meta = new WBORecord("meta", "global");
     meta.payload.syncID = this.syncID;
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -268,20 +268,20 @@ function encryptPayload(cleartext) {
     cleartext = JSON.stringify(cleartext);
   }
 
   return {ciphertext: cleartext, // ciphertext == cleartext with fake crypto
           IV: "irrelevant",
           hmac: fakeSHA256HMAC(cleartext, Utils.makeHMACKey(""))};
 }
 
-function generateNewKeys(collections) {
-  let wbo = CollectionKeys.generateNewKeysWBO(collections);
+function generateNewKeys(collectionKeys, collections=null) {
+  let wbo = collectionKeys.generateNewKeysWBO(collections);
   let modified = new_timestamp();
-  CollectionKeys.setContents(wbo.cleartext, modified);
+  collectionKeys.setContents(wbo.cleartext, modified);
 }
 
 /*
  * A fake engine implementation.
  * This is used all over the place.
  *
  * Complete with record, store, and tracker implementations.
  */
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -153,17 +153,17 @@ add_test(function test_disabled_install_
 
   const USER       = "foo";
   const PASSWORD   = "password";
   const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
   const ADDON_ID   = "addon1@tests.mozilla.org";
 
   new SyncTestingInfrastructure(USER, PASSWORD, PASSPHRASE);
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let contents = {
     meta: {global: {engines: {addons: {version: engine.version,
                                       syncID:  engine.syncID}}}},
     crypto: {},
     addons: {}
   };
 
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -468,12 +468,12 @@ add_test(function test_bookmark_tag_but_
 });
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level  = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level   = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Tracker.Bookmarks").level = Log4Moz.Level.Trace;
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   run_next_test();
 }
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -12,17 +12,17 @@ function prepareBookmarkItem(collection,
   let b = new Bookmark(collection, id);
   b.cleartext.stuff = "my payload here";
   return b;
 }
 
 function run_test() {
   Service.identity.username = "john@example.com";
   Service.identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
   let keyBundle = Service.identity.syncKeyBundle;
 
   let log = Log4Moz.repository.getLogger("Test");
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   log.info("Creating a record");
 
   let u = "http://localhost:8080/storage/bookmarks/foo";
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -225,12 +225,12 @@ add_test(function test_smart_bookmarks_d
     Service.recordManager.clearCache();
   }
 });
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   run_next_test();
 }
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -44,63 +44,63 @@ add_test(function test_bad_hmac() {
 
   function check_client_deleted(id) {
     let coll = user.collection("clients");
     let wbo  = coll.wbo(id);
     return !wbo || !wbo.payload;
   }
 
   function uploadNewKeys() {
-    generateNewKeys();
-    let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
     serverKeys.encrypt(Service.identity.syncKeyBundle);
     do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
   }
 
   try {
     let passphrase     = "abcdeabcdeabcdeabcdeabcdea";
     Service.serverURL  = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
     Service.login("foo", "ilovejane", passphrase);
 
-    generateNewKeys();
+    generateNewKeys(Service.collectionKeys);
 
     _("First sync, client record is uploaded");
     do_check_eq(engine.lastRecordUpload, 0);
     check_clients_count(0);
     engine._sync();
     check_clients_count(1);
     do_check_true(engine.lastRecordUpload > 0);
 
     // Initial setup can wipe the server, so clean up.
     deletedCollections = [];
     deletedItems       = [];
 
     _("Change our keys and our client ID, reupload keys.");
     let oldLocalID  = engine.localID;     // Preserve to test for deletion!
     engine.localID = Utils.makeGUID();
     engine.resetClient();
-    generateNewKeys();
-    let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
     serverKeys.encrypt(Service.identity.syncKeyBundle);
     do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
 
     _("Sync.");
     engine._sync();
 
     _("Old record " + oldLocalID + " was deleted, new one uploaded.");
     check_clients_count(1);
     check_client_deleted(oldLocalID);
 
     _("Now change our keys but don't upload them. " +
       "That means we get an HMAC error but redownload keys.");
     Service.lastHMACEvent = 0;
     engine.localID = Utils.makeGUID();
     engine.resetClient();
-    generateNewKeys();
+    generateNewKeys(Service.collectionKeys);
     deletedCollections = [];
     deletedItems       = [];
     check_clients_count(1);
     engine._sync();
 
     _("Old record was not deleted, new one uploaded.");
     do_check_eq(deletedCollections.length, 0);
     do_check_eq(deletedItems.length, 0);
@@ -126,26 +126,26 @@ add_test(function test_bad_hmac() {
     uploadNewKeys();
 
     // Create a new client record and new keys. Now our keys are wrong, as well
     // as the object on the server. We'll download the new keys and also delete
     // the bad client record.
     oldLocalID  = engine.localID;         // Preserve to test for deletion!
     engine.localID = Utils.makeGUID();
     engine.resetClient();
-    generateNewKeys();
-    let oldKey = CollectionKeys.keyForCollection();
+    generateNewKeys(Service.collectionKeys);
+    let oldKey = Service.collectionKeys.keyForCollection();
 
     do_check_eq(deletedCollections.length, 0);
     do_check_eq(deletedItems.length, 0);
     engine._sync();
     do_check_eq(deletedItems.length, 1);
     check_client_deleted(oldLocalID);
     check_clients_count(1);
-    let newKey = CollectionKeys.keyForCollection();
+    let newKey = Service.collectionKeys.keyForCollection();
     do_check_false(oldKey.equals(newKey));
 
   } finally {
     Svc.Prefs.resetBranch("");
     Service.recordManager.clearCache();
     server.stop(run_next_test);
   }
 });
@@ -164,17 +164,17 @@ add_test(function test_properties() {
     run_next_test();
   }
 });
 
 add_test(function test_sync() {
   _("Ensure that Clients engine uploads a new client record once a week.");
 
   new SyncTestingInfrastructure();
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let contents = {
     meta: {global: {engines: {clients: {version: engine.version,
                                         syncID: engine.syncID}}}},
     clients: {},
     crypto: {}
   };
   let server = serverForUsers({"foo": "password"}, contents);
@@ -404,17 +404,17 @@ add_test(function test_process_incoming_
 });
 
 add_test(function test_command_sync() {
   _("Ensure that commands are synced across clients.");
 
   new SyncTestingInfrastructure();
 
   engine._store.wipe();
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let contents = {
     meta: {global: {engines: {clients: {version: engine.version,
                                         syncID: engine.syncID}}}},
     clients: {},
     crypto: {}
   };
   let server   = serverForUsers({"foo": "password"}, contents);
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -55,97 +55,97 @@ add_test(function test_locally_changed_k
 
     setBasicCredentials("johndoe", "password", passphrase);
     Service.serverURL = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
 
     Service.engineManager.register(HistoryEngine);
 
     function corrupt_local_keys() {
-      CollectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
-                                         Svc.Crypto.generateRandomKey()];
+      Service.collectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
+                                                 Svc.Crypto.generateRandomKey()];
     }
 
     _("Setting meta.");
 
     // Bump version on the server.
     let m = new WBORecord("meta", "global");
     m.payload = {"syncID": "foooooooooooooooooooooooooo",
                  "storageVersion": STORAGE_VERSION};
     m.upload(Service.resource(Service.metaURL));
 
     _("New meta/global: " + JSON.stringify(johndoe.collection("meta").wbo("global")));
 
     // Upload keys.
-    generateNewKeys();
-    let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
     serverKeys.encrypt(Service.identity.syncKeyBundle);
     do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
 
     // Check that login works.
     do_check_true(Service.login("johndoe", "ilovejane", passphrase));
     do_check_true(Service.isLoggedIn);
 
     // Sync should upload records.
     Service.sync();
 
     // Tabs exist.
     _("Tabs modified: " + johndoe.modified("tabs"));
     do_check_true(johndoe.modified("tabs") > 0);
 
-    let coll_modified = CollectionKeys.lastModified;
+    let coll_modified = Service.collectionKeys.lastModified;
 
     // Let's create some server side history records.
-    let liveKeys = CollectionKeys.keyForCollection("history");
+    let liveKeys = Service.collectionKeys.keyForCollection("history");
     _("Keys now: " + liveKeys.keyPair);
     let visitType = Ci.nsINavHistoryService.TRANSITION_LINK;
     let history   = johndoe.createCollection("history");
     for (let i = 0; i < 5; i++) {
       let id = 'record-no--' + i;
       let modified = Date.now()/1000 - 60*(i+10);
 
       let w = new CryptoWrapper("history", "id");
       w.cleartext = {
         id: id,
         histUri: "http://foo/bar?" + id,
         title: id,
         sortindex: i,
         visits: [{date: (modified - 5) * 1000000, type: visitType}],
         deleted: false};
-      w.encrypt();
+      w.encrypt(liveKeys);
 
       let payload = {ciphertext: w.ciphertext,
                      IV:         w.IV,
                      hmac:       w.hmac};
       history.insert(id, payload, modified);
     }
 
     history.timestamp = Date.now() / 1000;
     let old_key_time = johndoe.modified("crypto");
     _("Old key time: " + old_key_time);
 
     // Check that we can decrypt one.
     let rec = new CryptoWrapper("history", "record-no--0");
     rec.fetch(Service.resource(Service.storageURL + "history/record-no--0"));
     _(JSON.stringify(rec));
-    do_check_true(!!rec.decrypt());
+    do_check_true(!!rec.decrypt(liveKeys));
 
     do_check_eq(hmacErrorCount, 0);
 
     // Fill local key cache with bad data.
     corrupt_local_keys();
-    _("Keys now: " + CollectionKeys.keyForCollection("history").keyPair);
+    _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
 
     do_check_eq(hmacErrorCount, 0);
 
     _("HMAC error count: " + hmacErrorCount);
     // Now syncing should succeed, after one HMAC error.
     Service.sync();
     do_check_eq(hmacErrorCount, 1);
-    _("Keys now: " + CollectionKeys.keyForCollection("history").keyPair);
+    _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
 
     // And look! We downloaded history!
     let store = Service.engineManager.get("history")._store;
     do_check_true(store.urlExists("http://foo/bar?record-no--0"));
     do_check_true(store.urlExists("http://foo/bar?record-no--1"));
     do_check_true(store.urlExists("http://foo/bar?record-no--2"));
     do_check_true(store.urlExists("http://foo/bar?record-no--3"));
     do_check_true(store.urlExists("http://foo/bar?record-no--4"));
@@ -160,17 +160,17 @@ add_test(function test_locally_changed_k
       let w = new CryptoWrapper("history", "id");
       w.cleartext = {
         id: id,
         histUri: "http://foo/bar?" + id,
         title: id,
         sortindex: i,
         visits: [{date: (modified - 5 ) * 1000000, type: visitType}],
         deleted: false};
-      w.encrypt();
+      w.encrypt(Service.collectionKeys.keyForCollection("history"));
       w.hmac = w.hmac.toUpperCase();
 
       let payload = {ciphertext: w.ciphertext,
                      IV:         w.IV,
                      hmac:       w.hmac};
       history.insert(id, payload, modified);
     }
     history.timestamp = Date.now() / 1000;
@@ -178,17 +178,17 @@ add_test(function test_locally_changed_k
     _("Server key time hasn't changed.");
     do_check_eq(johndoe.modified("crypto"), old_key_time);
 
     _("Resetting HMAC error timer.");
     Service.lastHMACEvent = 0;
 
     _("Syncing...");
     Service.sync();
-    _("Keys now: " + CollectionKeys.keyForCollection("history").keyPair);
+    _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
     _("Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history.");
     do_check_true(johndoe.modified("crypto") > old_key_time);
     do_check_eq(hmacErrorCount, 6);
     do_check_false(store.urlExists("http://foo/bar?record-no--5"));
     do_check_false(store.urlExists("http://foo/bar?record-no--6"));
     do_check_false(store.urlExists("http://foo/bar?record-no--7"));
     do_check_false(store.urlExists("http://foo/bar?record-no--8"));
     do_check_false(store.urlExists("http://foo/bar?record-no--9"));
--- a/services/sync/tests/unit/test_engine_abort.js
+++ b/services/sync/tests/unit/test_engine_abort.js
@@ -3,17 +3,17 @@
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 add_test(function test_processIncoming_abort() {
   _("An abort exception, raised in applyIncoming, will abort _processIncoming.");
   new SyncTestingInfrastructure();
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let engine = new RotaryEngine(Service);
 
   _("Create some server data.");
   let meta_global = Service.recordManager.set(engine.metaURL,
                                               new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                           syncID: engine.syncID}};
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -54,17 +54,17 @@ function run_test() {
 
   run_next_test();
 }
 
 function generateCredentialsChangedFailure() {
   // Make sync fail due to changed credentials. We simply re-encrypt
   // the keys with a different Sync Key, without changing the local one.
   let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
-  let keys = CollectionKeys.asWBO();
+  let keys = Service.collectionKeys.asWBO();
   keys.encrypt(newSyncKeyBundle);
   keys.upload(Service.resource(Service.cryptoKeysURL));
 }
 
 function service_unavailable(request, response) {
   let body = "Service Unavailable";
   response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
   response.setHeader("Retry-After", "42");
@@ -126,18 +126,18 @@ function setUp() {
   setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   return generateAndUploadKeys();
 }
 
 function generateAndUploadKeys() {
-  generateNewKeys();
-  let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+  generateNewKeys(Service.collectionKeys);
+  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.identity.syncKeyBundle);
   return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
 }
 
 function clean() {
   Service.startOver();
   Status.resetSync();
   Status.resetBackoff();
@@ -860,17 +860,17 @@ add_test(function test_crypto_keys_login
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   function onUIUpdate() {
@@ -994,17 +994,17 @@ add_test(function test_download_crypto_k
   // Test crypto/keys prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
@@ -1233,17 +1233,17 @@ add_test(function test_download_crypto_k
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
@@ -1472,17 +1472,17 @@ add_test(function test_download_crypto_k
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
   setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
-  CollectionKeys.clear();
+  Service.collectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
--- a/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
+++ b/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
@@ -54,18 +54,18 @@ function sync_httpd_setup() {
 function setUp() {
   setBasicCredentials("johndoe", "ilovejane", "aabcdeabcdeabcdeabcdeabcde");
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   new FakeCryptoService();
 }
 
 function generateAndUploadKeys() {
-  generateNewKeys();
-  let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+  generateNewKeys(Service.collectionKeys);
+  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.identity.syncKeyBundle);
   let res = Service.resource("http://localhost:8080/1.1/johndoe/storage/crypto/keys");
   return serverKeys.upload(res).success;
 }
 
 
 add_test(function test_backoff500() {
   _("Test: HTTP 500 sets backoff status.");
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -130,12 +130,12 @@ add_test(function test_processIncoming_m
     PlacesUtils.history.removeAllPages();
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Service.recordManager.clearCache();
   }
 });
 
 function run_test() {
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   run_next_test();
 }
--- a/services/sync/tests/unit/test_hmac_error.js
+++ b/services/sync/tests/unit/test_hmac_error.js
@@ -78,17 +78,17 @@ add_test(function hmac_error_during_404(
   };
 
   let server = sync_httpd_setup(handlers);
 
   try {
     _("Syncing.");
     Service.sync();
     _("Partially resetting client, as if after a restart, and forcing redownload.");
-    CollectionKeys.clear();
+    Service.collectionKeys.clear();
     engine.lastSync = 0;        // So that we redownload records.
     key404Counter = 1;
     _("---------------------------");
     Service.sync();
     _("---------------------------");
 
     // Two rotary items, one client record... no errors.
     do_check_eq(hmacErrorCount, 0)
@@ -208,17 +208,17 @@ add_test(function hmac_error_during_node
       hasKeys = keysWBO.modified;
       do_check_true(!hasData == !hasKeys);
 
       // Kick off another sync. Can't just call it, because we're inside the
       // lock...
       Utils.nextTick(function() {
         _("Now a fresh sync will get no HMAC errors.");
         _("Partially resetting client, as if after a restart, and forcing redownload.");
-        CollectionKeys.clear();
+        Service.collectionKeys.clear();
         engine.lastSync = 0;
         hmacErrorCount = 0;
 
         onSyncFinished = function() {
           // Two rotary items, one client record... no errors.
           do_check_eq(hmacErrorCount, 0)
 
           Svc.Obs.remove("weave:service:sync:finish", obs);
--- a/services/sync/tests/unit/test_interval_triggers.js
+++ b/services/sync/tests/unit/test_interval_triggers.js
@@ -33,18 +33,18 @@ function sync_httpd_setup() {
   });
 }
 
 function setUp() {
   setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
-  generateNewKeys();
-  let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+  generateNewKeys(Service.collectionKeys);
+  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.identity.syncKeyBundle);
   return serverKeys.upload(Service.resource(Service.cryptoKeysURL));
 }
 
 function run_test() {
   initTestLogging("Trace");
 
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
--- a/services/sync/tests/unit/test_keys.js
+++ b/services/sync/tests/unit/test_keys.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 
+let collectionKeys = new CollectionKeyManager();
+
 function sha256HMAC(message, key) {
   let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key);
   return Utils.digestBytes(message, h);
 }
 
 function do_check_array_eq(a1, a2) {
   do_check_eq(a1.length, a2.length);
   for (let i = 0; i < a1.length; ++i) {
@@ -202,81 +204,81 @@ add_test(function test_collections_manag
 
   // Use passphrase (sync key) itself to encrypt the key bundle.
   storage_keys.encrypt(keyBundle);
 
   // Sanity checking.
   do_check_true(null == storage_keys.cleartext);
   do_check_true(null != storage_keys.ciphertext);
 
-  log.info("Updating CollectionKeys.");
+  log.info("Updating collection keys.");
 
   // updateContents decrypts the object, releasing the payload for us to use.
   // Returns true, because the default key has changed.
-  do_check_true(CollectionKeys.updateContents(keyBundle, storage_keys));
+  do_check_true(collectionKeys.updateContents(keyBundle, storage_keys));
   let payload = storage_keys.cleartext;
 
-  _("CK: " + JSON.stringify(CollectionKeys._collections));
+  _("CK: " + JSON.stringify(collectionKeys._collections));
 
   // Test that the CollectionKeyManager returns a similar WBO.
-  let wbo = CollectionKeys.asWBO("crypto", "keys");
+  let wbo = collectionKeys.asWBO("crypto", "keys");
 
   _("WBO: " + JSON.stringify(wbo));
   _("WBO cleartext: " + JSON.stringify(wbo.cleartext));
 
   // Check the individual contents.
   do_check_eq(wbo.collection, "crypto");
   do_check_eq(wbo.id, "keys");
   do_check_eq(undefined, wbo.modified);
-  do_check_eq(CollectionKeys.lastModified, storage_keys.modified);
+  do_check_eq(collectionKeys.lastModified, storage_keys.modified);
   do_check_true(!!wbo.cleartext.default);
   do_check_keypair_eq(payload.default, wbo.cleartext.default);
   do_check_keypair_eq(payload.collections.bookmarks, wbo.cleartext.collections.bookmarks);
 
-  do_check_true('bookmarks' in CollectionKeys._collections);
-  do_check_false('tabs' in CollectionKeys._collections);
+  do_check_true('bookmarks' in collectionKeys._collections);
+  do_check_false('tabs' in collectionKeys._collections);
 
   _("Updating contents twice with the same data doesn't proceed.");
   storage_keys.encrypt(keyBundle);
-  do_check_false(CollectionKeys.updateContents(keyBundle, storage_keys));
+  do_check_false(collectionKeys.updateContents(keyBundle, storage_keys));
 
   /*
    * Test that we get the right keys out when we ask for
    * a collection's tokens.
    */
   let b1 = new BulkKeyBundle("bookmarks");
   b1.keyPairB64 = [bookmarks_key64, bookmarks_hmac64];
-  let b2 = CollectionKeys.keyForCollection("bookmarks");
+  let b2 = collectionKeys.keyForCollection("bookmarks");
   do_check_keypair_eq(b1.keyPair, b2.keyPair);
 
   // Check key equality.
   do_check_true(b1.equals(b2));
   do_check_true(b2.equals(b1));
 
   b1 = new BulkKeyBundle("[default]");
   b1.keyPairB64 = [default_key64, default_hmac64];
 
   do_check_false(b1.equals(b2));
   do_check_false(b2.equals(b1));
 
-  b2 = CollectionKeys.keyForCollection(null);
+  b2 = collectionKeys.keyForCollection(null);
   do_check_keypair_eq(b1.keyPair, b2.keyPair);
 
   /*
    * Checking for update times.
    */
   let info_collections = {};
-  do_check_true(CollectionKeys.updateNeeded(info_collections));
+  do_check_true(collectionKeys.updateNeeded(info_collections));
   info_collections["crypto"] = 5000;
-  do_check_false(CollectionKeys.updateNeeded(info_collections));
+  do_check_false(collectionKeys.updateNeeded(info_collections));
   info_collections["crypto"] = 1 + (Date.now()/1000);              // Add one in case computers are fast!
-  do_check_true(CollectionKeys.updateNeeded(info_collections));
+  do_check_true(collectionKeys.updateNeeded(info_collections));
 
-  CollectionKeys.lastModified = null;
-  do_check_true(CollectionKeys.updateNeeded({}));
+  collectionKeys.lastModified = null;
+  do_check_true(collectionKeys.updateNeeded({}));
 
   /*
    * Check _compareKeyBundleCollections.
    */
   function newBundle(name) {
     let r = new BulkKeyBundle(name);
     r.generateRandom();
     return r;
@@ -288,24 +290,24 @@ add_test(function test_collections_manag
   let k5 = newBundle("k5");
   let coll1 = {"foo": k1, "bar": k2};
   let coll2 = {"foo": k1, "bar": k2};
   let coll3 = {"foo": k1, "bar": k3};
   let coll4 = {"foo": k4};
   let coll5 = {"baz": k5, "bar": k2};
   let coll6 = {};
 
-  let d1 = CollectionKeys._compareKeyBundleCollections(coll1, coll2); // []
-  let d2 = CollectionKeys._compareKeyBundleCollections(coll1, coll3); // ["bar"]
-  let d3 = CollectionKeys._compareKeyBundleCollections(coll3, coll2); // ["bar"]
-  let d4 = CollectionKeys._compareKeyBundleCollections(coll1, coll4); // ["bar", "foo"]
-  let d5 = CollectionKeys._compareKeyBundleCollections(coll5, coll2); // ["baz", "foo"]
-  let d6 = CollectionKeys._compareKeyBundleCollections(coll6, coll1); // ["bar", "foo"]
-  let d7 = CollectionKeys._compareKeyBundleCollections(coll5, coll5); // []
-  let d8 = CollectionKeys._compareKeyBundleCollections(coll6, coll6); // []
+  let d1 = collectionKeys._compareKeyBundleCollections(coll1, coll2); // []
+  let d2 = collectionKeys._compareKeyBundleCollections(coll1, coll3); // ["bar"]
+  let d3 = collectionKeys._compareKeyBundleCollections(coll3, coll2); // ["bar"]
+  let d4 = collectionKeys._compareKeyBundleCollections(coll1, coll4); // ["bar", "foo"]
+  let d5 = collectionKeys._compareKeyBundleCollections(coll5, coll2); // ["baz", "foo"]
+  let d6 = collectionKeys._compareKeyBundleCollections(coll6, coll1); // ["bar", "foo"]
+  let d7 = collectionKeys._compareKeyBundleCollections(coll5, coll5); // []
+  let d8 = collectionKeys._compareKeyBundleCollections(coll6, coll6); // []
 
   do_check_true(d1.same);
   do_check_false(d2.same);
   do_check_false(d3.same);
   do_check_false(d4.same);
   do_check_false(d5.same);
   do_check_false(d6.same);
   do_check_true(d7.same);
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -105,50 +105,50 @@ function run_test() {
     }
     catch(ex) {
       error = ex;
     }
     do_check_eq(error.substr(0, 42), "Record SHA256 HMAC mismatch: should be foo");
 
     // Checking per-collection keys and default key handling.
 
-    generateNewKeys();
+    generateNewKeys(Service.collectionKeys);
     let bu = "http://localhost:8080/storage/bookmarks/foo";
     let bookmarkItem = prepareCryptoWrap("bookmarks", "foo");
-    bookmarkItem.encrypt();
+    bookmarkItem.encrypt(Service.collectionKeys.keyForCollection("bookmarks"));
     log.info("Ciphertext is " + bookmarkItem.ciphertext);
     do_check_true(bookmarkItem.ciphertext != null);
     log.info("Decrypting the record explicitly with the default key.");
-    do_check_eq(bookmarkItem.decrypt(CollectionKeys._default).stuff, "my payload here");
+    do_check_eq(bookmarkItem.decrypt(Service.collectionKeys._default).stuff, "my payload here");
 
     // Per-collection keys.
     // Generate a key for "bookmarks".
-    generateNewKeys(["bookmarks"]);
+    generateNewKeys(Service.collectionKeys, ["bookmarks"]);
     bookmarkItem = prepareCryptoWrap("bookmarks", "foo");
     do_check_eq(bookmarkItem.collection, "bookmarks");
 
     // Encrypt. This'll use the "bookmarks" encryption key, because we have a
     // special key for it. The same key will need to be used for decryption.
-    bookmarkItem.encrypt();
+    bookmarkItem.encrypt(Service.collectionKeys.keyForCollection("bookmarks"));
     do_check_true(bookmarkItem.ciphertext != null);
 
     // Attempt to use the default key, because this is a collision that could
     // conceivably occur in the real world. Decryption will error, because
     // it's not the bookmarks key.
     let err;
     try {
-      bookmarkItem.decrypt(CollectionKeys._default);
+      bookmarkItem.decrypt(Service.collectionKeys._default);
     } catch (ex) {
       err = ex;
     }
     do_check_eq("Record SHA256 HMAC mismatch", err.substr(0, 27));
 
     // Explicitly check that it's using the bookmarks key.
     // This should succeed.
-    do_check_eq(bookmarkItem.decrypt(CollectionKeys.keyForCollection("bookmarks")).stuff,
+    do_check_eq(bookmarkItem.decrypt(Service.collectionKeys.keyForCollection("bookmarks")).stuff,
         "my payload here");
 
     log.info("Done!");
   }
   finally {
     server.stop(do_test_finished);
   }
 }
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -95,17 +95,17 @@ add_test(function v4_upgrade() {
     // Same should happen after a wipe.
     _("Syncing after server has been upgraded and wiped.");
     Service.wipeServer();
     test_out_of_date();
 
     // Now's a great time to test what happens when keys get replaced.
     _("Syncing afresh...");
     Service.logout();
-    CollectionKeys.clear();
+    Service.collectionKeys.clear();
     Service.serverURL = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
     meta_global.payload = JSON.stringify({"syncID": "foooooooooooooobbbbbbbbbbbb",
                                           "storageVersion": STORAGE_VERSION});
     collections.meta = Date.now() / 1000;
     Service.recordManager.set(Service.metaURL, meta_global);
     Service.login("johndoe", "ilovejane", passphrase);
     do_check_true(Service.isLoggedIn);
@@ -128,17 +128,17 @@ add_test(function v4_upgrade() {
       _("Retrieved WBO:       " + JSON.stringify(serverDecrypted));
       _("serverKeys:          " + JSON.stringify(serverKeys));
 
       return serverDecrypted.default;
     }
 
     function retrieve_and_compare_default(should_succeed) {
       let serverDefault = retrieve_server_default();
-      let localDefault = CollectionKeys.keyForCollection().keyPairB64;
+      let localDefault = Service.collectionKeys.keyForCollection().keyPairB64;
 
       _("Retrieved keyBundle: " + JSON.stringify(serverDefault));
       _("Local keyBundle:     " + JSON.stringify(localDefault));
 
       if (should_succeed)
         do_check_eq(JSON.stringify(serverDefault), JSON.stringify(localDefault));
       else
         do_check_neq(JSON.stringify(serverDefault), JSON.stringify(localDefault));
@@ -240,18 +240,18 @@ add_test(function v5_upgrade() {
     setBasicCredentials("johndoe", "ilovejane", passphrase);
     Service.serverURL = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
 
     // Test an upgrade where the contents of the server would cause us to error
     // -- keys decrypted with a different sync key, for example.
     _("Testing v4 -> v5 (or similar) upgrade.");
     function update_server_keys(syncKeyBundle, wboName, collWBO) {
-      generateNewKeys();
-      serverKeys = CollectionKeys.asWBO("crypto", wboName);
+      generateNewKeys(Service.collectionKeys);
+      serverKeys = Service.collectionKeys.asWBO("crypto", wboName);
       serverKeys.encrypt(syncKeyBundle);
       let res = Service.resource(Service.storageURL + collWBO);
       do_check_true(serverKeys.upload(res).success);
     }
 
     _("Bumping version.");
     // Bump version on the server.
     let m = new WBORecord("meta", "global");
@@ -262,17 +262,17 @@ add_test(function v5_upgrade() {
     _("New meta/global: " + JSON.stringify(meta_global));
 
     // Fill the keys with bad data.
     let badKeys = new SyncKeyBundle("foobar", "aaaaaaaaaaaaaaaaaaaaaaaaaa");
     update_server_keys(badKeys, "keys", "crypto/keys");  // v4
     update_server_keys(badKeys, "bulk", "crypto/bulk");  // v5
 
     _("Generating new keys.");
-    generateNewKeys();
+    generateNewKeys(Service.collectionKeys);
 
     // Now sync and see what happens. It should be a version fail, not a crypto
     // fail.
 
     _("Logging in.");
     try {
       Service.login("johndoe", "ilovejane", passphrase);
     }
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -128,35 +128,35 @@ function run_test() {
     do_check_false(Service.verifyAndFetchSymmetricKeys());
     do_check_eq(Status.sync, CREDENTIALS_CHANGED);
     do_check_eq(Status.login, LOGIN_FAILED_INVALID_PASSPHRASE);
     Service.identity.syncKey = pp;
     do_check_true(Service.verifyAndFetchSymmetricKeys());
 
     // changePassphrase wipes our keys, and they're regenerated on next sync.
     _("Checking changed passphrase.");
-    let existingDefault = CollectionKeys.keyForCollection();
+    let existingDefault = Service.collectionKeys.keyForCollection();
     let existingKeysPayload = keysWBO.payload;
     let newPassphrase = "bbbbbabcdeabcdeabcdeabcdea";
     Service.changePassphrase(newPassphrase);
 
     _("Local key cache is full, but different.");
-    do_check_true(!!CollectionKeys._default);
-    do_check_false(CollectionKeys._default.equals(existingDefault));
+    do_check_true(!!Service.collectionKeys._default);
+    do_check_false(Service.collectionKeys._default.equals(existingDefault));
 
     _("Server has new keys.");
     do_check_true(!!keysWBO.payload);
     do_check_true(!!keysWBO.modified);
     do_check_neq(keysWBO.payload, existingKeysPayload);
 
     // Try to screw up HMAC calculation.
     // Re-encrypt keys with a new random keybundle, and upload them to the
     // server, just as might happen with a second client.
     _("Attempting to screw up HMAC by re-encrypting keys.");
-    let keys = CollectionKeys.asWBO();
+    let keys = Service.collectionKeys.asWBO();
     let b = new BulkKeyBundle("hmacerror");
     b.generateRandom();
     collections.crypto = keys.modified = 100 + (Date.now()/1000);  // Future modification time.
     keys.encrypt(b);
     keys.upload(Service.resource(Service.cryptoKeysURL));
 
     do_check_false(Service.verifyAndFetchSymmetricKeys());
     do_check_eq(Status.login, LOGIN_FAILED_INVALID_PASSPHRASE);
--- a/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
+++ b/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
@@ -66,18 +66,18 @@ function sync_httpd_setup(handlers) {
   return httpd_setup(handlers);
 }
 
 function setUp() {
   new SyncTestingInfrastructure("johndoe", "ilovejane",
                                 "abcdeabcdeabcdeabcdeabcdea");
   // Ensure that the server has valid keys so that logging in will work and not
   // result in a server wipe, rendering many of these tests useless.
-  generateNewKeys();
-  let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+  generateNewKeys(Service.collectionKeys);
+  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.identity.syncKeyBundle);
   return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
 }
 
 const PAYLOAD = 42;
 
 
 function run_test() {
@@ -249,17 +249,17 @@ add_test(function test_enabledRemotely()
     upd("steam", new ServerWBO("steam", {}).handler())
   });
   setUp();
 
   // We need to be very careful how we do this, so that we don't trigger a
   // fresh start!
   try {
     _("Upload some keys to avoid a fresh start.");
-    let wbo = CollectionKeys.generateNewKeysWBO();
+    let wbo = Service.collectionKeys.generateNewKeysWBO();
     wbo.encrypt(Service.identity.syncKeyBundle);
     do_check_eq(200, wbo.upload(Service.resource(Service.cryptoKeysURL)).status);
 
     _("Engine is disabled.");
     do_check_false(engine.enabled);
 
     _("Sync.");
     Service.sync();
--- a/services/sync/tests/unit/test_service_wipeClient.js
+++ b/services/sync/tests/unit/test_service_wipeClient.js
@@ -64,20 +64,20 @@ add_test(function test_withEngineList() 
     Service.engineManager.get("cannotdecrypt").wasWiped = false;
     Service.startOver();
   }
 
   run_next_test();
 });
 
 add_test(function test_startOver_clears_keys() {
-  generateNewKeys();
-  do_check_true(!!CollectionKeys.keyForCollection());
+  generateNewKeys(Service.collectionKeys);
+  do_check_true(!!Service.collectionKeys.keyForCollection());
   Service.startOver();
-  do_check_false(!!CollectionKeys.keyForCollection());
+  do_check_false(!!Service.collectionKeys.keyForCollection());
 
   run_next_test();
 });
 
 add_test(function test_credentials_preserved() {
   _("Ensure that credentials are preserved if client is wiped.");
 
   // Required for wipeClient().
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -45,17 +45,17 @@ function createServerAndConfigureClient(
   server.registerUser(USER, "password");
   server.createContents(USER, contents);
   server.start();
 
   return [engine, server, USER];
 }
 
 function run_test() {
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
   Svc.Prefs.set("log.logger.engine.rotary", "Trace");
   run_next_test();
 }
 
 /*
  * Tests
  *
  * SyncEngine._sync() is divided into four rather independent steps:
@@ -221,17 +221,17 @@ add_test(function test_processIncoming_e
 add_test(function test_processIncoming_createFromServer() {
   _("SyncEngine._processIncoming creates new records from server data");
 
   let syncTesting = new SyncTestingInfrastructure();
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   Service.identity.username = "foo";
 
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   // Some server records that will be downloaded
   let collection = new ServerCollection();
   collection.insert('flying',
                     encryptPayload({id: 'flying',
                                     denomination: "LNER Class A3 4472"}));
   collection.insert('scotsman',
                     encryptPayload({id: 'scotsman',
@@ -1324,17 +1324,17 @@ add_test(function test_uploadOutgoing_to
   collection._wbos.flying = new ServerWBO('flying');
   collection._wbos.scotsman = new ServerWBO('scotsman');
 
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler(),
       "/1.1/foo/storage/rotary/flying": collection.wbo("flying").handler(),
       "/1.1/foo/storage/rotary/scotsman": collection.wbo("scotsman").handler()
   });
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let engine = makeRotaryEngine();
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine._store.items = {flying: "LNER Class A3 4472",
                          scotsman: "Flying Scotsman"};
   // Mark one of these records as changed
   engine._tracker.addChangedID('scotsman', 0);
 
@@ -1631,17 +1631,17 @@ add_test(function test_sync_partialUploa
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   Service.identity.username = "foo";
 
   let collection = new ServerCollection();
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler()
   });
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let engine = makeRotaryEngine();
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine.lastSyncLocal = 456;
 
   // Let the third upload fail completely
   var noOfUploads = 0;
   collection.post = (function(orig) {
@@ -1702,18 +1702,18 @@ add_test(function test_sync_partialUploa
 
 add_test(function test_canDecrypt_noCryptoKeys() {
   _("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key collection.");
   let syncTesting = new SyncTestingInfrastructure();
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   Service.identity.username = "foo";
 
-  // Wipe CollectionKeys so we can test the desired scenario.
-  CollectionKeys.clear();
+  // Wipe collection keys so we can test the desired scenario.
+  Service.collectionKeys.clear();
 
   let collection = new ServerCollection();
   collection._wbos.flying = new ServerWBO(
       'flying', encryptPayload({id: 'flying',
                                 denomination: "LNER Class A3 4472"}));
 
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler()
@@ -1731,18 +1731,17 @@ add_test(function test_canDecrypt_noCryp
 
 add_test(function test_canDecrypt_true() {
   _("SyncEngine.canDecrypt returns true if the engine can decrypt the items on the server.");
   let syncTesting = new SyncTestingInfrastructure();
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   Service.identity.username = "foo";
 
-  // Set up CollectionKeys, as service.js does.
-  generateNewKeys();
+  generateNewKeys(Service.collectionKeys);
 
   let collection = new ServerCollection();
   collection._wbos.flying = new ServerWBO(
       'flying', encryptPayload({id: 'flying',
                                 denomination: "LNER Class A3 4472"}));
 
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler()
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -49,18 +49,18 @@ function sync_httpd_setup() {
     "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "null")
   });
 }
 
 function setUp() {
   setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.clusterURL = TEST_CLUSTER_URL;
 
-  generateNewKeys();
-  let serverKeys = CollectionKeys.asWBO("crypto", "keys");
+  generateNewKeys(Service.collectionKeys);
+  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
   serverKeys.encrypt(Service.identity.syncKeyBundle);
   return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
 }
 
 function cleanUpAndGo(server) {
   Utils.nextTick(function () {
     Service.startOver();
     if (server) {
@@ -755,17 +755,17 @@ add_test(function test_sync_X_Weave_Back
     infoColl(request, response);
   }
   server.registerPathHandler(INFO_COLLECTIONS, infoCollWithBackoff);
 
   // Pretend we have two clients so that the regular sync interval is
   // sufficiently low.
   clientsEngine._store.create({id: "foo", cleartext: "bar"});
   let rec = clientsEngine._store.createRecord("foo", "clients");
-  rec.encrypt();
+  rec.encrypt(Service.collectionKeys.keyForCollection("clients"));
   rec.upload(Service.resource(clientsEngine.engineURL + rec.id));
 
   // Sync once to log in and get everything set up. Let's verify our initial
   // values.
   Service.sync();
   do_check_eq(Status.backoffInterval, 0);
   do_check_eq(Status.minimumNextSync, 0);
   do_check_eq(scheduler.syncInterval, scheduler.activeInterval);
@@ -812,17 +812,17 @@ add_test(function test_sync_503_Retry_Af
     response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
   }
   server.registerPathHandler(INFO_COLLECTIONS, infoCollWithMaintenance);
 
   // Pretend we have two clients so that the regular sync interval is
   // sufficiently low.
   clientsEngine._store.create({id: "foo", cleartext: "bar"});
   let rec = clientsEngine._store.createRecord("foo", "clients");
-  rec.encrypt();
+  rec.encrypt(Service.collectionKeys.keyForCollection("clients"));
   rec.upload(Service.resource(clientsEngine.engineURL + rec.id));
 
   // Sync once to log in and get everything set up. Let's verify our initial
   // values.
   Service.sync();
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.backoffInterval, 0);
   do_check_eq(Status.minimumNextSync, 0);