Bug 603489, 614489: simplified crypto. r=mconnor
authorRichard Newman <rnewman@mozilla.com>
Mon, 29 Nov 2010 16:41:17 -0800
changeset 58410 67138fbf6544906ffb7008b9d12bcefda45ad9d0
parent 58409 9d7a96749d81820085f3a2598df723409285d18b
child 58411 49607c89698bf81803de7dd1e3300150285b810e
push id1
push usershaver@mozilla.com
push dateTue, 04 Jan 2011 17:58:04 +0000
reviewersmconnor
bugs603489, 614489
Bug 603489, 614489: simplified crypto. r=mconnor
services/crypto/modules/WeaveCrypto.js
services/crypto/tests/unit/test_crypto_deriveKey.js
services/crypto/tests/unit/test_crypto_keypair.js
services/crypto/tests/unit/test_crypto_rewrap.js
services/crypto/tests/unit/test_crypto_verify.js
services/sync/modules/base_records/collection.js
services/sync/modules/base_records/crypto.js
services/sync/modules/base_records/keys.js
services/sync/modules/base_records/wbo.js
services/sync/modules/constants.js
services/sync/modules/engines.js
services/sync/modules/engines/bookmarks.js
services/sync/modules/engines/clients.js
services/sync/modules/engines/forms.js
services/sync/modules/engines/history.js
services/sync/modules/engines/passwords.js
services/sync/modules/engines/prefs.js
services/sync/modules/engines/tabs.js
services/sync/modules/identity.js
services/sync/modules/main.js
services/sync/modules/service.js
services/sync/modules/status.js
services/sync/modules/stores.js
services/sync/modules/type_records/bookmark.js
services/sync/modules/type_records/clients.js
services/sync/modules/type_records/forms.js
services/sync/modules/type_records/history.js
services/sync/modules/type_records/passwords.js
services/sync/modules/type_records/prefs.js
services/sync/modules/type_records/tabs.js
services/sync/modules/util.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/test_bookmark_predecessor.js
services/sync/tests/unit/test_bookmark_record.js
services/sync/tests/unit/test_bookmark_store.js
services/sync/tests/unit/test_clients_escape.js
services/sync/tests/unit/test_collection_inc_get.js
services/sync/tests/unit/test_forms_store.js
services/sync/tests/unit/test_history_store.js
services/sync/tests/unit/test_keys.js
services/sync/tests/unit/test_load_modules.js
services/sync/tests/unit/test_records_crypto.js
services/sync/tests/unit/test_records_crypto_generateEntry.js
services/sync/tests/unit/test_records_cryptometa.js
services/sync/tests/unit/test_records_keys.js
services/sync/tests/unit/test_records_wbo.js
services/sync/tests/unit/test_service_attributes.js
services/sync/tests/unit/test_service_login.js
services/sync/tests/unit/test_service_passphraseUTF8.js
services/sync/tests/unit/test_service_passwordUTF8.js
services/sync/tests/unit/test_service_persistLogin.js
services/sync/tests/unit/test_service_sync_401.js
services/sync/tests/unit/test_service_sync_checkServerError.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_verifyLogin.js
services/sync/tests/unit/test_service_wipeServer.js
services/sync/tests/unit/test_status_checkSetup.js
services/sync/tests/unit/test_syncengine.js
services/sync/tests/unit/test_syncengine_sync.js
services/sync/tests/unit/test_tab_store.js
services/sync/tests/unit/test_utils_atob.js
services/sync/tests/unit/test_utils_deriveKey.js
services/sync/tests/unit/test_utils_encodeBase32.js
services/sync/tests/unit/test_utils_lock.js
services/sync/tests/unit/test_utils_passphrase.js
--- a/services/crypto/modules/WeaveCrypto.js
+++ b/services/crypto/modules/WeaveCrypto.js
@@ -14,16 +14,17 @@
  * The Original Code is mozilla.org code.
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Justin Dolske <dolske@mozilla.com> (original author)
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -230,16 +231,17 @@ WeaveCrypto.prototype = {
         this.nss.CKA_UNWRAP  = 0x107;
 
         // security/nss/lib/softoken/secmodt.h
         this.nss.PK11_ATTR_SESSION   = 0x02;
         this.nss.PK11_ATTR_PUBLIC    = 0x08;
         this.nss.PK11_ATTR_SENSITIVE = 0x40;
 
         // security/nss/lib/util/secoidt.h
+        this.nss.SEC_OID_PKCS5_PBKDF2         = 291;
         this.nss.SEC_OID_HMAC_SHA1            = 294;
         this.nss.SEC_OID_PKCS1_RSA_ENCRYPTION = 16;
 
 
         // security/nss/lib/pk11wrap/pk11pub.h#286
         // SECStatus PK11_GenerateRandom(unsigned char *data,int len);
         this.nss.PK11_GenerateRandom = nsslib.declare("PK11_GenerateRandom",
                                                       ctypes.default_abi, this.nss_t.SECStatus,
@@ -447,18 +449,16 @@ WeaveCrypto.prototype = {
 
     //
     // IWeaveCrypto interfaces
     //
 
 
     algorithm : Ci.IWeaveCrypto.AES_256_CBC,
 
-    keypairBits : 2048,
-
     encrypt : function(clearTextUCS2, symmetricKey, iv) {
         this.log("encrypt() called");
 
         // js-ctypes autoconverts to a UTF8 buffer, but also includes a null
         // at the end which we don't want. Cast to make the length 1 byte shorter.
         let inputBuffer = new ctypes.ArrayType(ctypes.unsigned_char)(clearTextUCS2);
         inputBuffer = ctypes.cast(inputBuffer, ctypes.unsigned_char.array(inputBuffer.length - 1));
 
@@ -562,72 +562,16 @@ WeaveCrypto.prototype = {
             if (slot && !slot.isNull())
                 this.nss.PK11_FreeSlot(slot);
             if (ivParam && !ivParam.isNull())
                 this.nss.SECITEM_FreeItem(ivParam, true);
         }
     },
 
 
-    generateKeypair : function(passphrase, salt, iv, out_encodedPublicKey, out_wrappedPrivateKey) {
-        this.log("generateKeypair() called.");
-
-        let pubKey, privKey, slot;
-        try {
-            // Attributes for the private key. We're just going to wrap and extract the
-            // value, so they're not critical. The _PUBLIC attribute just indicates the
-            // object can be accessed without being logged into the token.
-            let attrFlags = (this.nss.PK11_ATTR_SESSION | this.nss.PK11_ATTR_PUBLIC | this.nss.PK11_ATTR_SENSITIVE);
-
-            pubKey  = new this.nss_t.SECKEYPublicKey.ptr();
-
-            let rsaParams = new this.nss_t.PK11RSAGenParams();
-            rsaParams.keySizeInBits = this.keypairBits; // 1024, 2048, etc.
-            rsaParams.pe = 65537;                       // public exponent.
-
-            slot = this.nss.PK11_GetInternalSlot();
-            if (slot.isNull())
-                throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE);
-
-            // Generate the keypair.
-            privKey = this.nss.PK11_GenerateKeyPairWithFlags(slot,
-                                                             this.nss.CKM_RSA_PKCS_KEY_PAIR_GEN,
-                                                             rsaParams.address(),
-                                                             pubKey.address(),
-                                                             attrFlags, null);
-            if (privKey.isNull())
-                throw Components.Exception("keypair generation failed", Cr.NS_ERROR_FAILURE);
-            
-            let s = this.nss.PK11_SetPrivateKeyNickname(privKey, "Weave User PrivKey");
-            if (s)
-                throw Components.Exception("key nickname failed", Cr.NS_ERROR_FAILURE);
-
-            let wrappedPrivateKey = this._wrapPrivateKey(privKey, passphrase, salt, iv);
-            out_wrappedPrivateKey.value = wrappedPrivateKey; // outparam
-
-            let derKey = this.nss.SECKEY_EncodeDERSubjectPublicKeyInfo(pubKey);
-            if (derKey.isNull())
-              throw Components.Exception("SECKEY_EncodeDERSubjectPublicKeyInfo failed", Cr.NS_ERROR_FAILURE);
-
-            let encodedPublicKey = this.encodeBase64(derKey.contents.data, derKey.contents.len);
-            out_encodedPublicKey.value = encodedPublicKey; // outparam
-        } catch (e) {
-            this.log("generateKeypair: failed: " + e);
-            throw e;
-        } finally {
-            if (pubKey && !pubKey.isNull())
-                this.nss.SECKEY_DestroyPublicKey(pubKey);
-            if (privKey && !privKey.isNull())
-                this.nss.SECKEY_DestroyPrivateKey(privKey);
-            if (slot && !slot.isNull())
-                this.nss.PK11_FreeSlot(slot);
-        }
-    },
-
-
     generateRandomKey : function() {
         this.log("generateRandomKey() called");
         let encodedKey, keygenMech, keySize;
 
         // Doesn't NSS have a lookup function to do this?
         switch(this.algorithm) {
           case Ci.IWeaveCrypto.AES_128_CBC:
             keygenMech = this.nss.CKM_AES_KEY_GEN;
@@ -697,290 +641,16 @@ WeaveCrypto.prototype = {
         let scratch = new ctypes.ArrayType(ctypes.unsigned_char, byteCount)();
         if (this.nss.PK11_GenerateRandom(scratch, byteCount))
             throw Components.Exception("PK11_GenrateRandom failed", Cr.NS_ERROR_FAILURE);
 
         return this.encodeBase64(scratch.address(), scratch.length);
     },
 
 
-    wrapSymmetricKey : function(symmetricKey, encodedPublicKey) {
-        this.log("wrapSymmetricKey() called");
-
-        // Step 1. Get rid of the base64 encoding on the inputs.
-
-        let pubKeyData = this.makeSECItem(encodedPublicKey, true);
-        let symKeyData = this.makeSECItem(symmetricKey, true);
-
-        // This buffer is much larger than needed, but that's ok.
-        let keyData = new ctypes.ArrayType(ctypes.unsigned_char, 4096)();
-        let wrappedKey = new this.nss_t.SECItem(this.nss.SIBUFFER, keyData, keyData.length);
-
-        // Step 2. Put the symmetric key bits into a P11 key object.
-        let slot, symKey, pubKeyInfo, pubKey;
-        try {
-            slot = this.nss.PK11_GetInternalSlot();
-            if (slot.isNull())
-                throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE);
-
-            // ImportSymKey wants a mechanism, from which it derives the key type.
-            let keyMech = this.nss.PK11_AlgtagToMechanism(this.algorithm);
-
-            // This imports a key with the usage set for encryption, but that doesn't
-            // really matter because we're just going to wrap it up and not use it.
-            symKey = this.nss.PK11_ImportSymKey(slot, keyMech, this.nss.PK11_OriginUnwrap, this.nss.CKA_ENCRYPT, symKeyData.address(), null);
-            if (symKey.isNull())
-                throw Components.Exception("symkey import failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 3. Put the public key bits into a P11 key object.
-
-            // Can't just do this directly, it's expecting a minimal ASN1 blob
-            // pubKey = SECKEY_ImportDERPublicKey(&pubKeyData, CKK_RSA);
-            pubKeyInfo = this.nss.SECKEY_DecodeDERSubjectPublicKeyInfo(pubKeyData.address());
-            if (pubKeyInfo.isNull())
-                throw Components.Exception("SECKEY_DecodeDERSubjectPublicKeyInfo failed", Cr.NS_ERROR_FAILURE);
-
-            pubKey = this.nss.SECKEY_ExtractPublicKey(pubKeyInfo);
-            if (pubKey.isNull())
-                throw Components.Exception("SECKEY_ExtractPublicKey failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 4. Wrap the symmetric key with the public key.
-
-            let wrapMech = this.nss.PK11_AlgtagToMechanism(this.nss.SEC_OID_PKCS1_RSA_ENCRYPTION);
-
-            let s = this.nss.PK11_PubWrapSymKey(wrapMech, pubKey, symKey, wrappedKey.address());
-            if (s)
-                throw Components.Exception("PK11_PubWrapSymKey failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 5. Base64 encode the wrapped key, cleanup, and return to caller.
-            return this.encodeBase64(wrappedKey.data, wrappedKey.len);
-        } catch (e) {
-            this.log("wrapSymmetricKey: failed: " + e);
-            throw e;
-        } finally {
-            if (pubKey && !pubKey.isNull())
-                this.nss.SECKEY_DestroyPublicKey(pubKey);
-            if (pubKeyInfo && !pubKeyInfo.isNull())
-                this.nss.SECKEY_DestroySubjectPublicKeyInfo(pubKeyInfo);
-            if (symKey && !symKey.isNull())
-                this.nss.PK11_FreeSymKey(symKey);
-            if (slot && !slot.isNull())
-                this.nss.PK11_FreeSlot(slot);
-        }
-    },
-
-
-    unwrapSymmetricKey : function(wrappedSymmetricKey, wrappedPrivateKey, passphrase, salt, iv) {
-        this.log("unwrapSymmetricKey() called");
-        let privKeyUsageLength = 1;
-        let privKeyUsage = new ctypes.ArrayType(this.nss_t.CK_ATTRIBUTE_TYPE, privKeyUsageLength)();
-        privKeyUsage[0] = this.nss.CKA_UNWRAP;
-
-        // Step 1. Get rid of the base64 encoding on the inputs.
-        let wrappedPrivKey = this.makeSECItem(wrappedPrivateKey, true);
-        let wrappedSymKey  = this.makeSECItem(wrappedSymmetricKey, true);
-
-        let ivParam, slot, pbeKey, symKey, privKey, symKeyData;
-        try {
-            // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form.
-            pbeKey = this._deriveKeyFromPassphrase(passphrase, salt);
-            let ivItem = this.makeSECItem(iv, true);
-
-            // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD
-            let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm);
-            wrapMech = this.nss.PK11_GetPadMechanism(wrapMech);
-            if (wrapMech == this.nss.CKM_INVALID_MECHANISM)
-                throw Components.Exception("unwrapSymKey: unknown key mech", Cr.NS_ERROR_FAILURE);
-
-            ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address());
-            if (ivParam.isNull())
-                throw Components.Exception("unwrapSymKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 3. Unwrap the private key with the key from the passphrase.
-            slot = this.nss.PK11_GetInternalSlot();
-            if (slot.isNull())
-                throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE);
-
-            // Normally, one wants to associate a private key with a public key.
-            // P11_UnwrapPrivKey() passes its keyID arg to PK11_MakeIDFromPubKey(),
-            // which hashes the public key to create an ID (or, for small inputs,
-            // assumes it's already hashed and does nothing).
-            // We don't really care about this, because our unwrapped private key will
-            // just live long enough to unwrap the bulk data key. So, we'll just jam in
-            // a random value... We have an IV handy, so that will suffice.
-            let keyID = ivItem.address();
-
-            privKey = this.nss.PK11_UnwrapPrivKey(slot,
-                                                  pbeKey, wrapMech, ivParam, wrappedPrivKey.address(),
-                                                  null,   // label
-                                                  keyID,
-                                                  false, // isPerm (token object)
-                                                  true,  // isSensitive
-                                                  this.nss.CKK_RSA,
-                                                  privKeyUsage.addressOfElement(0), privKeyUsageLength,
-                                                  null);  // wincx
-            if (privKey.isNull())
-                throw Components.Exception("PK11_UnwrapPrivKey failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 4. Unwrap the symmetric key with the user's private key.
-
-            // XXX also have PK11_PubUnwrapSymKeyWithFlags() if more control is needed.
-            // (last arg is keySize, 0 seems to work)
-            symKey = this.nss.PK11_PubUnwrapSymKey(privKey, wrappedSymKey.address(), wrapMech,
-                                                   this.nss.CKA_DECRYPT, 0);
-            if (symKey.isNull())
-                throw Components.Exception("PK11_PubUnwrapSymKey failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 5. Base64 encode the unwrapped key, cleanup, and return to caller.
-            if (this.nss.PK11_ExtractKeyValue(symKey))
-                throw Components.Exception("PK11_ExtractKeyValue failed.", Cr.NS_ERROR_FAILURE);
-
-            symKeyData = this.nss.PK11_GetKeyData(symKey);
-            if (symKeyData.isNull())
-                throw Components.Exception("PK11_GetKeyData failed.", Cr.NS_ERROR_FAILURE);
-
-            return this.encodeBase64(symKeyData.contents.data, symKeyData.contents.len);
-        } catch (e) {
-            this.log("unwrapSymmetricKey: failed: " + e);
-            throw e;
-        } finally {
-            if (privKey && !privKey.isNull())
-                this.nss.SECKEY_DestroyPrivateKey(privKey);
-            if (symKey && !symKey.isNull())
-                this.nss.PK11_FreeSymKey(symKey);
-            if (pbeKey && !pbeKey.isNull())
-                this.nss.PK11_FreeSymKey(pbeKey);
-            if (slot && !slot.isNull())
-                this.nss.PK11_FreeSlot(slot);
-            if (ivParam && !ivParam.isNull())
-                this.nss.SECITEM_FreeItem(ivParam, true);
-        }
-    },
-
-
-    rewrapPrivateKey : function(wrappedPrivateKey, oldPassphrase, salt, iv, newPassphrase) {
-        this.log("rewrapPrivateKey() called");
-        let privKeyUsageLength = 1;
-        let privKeyUsage = new ctypes.ArrayType(this.nss_t.CK_ATTRIBUTE_TYPE, privKeyUsageLength)();
-        privKeyUsage[0] = this.nss.CKA_UNWRAP;
-
-        // Step 1. Get rid of the base64 encoding on the inputs.
-        let wrappedPrivKey = this.makeSECItem(wrappedPrivateKey, true);
-
-        let pbeKey, ivParam, slot, privKey;
-        try {
-            // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form.
-            let pbeKey = this._deriveKeyFromPassphrase(oldPassphrase, salt);
-            let ivItem = this.makeSECItem(iv, true);
-
-            // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD
-            let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm);
-            wrapMech = this.nss.PK11_GetPadMechanism(wrapMech);
-            if (wrapMech == this.nss.CKM_INVALID_MECHANISM)
-                throw Components.Exception("rewrapSymKey: unknown key mech", Cr.NS_ERROR_FAILURE);
-
-            ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address());
-            if (ivParam.isNull())
-                throw Components.Exception("rewrapSymKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 3. Unwrap the private key with the key from the passphrase.
-            slot = this.nss.PK11_GetInternalSlot();
-            if (slot.isNull())
-                throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE);
-
-            let keyID = ivItem.address();
-
-            privKey = this.nss.PK11_UnwrapPrivKey(slot,
-                                                  pbeKey, wrapMech, ivParam, wrappedPrivKey.address(),
-                                                  null,   // label
-                                                  keyID,
-                                                  false, // isPerm (token object)
-                                                  true,  // isSensitive
-                                                  this.nss.CKK_RSA,
-                                                  privKeyUsage.addressOfElement(0), privKeyUsageLength,
-                                                  null);  // wincx
-            if (privKey.isNull())
-                throw Components.Exception("PK11_UnwrapPrivKey failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 4. Rewrap the private key with the new passphrase.
-            return this._wrapPrivateKey(privKey, newPassphrase, salt, iv);
-        } catch (e) {
-            this.log("rewrapPrivateKey: failed: " + e);
-            throw e;
-        } finally {
-            if (privKey && !privKey.isNull())
-                this.nss.SECKEY_DestroyPrivateKey(privKey);
-            if (slot && !slot.isNull())
-                this.nss.PK11_FreeSlot(slot);
-            if (ivParam && !ivParam.isNull())
-                this.nss.SECITEM_FreeItem(ivParam, true);
-            if (pbeKey && !pbeKey.isNull())
-                this.nss.PK11_FreeSymKey(pbeKey);
-        }
-    },
-
-
-    verifyPassphrase : function(wrappedPrivateKey, passphrase, salt, iv) {
-        this.log("verifyPassphrase() called");
-        let privKeyUsageLength = 1;
-        let privKeyUsage = new ctypes.ArrayType(this.nss_t.CK_ATTRIBUTE_TYPE, privKeyUsageLength)();
-        privKeyUsage[0] = this.nss.CKA_UNWRAP;
-
-        // Step 1. Get rid of the base64 encoding on the inputs.
-        let wrappedPrivKey = this.makeSECItem(wrappedPrivateKey, true);
-
-        let pbeKey, ivParam, slot, privKey;
-        try {
-            // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form.
-            pbeKey = this._deriveKeyFromPassphrase(passphrase, salt);
-            let ivItem = this.makeSECItem(iv, true);
-
-            // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD
-            let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm);
-            wrapMech = this.nss.PK11_GetPadMechanism(wrapMech);
-            if (wrapMech == this.nss.CKM_INVALID_MECHANISM)
-                throw Components.Exception("rewrapSymKey: unknown key mech", Cr.NS_ERROR_FAILURE);
-
-            ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address());
-            if (ivParam.isNull())
-                throw Components.Exception("rewrapSymKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE);
-
-            // Step 3. Unwrap the private key with the key from the passphrase.
-            slot = this.nss.PK11_GetInternalSlot();
-            if (slot.isNull())
-                throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE);
-
-            let keyID = ivItem.address();
-
-            privKey = this.nss.PK11_UnwrapPrivKey(slot,
-                                                  pbeKey, wrapMech, ivParam, wrappedPrivKey.address(),
-                                                  null,   // label
-                                                  keyID,
-                                                  false, // isPerm (token object)
-                                                  true,  // isSensitive
-                                                  this.nss.CKK_RSA,
-                                                  privKeyUsage.addressOfElement(0), privKeyUsageLength,
-                                                  null);  // wincx
-            return (!privKey.isNull());
-        } catch (e) {
-            this.log("verifyPassphrase: failed: " + e);
-            throw e;
-        } finally {
-            if (privKey && !privKey.isNull())
-                this.nss.SECKEY_DestroyPrivateKey(privKey);
-            if (slot && !slot.isNull())
-                this.nss.PK11_FreeSlot(slot);
-            if (ivParam && !ivParam.isNull())
-                this.nss.SECITEM_FreeItem(ivParam, true);
-            if (pbeKey && !pbeKey.isNull())
-                this.nss.PK11_FreeSymKey(pbeKey);
-        }
-    },
-
-
     //
     // Utility functions
     //
 
 
     // Compress a JS string (2-byte chars) into a normal C string (1-byte chars)
     // EG, for "ABC",  0x0041, 0x0042, 0x0043 --> 0x41, 0x42, 0x43
     byteCompress : function (jsString, charArray) {
@@ -995,114 +665,89 @@ WeaveCrypto.prototype = {
         let expanded = "";
         let len = charArray.length;
         let intData = ctypes.cast(charArray, ctypes.uint8_t.array(len));
         for (let i = 0; i < len; i++)
             expanded += String.fromCharCode(intData[i]);
         return expanded;
     },
 
-    encodeBase64 : function (data, len) {
+    expandData : function expandData(data, len) {
         // Byte-expand the buffer, so we can treat it as a UCS-2 string
         // consisting of u0000 - u00FF.
         let expanded = "";
         let intData = ctypes.cast(data, ctypes.uint8_t.array(len).ptr).contents;
         for (let i = 0; i < len; i++)
             expanded += String.fromCharCode(intData[i]);
-        return btoa(expanded);
+      return expanded;
     },
 
+    encodeBase64 : function (data, len) {
+        return btoa(this.expandData(data, len));
+    },
 
     makeSECItem : function(input, isEncoded) {
         if (isEncoded)
             input = atob(input);
         let outputData = new ctypes.ArrayType(ctypes.unsigned_char, input.length)();
         this.byteCompress(input, outputData);
 
         return new this.nss_t.SECItem(this.nss.SIBUFFER, outputData, outputData.length);
     },
 
-    _deriveKeyFromPassphrase : function (passphrase, salt) {
-        this.log("_deriveKeyFromPassphrase() called.");
+
+    /**
+     * Returns the expanded data string for the derived key.
+     */
+    deriveKeyFromPassphrase : function deriveKeyFromPassphrase(passphrase, salt, keyLength) {
+        this.log("deriveKeyFromPassphrase() called.");
         let passItem = this.makeSECItem(passphrase, false);
         let saltItem = this.makeSECItem(salt, true);
 
-        // http://mxr.mozilla.org/seamonkey/source/security/nss/lib/pk11wrap/pk11pbe.c#1261
-
-        // Bug 436577 prevents us from just using SEC_OID_PKCS5_PBKDF2 here
         let pbeAlg = this.algorithm;
         let cipherAlg = this.algorithm; // ignored by callee when pbeAlg != a pkcs5 mech.
         let prfAlg = this.nss.SEC_OID_HMAC_SHA1; // callee picks if SEC_OID_UNKNOWN, but only SHA1 is supported
 
-        let keyLength  = 0;    // Callee will pick.
+        let keyLength  = keyLength || 0;    // 0 = Callee will pick.
         let iterations = 4096; // PKCS#5 recommends at least 1000.
 
-        let algid, slot, symKey;
+        let algid, slot, symKey, keyData;
         try {
             algid = this.nss.PK11_CreatePBEV2AlgorithmID(pbeAlg, cipherAlg, prfAlg,
                                                         keyLength, iterations, saltItem.address());
             if (algid.isNull())
                 throw Components.Exception("PK11_CreatePBEV2AlgorithmID failed", Cr.NS_ERROR_FAILURE);
 
             slot = this.nss.PK11_GetInternalSlot();
             if (slot.isNull())
                 throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE);
 
             symKey = this.nss.PK11_PBEKeyGen(slot, algid, passItem.address(), false, null);
             if (symKey.isNull())
                 throw Components.Exception("PK11_PBEKeyGen failed", Cr.NS_ERROR_FAILURE);
+
+            // Take the PK11SymKeyStr, returning the extracted key data.
+            if (this.nss.PK11_ExtractKeyValue(symKey)) {
+                throw this.makeException("PK11_ExtractKeyValue failed.", Cr.NS_ERROR_FAILURE);
+            }
+
+            keyData = this.nss.PK11_GetKeyData(symKey);
+
+            if (keyData.isNull())
+                throw Components.Exception("PK11_GetKeyData failed", Cr.NS_ERROR_FAILURE);
+
+            // This copies the key contents into a JS string, so we don't leak.
+            // The `finally` block below will clean up.
+            return this.expandData(keyData.contents.data, keyData.contents.len);
+
         } catch (e) {
-            this.log("_deriveKeyFromPassphrase: failed: " + e);
+            this.log("deriveKeyFromPassphrase: failed: " + e);
             throw e;
         } finally {
             if (algid && !algid.isNull())
                 this.nss.SECOID_DestroyAlgorithmID(algid, true);
             if (slot && !slot.isNull())
                 this.nss.PK11_FreeSlot(slot);
-        }
-
-        return symKey;
+            if (symKey && !symKey.isNull())
+                this.nss.PK11_FreeSymKey(symKey);
+    }
     },
-
-
-    _wrapPrivateKey : function(privKey, passphrase, salt, iv) {
-        this.log("_wrapPrivateKey() called.");
-        let ivParam, pbeKey, wrappedKey;
-        try {
-            // Convert our passphrase to a symkey and get the IV in the form we want.
-            pbeKey = this._deriveKeyFromPassphrase(passphrase, salt);
-
-            let ivItem = this.makeSECItem(iv, true);
-
-            // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD
-            let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm);
-            wrapMech = this.nss.PK11_GetPadMechanism(wrapMech);
-            if (wrapMech == this.nss.CKM_INVALID_MECHANISM)
-                throw Components.Exception("wrapPrivKey: unknown key mech", Cr.NS_ERROR_FAILURE);
-
-            let ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address());
-            if (ivParam.isNull())
-                throw Components.Exception("wrapPrivKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE);
-
-            // Use a buffer to hold the wrapped key. NSS says about 1200 bytes for
-            // a 2048-bit RSA key, so a 4096 byte buffer should be plenty.
-            let keyData = new ctypes.ArrayType(ctypes.unsigned_char, 4096)();
-            wrappedKey = new this.nss_t.SECItem(this.nss.SIBUFFER, keyData, keyData.length);
-
-            let s = this.nss.PK11_WrapPrivKey(privKey.contents.pkcs11Slot,
-                                              pbeKey, privKey,
-                                              wrapMech, ivParam,
-                                              wrappedKey.address(), null);
-            if (s)
-                throw Components.Exception("wrapPrivKey: PK11_WrapPrivKey failed", Cr.NS_ERROR_FAILURE);
-
-            return this.encodeBase64(wrappedKey.data, wrappedKey.len);
-        } catch (e) {
-            this.log("_wrapPrivateKey: failed: " + e);
-            throw e;
-        } finally {
-            if (ivParam && !ivParam.isNull())
-                this.nss.SECITEM_FreeItem(ivParam, true);
-            if (pbeKey && !pbeKey.isNull())
-                this.nss.PK11_FreeSymKey(pbeKey);
-        }
-    }
 };
new file mode 100644
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_deriveKey.js
@@ -0,0 +1,38 @@
+var btoa;
+
+function test_derive(cryptoSvc) {
+  // Extracted from test_utils_deriveKey.
+  let pp = "secret phrase";
+  let salt = "RE5YUHpQcGl3bg==";   // btoa("DNXPzPpiwn")
+  
+  // 16-byte, extract key data.
+  let k = cryptoSvc.deriveKeyFromPassphrase(pp, salt, 16);
+  do_check_eq(16, k.length);
+  do_check_eq(btoa(k), "d2zG0d2cBfXnRwMUGyMwyg==");
+  
+  // Test different key lengths.
+  k = cryptoSvc.deriveKeyFromPassphrase(pp, salt, 32);
+  do_check_eq(32, k.length);
+  let encKey = btoa(k);
+  
+  // Test via encryption.
+  let iv = cryptoSvc.generateRandomIV();
+  do_check_eq(cryptoSvc.decrypt(cryptoSvc.encrypt("bacon", encKey, iv), encKey, iv), "bacon");
+  
+  // Test default length (32).
+  k = cryptoSvc.deriveKeyFromPassphrase(pp, salt, null);
+  do_check_eq(32, k.length);
+  do_check_eq(encKey, btoa(k));
+}
+
+function run_test() {
+  let cryptoSvc;
+  try {
+    let backstagePass = Components.utils.import("resource://services-crypto/WeaveCrypto.js");
+    btoa = backstagePass.btoa;
+  } catch (ex) {
+    _("Aborting test: no WeaveCrypto.js.");
+    return;
+  }
+  test_derive(new WeaveCrypto());
+}
deleted file mode 100644
--- a/services/crypto/tests/unit/test_crypto_keypair.js
+++ /dev/null
@@ -1,70 +0,0 @@
-let cryptoSvc;
-try {
-  Components.utils.import("resource://services-crypto/WeaveCrypto.js");
-  cryptoSvc = new WeaveCrypto();
-} catch (ex) {
-  // Fallback to binary WeaveCrypto
-  cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]
-                .getService(Ci.IWeaveCrypto);
-}
-
-function run_test() {
-  var salt = cryptoSvc.generateRandomBytes(16);
-  do_check_eq(salt.length, 24);
-
-  var iv = cryptoSvc.generateRandomIV();
-  do_check_eq(iv.length, 24);
-
-  var symKey = cryptoSvc.generateRandomKey();
-  do_check_eq(symKey.length, 44);
-
-
-  // Tests with a 2048 bit key (the default)
-  do_check_eq(cryptoSvc.keypairBits, 2048)
-
-  var pubOut = {};
-  var privOut = {};
-  cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut);
-  var pubKey = pubOut.value;
-  var privKey = privOut.value;
-  do_check_true(!!pubKey);
-  do_check_true(!!privKey);
-  do_check_eq(pubKey.length, 392);
-  do_check_true(privKey.length == 1624 || privKey.length == 1644);
-
-  // do some key wrapping
-  var wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey);
-  do_check_eq(wrappedKey.length, 344);
-
-  var unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey,
-                                                  "my passphrase", salt, iv);
-  do_check_eq(unwrappedKey.length, 44);
-
-  // The acid test... Is our unwrapped key the same thing we started with?
-  do_check_eq(unwrappedKey, symKey);
-
-
-  // Tests with a 1024 bit key
-  cryptoSvc.keypairBits = 1024;
-  do_check_eq(cryptoSvc.keypairBits, 1024)
-
-  cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut);
-  var pubKey = pubOut.value;
-  var privKey = privOut.value;
-  do_check_true(!!pubKey);
-  do_check_true(!!privKey);
-  do_check_eq(pubKey.length, 216);
-  do_check_eq(privKey.length, 856);
-
-  // do some key wrapping
-  wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey);
-  do_check_eq(wrappedKey.length, 172);
-  unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey,
-                                                  "my passphrase", salt, iv);
-  do_check_eq(unwrappedKey.length, 44);
-
-  // The acid test... Is our unwrapped key the same thing we started with?
-  do_check_eq(unwrappedKey, symKey);
-
-
-}
deleted file mode 100644
--- a/services/crypto/tests/unit/test_crypto_rewrap.js
+++ /dev/null
@@ -1,42 +0,0 @@
-let cryptoSvc;
-try {
-  Components.utils.import("resource://services-crypto/WeaveCrypto.js");
-  cryptoSvc = new WeaveCrypto();
-} catch (ex) {
-  // Fallback to binary WeaveCrypto
-  cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]
-                .getService(Ci.IWeaveCrypto);
-}
-
-function run_test() {
-  var salt = cryptoSvc.generateRandomBytes(16);
-  var iv = cryptoSvc.generateRandomIV();
-  var symKey = cryptoSvc.generateRandomKey();
-
-  // Tests with a 2048 bit key (the default)
-  do_check_eq(cryptoSvc.keypairBits, 2048)
-  var pubOut = {};
-  var privOut = {};
-  cryptoSvc.generateKeypair("old passphrase", salt, iv, pubOut, privOut);
-  var pubKey = pubOut.value;
-  var privKey = privOut.value;
-
-  // do some key wrapping
-  var wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey);
-  var unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey,
-                                                  "old passphrase", salt, iv);
-
-  // Is our unwrapped key the same thing we started with?
-  do_check_eq(unwrappedKey, symKey);
-
-  // Rewrap key with a new passphrase
-  var newPrivKey = cryptoSvc.rewrapPrivateKey(privKey, "old passphrase",
-                                              salt, iv, "new passphrase");
-  
-  // Unwrap symkey with new symkey
-  var newUnwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, newPrivKey,
-                                                     "new passphrase", salt, iv);
-  
-  // The acid test... Is this unwrapped symkey the same as before?
-  do_check_eq(newUnwrappedKey, unwrappedKey);
-}
deleted file mode 100644
--- a/services/crypto/tests/unit/test_crypto_verify.js
+++ /dev/null
@@ -1,30 +0,0 @@
-let cryptoSvc;
-try {
-  Components.utils.import("resource://services-crypto/WeaveCrypto.js");
-  cryptoSvc = new WeaveCrypto();
-} catch (ex) {
-  // Fallback to binary WeaveCrypto
-  cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]
-                .getService(Ci.IWeaveCrypto);
-}
-
-function run_test() {
-  var salt = cryptoSvc.generateRandomBytes(16);
-  var iv = cryptoSvc.generateRandomIV();
-
-  // Tests with a 2048 bit key (the default)
-  do_check_eq(cryptoSvc.keypairBits, 2048)
-  var privOut = {};
-  cryptoSvc.generateKeypair("passphrase", salt, iv, {}, privOut);
-  var privKey = privOut.value;
-
-  // Check with correct passphrase
-  var shouldBeTrue = cryptoSvc.verifyPassphrase(privKey, "passphrase",
-                                                salt, iv);
-  do_check_eq(shouldBeTrue, true);
-
-  // Check with incorrect passphrase
-  var shouldBeFalse = cryptoSvc.verifyPassphrase(privKey, "NotPassphrase",
-                                                 salt, iv);
-  do_check_eq(shouldBeFalse, false);
-}
--- a/services/sync/modules/base_records/collection.js
+++ b/services/sync/modules/base_records/collection.js
@@ -133,32 +133,26 @@ Collection.prototype = {
   clearRecords: function Coll_clearRecords() {
     this._data = [];
   },
 
   set recordHandler(onRecord) {
     // Save this because onProgress is called with this as the ChannelListener
     let coll = this;
 
-    // Prepare a dummyUri so that records can generate the correct
-    // relative URLs.  The last bit will be replaced with record.id.
-    let dummyUri = this.uri.clone().QueryInterface(Ci.nsIURL);
-    dummyUri.filePath += "/replaceme";
-    dummyUri.query = "";
-
     // Switch to newline separated records for incremental parsing
     coll.setHeader("Accept", "application/newlines");
 
     this._onProgress = function() {
       let newline;
       while ((newline = this._data.indexOf("\n")) > 0) {
         // Split the json record from the rest of the data
         let json = this._data.slice(0, newline);
         this._data = this._data.slice(newline + 1);
 
         // Deserialize a record from json and give it to the callback
-        let record = new coll._recordObj(dummyUri);
+        let record = new coll._recordObj();
         record.deserialize(json);
         onRecord(record);
       }
     };
   }
 };
--- a/services/sync/modules/base_records/crypto.js
+++ b/services/sync/modules/base_records/crypto.js
@@ -14,196 +14,453 @@
  * The Original Code is Weave.
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2008
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Dan Mills <thunder@mozilla.com>
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-const EXPORTED_SYMBOLS = ['CryptoWrapper', 'CryptoMeta', 'CryptoMetas'];
+const EXPORTED_SYMBOLS = ["CryptoWrapper", "CollectionKeys", "BulkKeyBundle", "SyncKeyBundle"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/base_records/keys.js");
+Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/base_records/wbo.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/log4moz.js");
 
-function CryptoWrapper(uri) {
+function CryptoWrapper(collection, id) {
   this.cleartext = {};
-  WBORecord.call(this, uri);
+  WBORecord.call(this, collection, id);
   this.ciphertext = null;
+  this.id = id;
 }
 CryptoWrapper.prototype = {
   __proto__: WBORecord.prototype,
   _logName: "Record.CryptoWrapper",
 
-  get encryption() {
-    return this.uri.resolve(this.payload.encryption);
-  },
-  set encryption(value) {
-    this.payload.encryption = this.uri.getRelativeSpec(Utils.makeURI(value));
+  ciphertextHMAC: function ciphertextHMAC(keyBundle) {
+    let hmacKey = keyBundle.hmacKeyObject;
+    if (!hmacKey)
+      throw "Cannot compute HMAC with null key.";
+    
+    return Utils.sha256HMAC(this.ciphertext, hmacKey);
   },
 
-  encrypt: function CryptoWrapper_encrypt(passphrase) {
-    let pubkey = PubKeys.getDefaultKey();
-    let privkey = PrivKeys.get(pubkey.privateKeyUri);
+  /*
+   * Don't directly use the sync key. Instead, grab a key for this
+   * 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) {
 
-    let meta = CryptoMetas.get(this.encryption);
-    let symkey = meta.getKey(privkey, passphrase);
+    keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
+    if (!keyBundle)
+      throw new Error("Key bundle is null for " + this.uri.spec);
 
     this.IV = Svc.Crypto.generateRandomIV();
     this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext),
-                                         symkey, this.IV);
-    this.hmac = Utils.sha256HMAC(this.ciphertext, symkey.hmacKey);
+                                         keyBundle.encryptionKey, this.IV);
+    this.hmac = this.ciphertextHMAC(keyBundle);
     this.cleartext = null;
   },
 
-  decrypt: function CryptoWrapper_decrypt(passphrase, keyUri) {
-    let pubkey = PubKeys.getDefaultKey();
-    let privkey = PrivKeys.get(pubkey.privateKeyUri);
+  // Optional key bundle.
+  decrypt: function decrypt(keyBundle) {
+    
+    if (!this.ciphertext) {
+      throw "No ciphertext: nothing to decrypt?";
+    }
 
-    let meta = CryptoMetas.get(keyUri);
-    let symkey = meta.getKey(privkey, passphrase);
+    keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
+    if (!keyBundle)
+      throw new Error("Key bundle is null for " + this.collection + "/" + this.id);
 
     // Authenticate the encrypted blob with the expected HMAC
-    if (Utils.sha256HMAC(this.ciphertext, symkey.hmacKey) != this.hmac)
-      throw "Record SHA256 HMAC mismatch: " + this.hmac;
+    let computedHMAC = this.ciphertextHMAC(keyBundle);
+
+    if (computedHMAC != this.hmac) {
+      throw "Record SHA256 HMAC mismatch: " + this.hmac + ", not " + computedHMAC;
+    }
 
-    this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, symkey,
-                                                   this.IV));
+    // Handle invalid data here. Elsewhere we assume that cleartext is an object.
+    let json_result = JSON.parse(Svc.Crypto.decrypt(this.ciphertext,
+                                                    keyBundle.encryptionKey, this.IV));
+    
+    if (json_result && (json_result instanceof Object)) {
+      this.cleartext = json_result;
     this.ciphertext = null;
+    }
+    else {
+      throw "Decryption failed: result is <" + json_result + ">, not an object.";
+    }
 
-    // Verify that the encrypted id matches the requested record's id
+    // Verify that the encrypted id matches the requested record's id.
     if (this.cleartext.id != this.id)
       throw "Record id mismatch: " + [this.cleartext.id, this.id];
 
     return this.cleartext;
   },
 
   toString: function CryptoWrap_toString() "{ " + [
       "id: " + this.id,
       "index: " + this.sortindex,
       "modified: " + this.modified,
-      "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.cleartext))
+      "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.cleartext)),
+      "collection: " + (this.collection || "undefined")
     ].join("\n  ") + " }",
 
   // The custom setter below masks the parent's getter, so explicitly call it :(
   get id() WBORecord.prototype.__lookupGetter__("id").call(this),
 
   // Keep both plaintext and encrypted versions of the id to verify integrity
   set id(val) {
     WBORecord.prototype.__lookupSetter__("id").call(this, val);
     return this.cleartext.id = val;
   },
 };
 
 Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]);
 Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
 
-function CryptoMeta(uri) {
-  WBORecord.call(this, uri);
-  this.keyring = {};
+Utils.lazy(this, "CollectionKeys", CollectionKeyManager);
+
+
+/**
+ * Keeps track of mappings between collection names ('tabs') and
+ * keyStrs, which you can feed into KeyBundle to get encryption tokens.
+ *
+ * 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("CollectionKeys");
 }
-CryptoMeta.prototype = {
-  __proto__: WBORecord.prototype,
-  _logName: "Record.CryptoMeta",
+
+// TODO: persist this locally as an Identity. Bug 610913.
+// Note that the last modified time needs to be preserved.
+CollectionKeyManager.prototype = {
+  
+  keyForCollection: function(collection) {
+                      
+    // Moderately temporary debugging code.
+    this._log.trace("keyForCollection: " + collection + ". Default is " + (this._default ? "not null." : "null."));
+    
+    if (collection && this._collections[collection])
+      return this._collections[collection];
+    
+    return this._default;
+  },
 
-  getWrappedKey: function _getWrappedKey(privkey) {
-    // get the uri to our public key
-    let pubkeyUri = privkey.publicKeyUri.spec;
+  /**
+   * If `collections` (an array of strings) is provided, iterate
+   * over it and generate random keys for each collection.
+   */
+  generateNewKeys: function(collections) {
+    let newDefaultKey = new BulkKeyBundle(null, DEFAULT_KEYBUNDLE_NAME);
+    newDefaultKey.generateRandom();
+    
+    let newColls = {};
+    if (collections) {
+      collections.forEach(function (c) {
+        let b = new BulkKeyBundle(null, c);
+        b.generateRandom();
+        newColls[c] = b;
+      });
+    }
+    this._default = newDefaultKey;
+    this._collections = newColls;
+    this._lastModified = (Math.round(Date.now()/10)/100);
+  },
 
-    // each hash key is a relative uri, resolve those and match against ours
-    for (let relUri in this.keyring) {
-      if (pubkeyUri == this.baseUri.resolve(relUri))
-        return this.keyring[relUri];
+  asWBO: function(collection, id) {
+    let wbo = new CryptoWrapper(collection || "crypto", id || "keys");
+    let c = {};
+    for (let k in this._collections) {
+      c[k] = this._collections[k].keyPair;
     }
-    return null;
+    wbo.cleartext = {
+      "default": this._default ? this._default.keyPair : null,
+      "collections": c,
+      "id": id,
+      "collection": collection
+    };
+    wbo.modified = this._lastModified;
+    return wbo;
+  },
+
+  // Take the fetched info/collections WBO, checking the change
+  // time of the crypto collection.
+  updateNeeded: function(info_collections) {
+
+    this._log.info("Testing for updateNeeded. Last modified: " + this._lastModified);
+
+    // No local record of modification time? Need an update.
+    if (!this._lastModified)
+      return true;
+
+    // No keys on the server? We need an update, though our
+    // update handling will be a little more drastic...
+    if (!("crypto" in info_collections))
+      return true;
+
+    // Otherwise, we need an update if our modification time is stale.
+    return (info_collections["crypto"] > this._lastModified);
   },
 
-  getKey: function CryptoMeta_getKey(privkey, passphrase) {
-    let wrapped_key = this.getWrappedKey(privkey);
-    if (!wrapped_key)
-      throw "keyring doesn't contain a key for " + privkey.publicKeyUri.spec;
-
-    // Make sure the wrapped key hasn't been tampered with
-    let localHMAC = Utils.sha256HMAC(wrapped_key.wrapped, this.hmacKey);
-    if (localHMAC != wrapped_key.hmac)
-      throw "Key SHA256 HMAC mismatch: " + wrapped_key.hmac;
-
-    // Decrypt the symmetric key and make it a String object to add properties
-    let unwrappedKey = new String(
-      Svc.Crypto.unwrapSymmetricKey(
-        wrapped_key.wrapped,
-        privkey.keyData,
-        passphrase.passwordUTF8,
-        privkey.salt,
-        privkey.iv
-      )
-    );
-
-    unwrappedKey.hmacKey = Utils.makeHMACKey(unwrappedKey);
-
-    // Cache the result after the first get and just return it
-    return (this.getKey = function() unwrappedKey)();
+  setContents: function setContents(payload, modified) {
+    if ("collections" in payload) {
+      let out_coll = {};
+      let colls = payload["collections"];
+      for (let k in colls) {
+        let v = colls[k];
+        if (v) {
+          let keyObj = new BulkKeyBundle(null, k);
+          keyObj.keyPair = v;
+          if (keyObj) {
+            out_coll[k] = keyObj;
+          }
+        }
+      }
+      this._collections = out_coll;
+    }
+    if ("default" in payload) {
+      if (payload.default) {
+        let b = new BulkKeyBundle(null, DEFAULT_KEYBUNDLE_NAME);
+        b.keyPair = payload.default;
+        this._default = b;
+      }
+      else {
+        this._default = null;
+      }
+    }
+    
+    // The server will round the time, which can lead to us having spurious
+    // key refreshes. Do the best we can to get an accurate timestamp, but
+    // rounded to 2 decimal places.
+    // We could use .toFixed(2), but that's a little more multiplication and
+    // division...
+    this._lastModified = modified || (Math.round(Date.now()/10)/100);
+    return payload;
   },
 
-  addKey: function CryptoMeta_addKey(new_pubkey, privkey, passphrase) {
-    let symkey = this.getKey(privkey, passphrase);
-    this.addUnwrappedKey(new_pubkey, symkey);
-  },
-
-  addUnwrappedKey: function CryptoMeta_addUnwrappedKey(new_pubkey, symkey) {
-    // get the new public key
-    if (typeof new_pubkey == "string")
-      new_pubkey = PubKeys.get(new_pubkey);
-
-    // each hash key is a relative uri, resolve those and
-    // if we find the one we're about to add, remove it
-    for (let relUri in this.keyring) {
-      if (new_pubkey.uri.spec == this.uri.resolve(relUri))
-        delete this.keyring[relUri];
+  updateContents: function updateContents(syncKeyBundle, storage_keys) {
+    let log = this._log;
+    log.info("Updating collection keys...");
+    
+    // storage_keys is a WBO, fetched from storage/crypto/keys.
+    // Its payload is the default key, and a map of collections to keys.
+    // We lazily compute the key objects from the strings we're given.
+    
+    let payload;
+    try {
+      payload = storage_keys.decrypt(syncKeyBundle);
+    } catch (ex) {
+      log.warn("Got exception \"" + ex + "\" decrypting storage keys with sync key.");
+      log.info("Aborting updateContents. Rethrowing.");
+      throw ex;
     }
 
-    // Wrap the symmetric key and generate a HMAC for it
-    let wrapped = Svc.Crypto.wrapSymmetricKey(symkey, new_pubkey.keyData);
-    this.keyring[this.uri.getRelativeSpec(new_pubkey.uri)] = {
-      wrapped: wrapped,
-      hmac: Utils.sha256HMAC(wrapped, this.hmacKey)
-    };
+    let r = this.setContents(payload, storage_keys.modified);
+    log.info("Collection keys updated.");
+    return r;
+    }
+}
+
+/**
+ * Abuse Identity: store the collection name (or default) in the
+ * username field, and the keyStr in the password field.
+ *
+ * We very rarely want to override the realm, so pass null and
+ * it'll default to PWDMGR_KEYBUNDLE_REALM.
+ * 
+ * KeyBundle is the base class for two similar classes:
+ * 
+ * SyncKeyBundle:
+ *
+ *   A key string is provided, and it must be hashed to derive two different
+ *   keys (one HMAC, one AES).
+ *
+ * BulkKeyBundle:
+ *
+ *   Two independent keys are provided, or randomly generated on request.
+ * 
+ */
+function KeyBundle(realm, collectionName, keyStr) {
+  let realm = realm || PWDMGR_KEYBUNDLE_REALM;
+  
+  if (keyStr && !keyStr.charAt)
+    // Ensure it's valid.
+    throw "KeyBundle given non-string key.";
+  
+  Identity.call(this, realm, collectionName, keyStr);
+  this._hmac    = null;
+  this._encrypt = null;
+}
+
+KeyBundle.prototype = {
+  __proto__: Identity.prototype,
+  
+  /*
+   * Accessors for the two keys.
+   */
+  get encryptionKey() {
+    return this._encrypt;
+  },
+  
+  set encryptionKey(value) {
+    this._encrypt = value;
   },
 
   get hmacKey() {
-    let passphrase = ID.get("WeaveCryptoID").passwordUTF8;
-    return Utils.makeHMACKey(passphrase);
+    return this._hmac;
+  },
+  
+  set hmacKey(value) {
+    this._hmac = value;
+  },
+  
+  get hmacKeyObject() {
+    if (this.hmacKey)
+      return Utils.makeHMACKey(this.hmacKey);
+  },
+}
+
+function BulkKeyBundle(realm, collectionName) {
+  let log = Log4Moz.repository.getLogger("BulkKeyBundle");
+  log.info("BulkKeyBundle being created for " + collectionName);
+  KeyBundle.call(this, realm, collectionName);
+}
+
+BulkKeyBundle.prototype = {
+  __proto__: KeyBundle.prototype,
+   
+  generateRandom: function generateRandom() {
+    let generatedHMAC = Svc.Crypto.generateRandomKey();
+    let generatedEncr = Svc.Crypto.generateRandomKey();
+    this.keyPair = [generatedEncr, generatedHMAC];
+  },
+  
+  get keyPair() {
+    return [this._encrypt, btoa(this._hmac)];
+  },
+  
+  /*
+   * Use keyPair = [enc, hmac], or generateRandom(), when
+   * you want to manage the two individual keys.
+   */
+  set keyPair(value) {
+    if (value.length && (value.length == 2)) {
+      let json = JSON.stringify(value);
+      let en = value[0];
+      let hm = value[1];
+      
+      this.password = json;
+      this._hmac    = Utils.safeAtoB(hm);
+      this._encrypt = en;          // Store in base64.
+    }
+    else {
+      throw "Invalid keypair";
   }
+  },
 };
 
-Utils.deferGetSet(CryptoMeta, "payload", "keyring");
+function SyncKeyBundle(realm, collectionName, syncKey) {
+  let log = Log4Moz.repository.getLogger("SyncKeyBundle");
+  log.info("SyncKeyBundle being created for " + collectionName);
+  KeyBundle.call(this, realm, collectionName, syncKey);
+  if (syncKey)
+    this.keyStr = syncKey;      // Accessor sets up keys.
+} 
 
-Utils.lazy(this, 'CryptoMetas', CryptoRecordManager);
+SyncKeyBundle.prototype = {
+  __proto__: KeyBundle.prototype,
+
+  /*
+   * Use keyStr when you want to work with a key string that's
+   * hashed into individual keys.
+   */
+  get keyStr() {
+    return this.password;
+  },
 
-function CryptoRecordManager() {
-  RecordManager.call(this);
-}
-CryptoRecordManager.prototype = {
-  __proto__: RecordManager.prototype,
-  _recordType: CryptoMeta
+  set keyStr(value) {
+    this.password = value;
+    this._hmac = null;
+    this._encrypt = null;
+    this.generateEntry();
+  },
+  
+  /*
+   * Can't rely on password being set through any of our setters:
+   * Identity does work under the hood.
+   * 
+   * Consequently, make sure we derive keys if that work hasn't already been
+   * done.
+   */
+  get encryptionKey() {
+    if (!this._encrypt)
+      this.generateEntry();
+    return this._encrypt;
+  },
+  
+  get hmacKey() {
+    if (!this._hmac)
+      this.generateEntry();
+    return this._hmac;
+  },
+  
+  /*
+   * If we've got a string, hash it into keys and store them.
+   */
+  generateEntry: function generateEntry() {
+    let m = this.keyStr;
+    if (m) {
+      // Decode into a 16-byte string before we go any further.
+      m = Utils.decodeKeyBase32(m);
+      
+      // Reuse the hasher.
+      let h = Utils.makeHMACHasher();
+      
+      // First key.
+      let u = this.username; 
+      let k1 = Utils.makeHMACKey("" + HMAC_INPUT + u + "\x01");
+      let enc = Utils.sha256HMACBytes(m, k1, h);
+      
+      // Second key: depends on the output of the first run.
+      let k2 = Utils.makeHMACKey(enc + HMAC_INPUT + u + "\x02");
+      let hmac = Utils.sha256HMACBytes(m, k2, h);
+      
+      // Save them.
+      this._encrypt = btoa(enc);
+      this._hmac    = hmac;
+    }
+  }
 };
deleted file mode 100644
--- a/services/sync/modules/base_records/keys.js
+++ /dev/null
@@ -1,182 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Weave.
- *
- * The Initial Developer of the Original Code is Mozilla.
- * Portions created by the Initial Developer are Copyright (C) 2008
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *  Dan Mills <thunder@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-const EXPORTED_SYMBOLS = ['PubKey', 'PrivKey',
-                          'PubKeys', 'PrivKeys'];
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
-
-Cu.import("resource://services-sync/base_records/wbo.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/util.js");
-
-function PubKey(uri) {
-  WBORecord.call(this, uri);
-  this.type = "pubkey";
-  this.keyData = null;
-}
-PubKey.prototype = {
-  __proto__: WBORecord.prototype,
-  _logName: "Record.PubKey",
-
-  get privateKeyUri() {
-    if (!this.data)
-      return null;
-
-    // Use the uri if it resolves, otherwise return raw (uri type unresolvable)
-    let key = this.payload.privateKeyUri;
-    return Utils.makeURI(this.uri.resolve(key) || key);
-  },
-  set privateKeyUri(value) {
-    this.payload.privateKeyUri = this.uri.getRelativeSpec(Utils.makeURI(value));
-  },
-
-  get publicKeyUri() {
-    throw "attempted to get public key url from a public key!";
-  }
-};
-
-Utils.deferGetSet(PubKey, "payload", ["keyData", "type"]);
-
-function PrivKey(uri) {
-  WBORecord.call(this, uri);
-  this.type = "privkey";
-  this.salt = null;
-  this.iv = null;
-  this.keyData = null;
-}
-PrivKey.prototype = {
-  __proto__: WBORecord.prototype,
-  _logName: "Record.PrivKey",
-
-  get publicKeyUri() {
-    if (!this.data)
-      return null;
-
-    // Use the uri if it resolves, otherwise return raw (uri type unresolvable)
-    let key = this.payload.publicKeyUri;
-    return Utils.makeURI(this.uri.resolve(key) || key);
-  },
-  set publicKeyUri(value) {
-    this.payload.publicKeyUri = this.uri.getRelativeSpec(Utils.makeURI(value));
-  },
-
-  get privateKeyUri() {
-    throw "attempted to get private key url from a private key!";
-  }
-};
-
-Utils.deferGetSet(PrivKey, "payload", ["salt", "iv", "keyData", "type"]);
-
-// XXX unused/unfinished
-function SymKey(keyData, wrapped) {
-  this._data = keyData;
-  this._wrapped = wrapped;
-}
-SymKey.prototype = {
-  get wrapped() {
-    return this._wrapped;
-  },
-
-  unwrap: function SymKey_unwrap(privkey, passphrase, meta_record) {
-    this._data =
-      Svc.Crypto.unwrapSymmetricKey(this._data, privkey.keyData, passphrase,
-                                    privkey.salt, privkey.iv);
-  }
-};
-
-Utils.lazy(this, 'PubKeys', PubKeyManager);
-
-function PubKeyManager() {
-  RecordManager.call(this);
-}
-PubKeyManager.prototype = {
-  __proto__: RecordManager.prototype,
-  _recordType: PubKey,
-  _logName: "PubKeyManager",
-
-  get defaultKeyUri() this._defaultKeyUri,
-  set defaultKeyUri(value) { this._defaultKeyUri = value; },
-
-  getDefaultKey: function PubKeyManager_getDefaultKey() {
-    return this.get(this.defaultKeyUri);
-  },
-
-  createKeypair: function KeyMgr_createKeypair(passphrase, pubkeyUri, privkeyUri) {
-    if (!pubkeyUri)
-      throw "Missing or null parameter 'pubkeyUri'.";
-    if (!privkeyUri)
-      throw "Missing or null parameter 'privkeyUri'.";
-
-    this._log.debug("Generating RSA keypair");
-    let pubkey = new PubKey(pubkeyUri);
-    let privkey = new PrivKey(privkeyUri);
-    privkey.salt = Svc.Crypto.generateRandomBytes(16);
-    privkey.iv = Svc.Crypto.generateRandomIV();
-
-    let pub = {}, priv = {};
-    Svc.Crypto.generateKeypair(passphrase.passwordUTF8, privkey.salt,
-                               privkey.iv, pub, priv);
-    [pubkey.keyData, privkey.keyData] = [pub.value, priv.value];
-
-    pubkey.privateKeyUri = privkeyUri;
-    privkey.publicKeyUri = pubkeyUri;
-
-    this._log.debug("Generating RSA keypair... done");
-    return {pubkey: pubkey, privkey: privkey};
-  },
-
-  uploadKeypair: function PubKeyManager_uploadKeypair(keys) {
-    for each (let key in keys)
-      new Resource(key.uri).put(key);
-  }
-};
-
-Utils.lazy(this, 'PrivKeys', PrivKeyManager);
-
-function PrivKeyManager() {
-  PubKeyManager.call(this);
-}
-PrivKeyManager.prototype = {
-  __proto__: PubKeyManager.prototype,
-  _recordType: PrivKey,
-  _logName: "PrivKeyManager"
-};
--- a/services/sync/modules/base_records/wbo.js
+++ b/services/sync/modules/base_records/wbo.js
@@ -14,16 +14,17 @@
  * The Original Code is Weave.
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2008
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Dan Mills <thunder@mozilla.com>
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -40,54 +41,61 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
-function WBORecord(uri) {
-  if (uri == null)
-    throw "WBOs must have a URI!";
-
+function WBORecord(collection, id) {
   this.data = {};
   this.payload = {};
-  this.uri = uri;
+  this.collection = collection;      // Optional.
+  this.id = id;                      // Optional.
 }
 WBORecord.prototype = {
   _logName: "Record.WBO",
 
-  // NOTE: baseUri must have a trailing slash, or baseUri.resolve() will omit
-  //       the collection name
-  get uri() {
-    return Utils.makeURL(this.baseUri.resolve(encodeURI(this.id)));
-  },
-  set uri(value) {
-    if (typeof(value) != "string")
-      value = value.spec;
-    let parts = value.split('/');
-    this.id = parts.pop();
-    this.baseUri = Utils.makeURI(parts.join('/') + '/');
-  },
-
   get sortindex() {
     if (this.data.sortindex)
       return this.data.sortindex;
     return 0;
   },
 
+  // Get thyself from your URI, then deserialize.
+  // Set thine 'response' field.
+  fetch: function fetch(uri) {
+    let r = new Resource(uri).get();
+    if (r.success) {
+      this.deserialize(r);   // Warning! Muffles exceptions!
+    }
+    this.response = r;
+    return this;
+  },
+  
+  upload: function upload(uri) {
+    return new Resource(uri).put(this);
+  },
+  
+  // Take a base URI string, with trailing slash, and return the URI of this
+  // WBO based on collection and ID.
+  uri: function(base) {
+    if (this.collection && this.id)
+      return Utils.makeURL(base + this.collection + "/" + this.id);
+    return null;
+  },
+  
   deserialize: function deserialize(json) {
     this.data = json.constructor.toString() == String ? JSON.parse(json) : json;
 
     try {
       // The payload is likely to be JSON, but if not, keep it as a string
       this.payload = JSON.parse(this.payload);
-    }
-    catch(ex) {}
+    } catch(ex) {}
   },
 
   toJSON: function toJSON() {
     // Copy fields from data to be stringified, making sure payload is a string
     let obj = {};
     for (let [key, val] in Iterator(this.data))
       obj[key] = key == "payload" ? JSON.stringify(val) : val;
     return obj;
@@ -123,42 +131,41 @@ RecordManager.prototype = {
       // Don't parse and save the record on failure
       if (!this.response.success)
         return null;
 
       let record = new this._recordType(url);
       record.deserialize(this.response);
 
       return this.set(url, record);
-    }
-    catch(ex) {
+    } catch(ex) {
       this._log.debug("Failed to import record: " + Utils.exceptionStr(ex));
       return null;
     }
   },
 
   get: function RecordMgr_get(url) {
     // Use a url string as the key to the hash
     let spec = url.spec ? url.spec : url;
     if (spec in this._records)
       return this._records[spec];
     return this.import(url);
   },
 
-  set: function RegordMgr_set(url, record) {
+  set: function RecordMgr_set(url, record) {
     let spec = url.spec ? url.spec : url;
     return this._records[spec] = record;
   },
 
-  contains: function RegordMgr_contains(url) {
+  contains: function RecordMgr_contains(url) {
     if ((url.spec || url) in this._records)
       return true;
     return false;
   },
 
   clearCache: function recordMgr_clearCache() {
     this._records = {};
   },
 
-  del: function RegordMgr_del(url) {
+  del: function RecordMgr_del(url) {
     delete this._records[url];
   }
 };
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -14,16 +14,17 @@
  * The Original Code is Bookmarks Sync.
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2007
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Dan Mills <thunder@mozilla.com>
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -39,27 +40,40 @@ let EXPORTED_SYMBOLS = [((this[key] = va
 
 WEAVE_CHANNEL:                         "@xpi_type@",
 WEAVE_VERSION:                         "@weave_version@",
 WEAVE_ID:                              "@weave_id@",
 
 // Version of the data format this client supports. The data format describes
 // how records are packaged; this is separate from the Server API version and
 // the per-engine cleartext formats.
-STORAGE_VERSION:                       3,
+STORAGE_VERSION:                       4,
 
 UPDATED_DEV_URL:                       "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@",
 UPDATED_REL_URL:                       "http://www.mozilla.com/firefox/sync/updated.html",
 
 PREFS_BRANCH:                          "services.sync.",
 
 // Host "key" to access Weave Identity in the password manager
 PWDMGR_HOST:                           "chrome://weave",
 PWDMGR_PASSWORD_REALM:                 "Mozilla Services Password",
 PWDMGR_PASSPHRASE_REALM:               "Mozilla Services Encryption Passphrase",
+PWDMGR_KEYBUNDLE_REALM:                "Mozilla Services Key Bundles",
+
+// Put in [] because those aren't allowed in a collection name.
+DEFAULT_KEYBUNDLE_NAME:                "[default]",
+
+// Our extra input to SHA256-HMAC in generateEntry.
+// This includes the full crypto spec; change this when our algo changes.
+HMAC_INPUT:                            "Sync-AES_256_CBC-HMAC256",
+
+// Key dimensions.
+SYNC_KEY_ENCODED_LENGTH:               26,
+SYNC_KEY_DECODED_LENGTH:               16,
+SYNC_KEY_HYPHENATED_LENGTH:            31,    // 26 chars, 5 hyphens.
 
 // Sync intervals for various clients configurations
 SINGLE_USER_SYNC:                      24 * 60 * 60 * 1000, // 1 day
 MULTI_DESKTOP_SYNC:                    60 * 60 * 1000, // 1 hour
 MULTI_MOBILE_SYNC:                     5 * 60 * 1000, // 5 minutes
 PARTIAL_DATA_SYNC:                     60 * 1000, // 1 minute
 
 // 50 is hardcoded here because of URL length restrictions.
@@ -109,19 +123,16 @@ LOGIN_FAILED_NETWORK_ERROR:            "
 LOGIN_FAILED_SERVER_ERROR:             "error.login.reason.server",
 LOGIN_FAILED_INVALID_PASSPHRASE:       "error.login.reason.synckey",
 LOGIN_FAILED_LOGIN_REJECTED:           "error.login.reason.account",
 
 // sync failure status codes
 METARECORD_DOWNLOAD_FAIL:              "error.sync.reason.metarecord_download_fail",
 VERSION_OUT_OF_DATE:                   "error.sync.reason.version_out_of_date",
 DESKTOP_VERSION_OUT_OF_DATE:           "error.sync.reason.desktop_version_out_of_date",
-KEYS_DOWNLOAD_FAIL:                    "error.sync.reason.keys_download_fail",
-NO_KEYS_NO_KEYGEN:                     "error.sync.reason.no_keys_no_keygen",
-KEYS_UPLOAD_FAIL:                      "error.sync.reason.keys_upload_fail",
 SETUP_FAILED_NO_PASSPHRASE:            "error.sync.reason.setup_failed_no_passphrase",
 CREDENTIALS_CHANGED:                   "error.sync.reason.credentials_changed",
 ABORT_SYNC_COMMAND:                    "aborting sync, process commands said so",
 NO_SYNC_NODE_FOUND:                    "error.sync.reason.no_node_found",
 OVER_QUOTA:                            "error.sync.reason.over_quota",
 
 RESPONSE_OVER_QUOTA:                   "14",
 
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -16,16 +16,17 @@
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2007
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Dan Mills <thunder@mozilla.com>
  *  Myk Melez <myk@mozilla.org>
  *  Philipp von Weitershausen <philipp@weitershausen.de>
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -40,17 +41,16 @@ const EXPORTED_SYMBOLS = ['Engines', 'En
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/collection.js");
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/base_records/wbo.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/ext/Observers.js");
 Cu.import("resource://services-sync/ext/Sync.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/stores.js");
@@ -76,18 +76,21 @@ EngineManagerSvc.prototype = {
         let engine = this.get(name);
         if (engine)
           engines.push(engine);
       }, this);
       return engines;
     }
 
     let engine = this._engines[name];
-    if (!engine)
+    if (!engine) {
       this._log.debug("Could not get engine: " + name);
+      if (Object.keys)
+        this._log.debug("Engines are: " + JSON.stringify(Object.keys(this._engines)));
+    }
     return engine;
   },
   getAll: function EngMgr_getAll() {
     return [engine for ([name, engine] in Iterator(Engines._engines))];
   },
   getEnabled: function EngMgr_getEnabled() {
     return this.getAll().filter(function(engine) engine.enabled);
   },
@@ -289,17 +292,17 @@ SyncEngine.prototype = {
   _recordObj: CryptoWrapper,
   version: 1,
 
   get storageURL() Svc.Prefs.get("clusterURL") + Svc.Prefs.get("storageAPI") +
     "/" + ID.get("WeaveID").username + "/storage/",
 
   get engineURL() this.storageURL + this.name,
 
-  get cryptoMetaURL() this.storageURL + "crypto/" + this.name,
+  get cryptoKeysURL() this.storageURL + "crypto/keys",
 
   get metaURL() this.storageURL + "meta/global",
 
   get syncID() {
     // Generate a random syncID if we don't have one
     let syncID = Svc.Prefs.get(this.name + ".syncID", "");
     return syncID == "" ? this.syncID = Utils.makeGUID() : syncID;
   },
@@ -343,59 +346,38 @@ SyncEngine.prototype = {
    * changed items.
    */
   getChangedIDs: function getChangedIDs() {
     return this._tracker.changedIDs;
   },
 
   // Create a new record using the store and add in crypto fields
   _createRecord: function SyncEngine__createRecord(id) {
-    let record = this._store.createRecord(id, this.engineURL + "/" + id);
+    let record = this._store.createRecord(id, this.name);
     record.id = id;
-    record.encryption = this.cryptoMetaURL;
+    record.collection = this.name;
     return record;
   },
 
   // Any setup that needs to happen at the beginning of each sync.
-  // Makes sure crypto records and keys are all set-up
   _syncStartup: function SyncEngine__syncStartup() {
-    this._log.trace("Ensuring server crypto records are there");
-
-    // Try getting/unwrapping the crypto record
-    let meta = CryptoMetas.get(this.cryptoMetaURL);
-    if (meta) {
-      try {
-        let pubkey = PubKeys.getDefaultKey();
-        let privkey = PrivKeys.get(pubkey.privateKeyUri);
-        meta.getKey(privkey, ID.get("WeaveCryptoID"));
-      }
-      catch(ex) {
-        // Indicate that we don't have a cryptometa to delete and reupload
-        this._log.debug("Purging bad data after failed unwrap crypto: " + ex);
-        meta = null;
-      }
-    }
-    // Don't proceed if we failed to get the crypto meta for reasons not 404
-    else if (CryptoMetas.response.status != 404) {
-      let resp = CryptoMetas.response;
-      resp.failureCode = ENGINE_METARECORD_DOWNLOAD_FAIL;
-      throw resp;
-    }
 
     // Determine if we need to wipe on outdated versions
     let metaGlobal = Records.get(this.metaURL);
     let engines = metaGlobal.payload.engines || {};
     let engineData = engines[this.name] || {};
 
+    let needsWipe = false;
+
     // Assume missing versions are 0 and wipe the server
     if ((engineData.version || 0) < this.version) {
       this._log.debug("Old engine data: " + [engineData.version, this.version]);
 
       // Prepare to clear the server and upload everything
-      meta = null;
+      needsWipe = true;
       this.syncID = "";
 
       // Set the newer version and newly generated syncID
       engineData.version = this.version;
       engineData.syncID = this.syncID;
 
       // Put the new data back into meta/global and mark for upload
       engines[this.name] = engineData;
@@ -410,35 +392,20 @@ SyncEngine.prototype = {
     }
     // Changes to syncID mean we'll need to upload everything
     else if (engineData.syncID != this.syncID) {
       this._log.debug("Engine syncIDs: " + [engineData.syncID, this.syncID]);
       this.syncID = engineData.syncID;
       this._resetClient();
     };
 
-    // Delete any existing data and reupload on bad version or missing meta
-    if (meta == null) {
+    // Delete any existing data and reupload on bad version or missing meta.
+    // No crypto component here...? We could regenerate per-collection keys...
+    if (needsWipe) {
       this.wipeServer(true);
-
-      // Generate a new crypto record
-      let symkey = Svc.Crypto.generateRandomKey();
-      let pubkey = PubKeys.getDefaultKey();
-      meta = new CryptoMeta(this.cryptoMetaURL);
-      meta.addUnwrappedKey(pubkey, symkey);
-      let res = new Resource(meta.uri);
-      let resp = res.put(meta);
-      if (!resp.success) {
-        this._log.debug("Metarecord upload fail:" + resp);
-        resp.failureCode = ENGINE_METARECORD_UPLOAD_FAIL;
-        throw resp;
-      }
-
-      // Cache the cryto meta that we just put on the server
-      CryptoMetas.set(meta.uri, meta);
     }
 
     // Save objects that need to be uploaded in this._modified. We also save
     // the timestamp of this fetch in this.lastSyncLocal. As we successfully
     // upload objects we remove them from this._modified. If an error occurs
     // or any objects fail to upload, they will remain in this._modified. At
     // the end of a sync, or after an error, we add all objects remaining in
     // this._modified to the tracker.
@@ -482,27 +449,24 @@ SyncEngine.prototype = {
 
     let count = {applied: 0, reconciled: 0};
     let handled = [];
     newitems.recordHandler = Utils.bind2(this, function(item) {
       // Grab a later last modified if possible
       if (this.lastModified == null || item.modified > this.lastModified)
         this.lastModified = item.modified;
 
+      // Track the collection for the WBO.
+      item.collection = this.name;
+      
       // Remember which records were processed
       handled.push(item.id);
 
       try {
-        // Short-circuit the key URI to the engine's one in case the WBO's
-        // might be wrong due to relative URI confusions (bug 600995).
-        try {
-          item.decrypt(ID.get("WeaveCryptoID"), this.cryptoMetaURL);
-        } catch (ex) {
-          item.decrypt(ID.get("WeaveCryptoID"), item.encryption);
-        }
+        item.decrypt();
         if (this._reconcile(item)) {
           count.applied++;
           this._tracker.ignoreAll = true;
           this._store.applyIncoming(item);
         } else {
           count.reconciled++;
           this._log.trace("Skipping reconciled incoming item " + item.id);
         }
@@ -701,17 +665,17 @@ SyncEngine.prototype = {
       });
 
       for each (let id in this._modifiedIDs) {
         try {
           let out = this._createRecord(id);
           if (this._log.level <= Log4Moz.Level.Trace)
             this._log.trace("Outgoing: " + out);
 
-          out.encrypt(ID.get("WeaveCryptoID"));
+          out.encrypt();
           up.pushData(out);
         }
         catch(ex) {
           this._log.warn("Error creating record: " + Utils.exceptionStr(ex));
         }
 
         // Partial upload
         if ((++count % MAX_UPLOAD_RECORDS) == 0)
@@ -784,19 +748,18 @@ 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);
     test.limit = 1;
     test.sort = "newest";
     test.full = true;
-    let self = this;
     test.recordHandler = function(record) {
-      record.decrypt(ID.get("WeaveCryptoID"), self.cryptoMetaURL);
+      record.decrypt();
       canDecrypt = true;
     };
 
     // Any failure fetching/decrypting will just result in false
     try {
       this._log.trace("Trying to decrypt a record from the server..");
       test.get();
     }
@@ -806,15 +769,13 @@ SyncEngine.prototype = {
 
     return canDecrypt;
   },
 
   _resetClient: function SyncEngine__resetClient() {
     this.resetLastSync();
   },
 
-  wipeServer: function wipeServer(ignoreCrypto) {
+  wipeServer: function wipeServer() {
     new Resource(this.engineURL).delete();
-    if (!ignoreCrypto)
-      new Resource(this.cryptoMetaURL).delete();
     this._resetClient();
   }
 };
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -752,96 +752,96 @@ BookmarksStore.prototype = {
     try {
       return Utils.anno(id, "bookmarks/staticTitle");
     } catch (e) {
       return "";
     }
   },
 
   // Create a record starting from the weave id (places guid)
-  createRecord: function createRecord(guid, uri) {
-    let placeId = idForGUID(guid);
+  createRecord: function createRecord(id, collection) {
+    let placeId = idForGUID(id);
     let record;
     if (placeId <= 0) { // deleted item
-      record = new PlacesItem(uri);
+      record = new PlacesItem(collection, id);
       record.deleted = true;
       return record;
     }
 
     let parent = Svc.Bookmark.getFolderIdForItem(placeId);
     switch (this._bms.getItemType(placeId)) {
     case this._bms.TYPE_BOOKMARK:
       let bmkUri = this._bms.getBookmarkURI(placeId).spec;
       if (this._ms && this._ms.hasMicrosummary(placeId)) {
-        record = new BookmarkMicsum(uri);
+        record = new BookmarkMicsum(collection, id);
         let micsum = this._ms.getMicrosummary(placeId);
         record.generatorUri = micsum.generator.uri.spec; // breaks local generators
         record.staticTitle = this._getStaticTitle(placeId);
       }
       else {
         if (bmkUri.search(/^place:/) == 0) {
-          record = new BookmarkQuery(uri);
+          record = new BookmarkQuery(collection, id);
 
           // Get the actual tag name instead of the local itemId
           let folder = bmkUri.match(/[:&]folder=(\d+)/);
           try {
             // There might not be the tag yet when creating on a new client
             if (folder != null) {
               folder = folder[1];
               record.folderName = this._bms.getItemTitle(folder);
               this._log.debug("query id: " + folder + " = " + record.folderName);
             }
           }
           catch(ex) {}
         }
         else
-          record = new Bookmark(uri);
+          record = new Bookmark(collection, id);
         record.title = this._bms.getItemTitle(placeId);
       }
 
       record.parentName = Svc.Bookmark.getItemTitle(parent);
       record.bmkUri = bmkUri;
       record.tags = this._getTags(record.bmkUri);
       record.keyword = this._bms.getKeywordForBookmark(placeId);
       record.description = this._getDescription(placeId);
       record.loadInSidebar = this._isLoadInSidebar(placeId);
       break;
 
     case this._bms.TYPE_FOLDER:
       if (this._ls.isLivemark(placeId)) {
-        record = new Livemark(uri);
+        record = new Livemark(collection, id);
 
         let siteURI = this._ls.getSiteURI(placeId);
         if (siteURI != null)
           record.siteUri = siteURI.spec;
         record.feedUri = this._ls.getFeedURI(placeId).spec;
 
       } else {
-        record = new BookmarkFolder(uri);
+        record = new BookmarkFolder(collection, id);
       }
 
       record.parentName = Svc.Bookmark.getItemTitle(parent);
       record.title = this._bms.getItemTitle(placeId);
       record.description = this._getDescription(placeId);
       break;
 
     case this._bms.TYPE_SEPARATOR:
-      record = new BookmarkSeparator(uri);
+      record = new BookmarkSeparator(collection, id);
       // Create a positioning identifier for the separator
       record.parentName = Svc.Bookmark.getItemTitle(parent);
       record.pos = Svc.Bookmark.getItemIndex(placeId);
       break;
 
     case this._bms.TYPE_DYNAMIC_CONTAINER:
-      record = new PlacesItem(uri);
+      record = new PlacesItem(collection, id);
       this._log.warn("Don't know how to serialize dynamic containers yet");
       break;
 
     default:
-      record = new PlacesItem(uri);
+      record = new PlacesItem(collection, id);
       this._log.warn("Unknown item type, cannot serialize: " +
                      this._bms.getItemType(placeId));
     }
 
     record.parentid = this._getParentGUIDForId(placeId);
     record.predecessorid = this._getPredecessorGUIDForId(placeId);
     record.sortindex = this._calculateIndex(record);
 
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -175,27 +175,27 @@ ClientStore.prototype = {
   update: function update(record) {
     // Only grab commands from the server; local name/type always wins
     if (record.id == Clients.localID)
       Clients.localCommands = record.commands;
     else
       this._remoteClients[record.id] = record.cleartext;
   },
 
-  createRecord: function createRecord(guid, uri) {
-    let record = new ClientsRec(uri);
+  createRecord: function createRecord(id, collection) {
+    let record = new ClientsRec(collection, id);
 
     // Package the individual components into a record for the local client
-    if (guid == Clients.localID) {
+    if (id == Clients.localID) {
       record.name = Clients.localName;
       record.type = Clients.localType;
       record.commands = Clients.localCommands;
     }
     else
-      record.cleartext = this._remoteClients[guid];
+      record.cleartext = this._remoteClients[id];
 
     return record;
   },
 
   itemExists: function itemExists(id) id in this.getAllIDs(),
 
   getAllIDs: function getAllIDs() {
     let ids = {};
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -158,19 +158,19 @@ FormStore.prototype = {
   changeItemID: function FormStore_changeItemID(oldID, newID) {
     FormWrapper.replaceGUID(oldID, newID);
   },
 
   itemExists: function FormStore_itemExists(id) {
     return FormWrapper.hasGUID(id);
   },
 
-  createRecord: function createRecord(guid, uri) {
-    let record = new FormRec(uri);
-    let entry = FormWrapper.getEntry(guid);
+  createRecord: function createRecord(id, collection) {
+    let record = new FormRec(collection, id);
+    let entry = FormWrapper.getEntry(id);
     if (entry != null) {
       record.name = entry.name;
       record.value = entry.value
     }
     else
       record.deleted = true;
     return record;
   },
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -378,19 +378,19 @@ HistoryStore.prototype = {
 
   urlExists: function HistStore_urlExists(url) {
     if (typeof(url) == "string")
       url = Utils.makeURI(url);
     // Don't call isVisited on a null URL to work around crasher bug 492442.
     return url ? this._hsvc.isVisited(url) : false;
   },
 
-  createRecord: function createRecord(guid, uri) {
-    let foo = this._findURLByGUID(guid);
-    let record = new HistoryRec(uri);
+  createRecord: function createRecord(id, collection) {
+    let foo = this._findURLByGUID(id);
+    let record = new HistoryRec(collection, id);
     if (foo) {
       record.histUri = foo.url;
       record.title = foo.title;
       record.sortindex = foo.frecency;
       record.visits = this._getVisits(record.histUri);
     }
     else
       record.deleted = true;
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -164,19 +164,19 @@ PasswordStore.prototype = {
   },
 
   itemExists: function PasswordStore__itemExists(id) {
     if (this._getLoginFromGUID(id))
       return true;
     return false;
   },
 
-  createRecord: function createRecord(guid, uri) {
-    let record = new LoginRec(uri);
-    let login = this._getLoginFromGUID(guid);
+  createRecord: function createRecord(id, collection) {
+    let record = new LoginRec(collection, id);
+    let login = this._getLoginFromGUID(id);
 
     if (login) {
       record.hostname = login.hostname;
       record.formSubmitURL = login.formSubmitURL;
       record.httpRealm = login.httpRealm;
       record.username = login.username;
       record.password = login.password;
       record.usernameField = login.usernameField;
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -184,20 +184,20 @@ PrefStore.prototype = {
   changeItemID: function PrefStore_changeItemID(oldID, newID) {
     this._log.trace("PrefStore GUID is constant!");
   },
 
   itemExists: function FormStore_itemExists(id) {
     return (id === Svc.AppInfo.ID);
   },
 
-  createRecord: function createRecord(guid, uri) {
-    let record = new PrefRec(uri);
+  createRecord: function createRecord(id, collection) {
+    let record = new PrefRec(collection, id);
 
-    if (guid == Svc.AppInfo.ID) {
+    if (id == Svc.AppInfo.ID) {
       record.value = this._getAllPrefs();
     } else {
       record.deleted = true;
     }
 
     return record;
   },
 
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -151,18 +151,18 @@ TabStore.prototype = {
           lastUsed: tab.extData && tab.extData.weaveLastUsed || 0
         });
       });
     });
 
     return allTabs;
   },
 
-  createRecord: function createRecord(guid, uri) {
-    let record = new TabSetRecord(uri);
+  createRecord: function createRecord(id, collection) {
+    let record = new TabSetRecord(collection, id);
     record.clientName = Clients.localName;
 
     // Don't provide any tabs to compare against and ignore the update later.
     if (Svc.Private.privateBrowsingEnabled && !PBPrefs.get("autostart")) {
       record.tabs = [];
       return record;
     }
 
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -41,16 +41,23 @@ const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/ext/Sync.js");
 Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
+// Avoid circular import.
+__defineGetter__("Service", function() {
+  delete this.Service;
+  Cu.import("resource://services-sync/service.js", this);
+  return this.Service;
+});
+
 Utils.lazy(this, 'ID', IDManager);
 
 // For storing identities we'll use throughout Weave
 function IDManager() {
   this._ids = {};
 }
 IDManager.prototype = {
   get: function IDMgr_get(name) {
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -14,16 +14,17 @@
  * The Original Code is Sync
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2007
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  * Philipp von Weitershausen <philipp@weitershausen.de>
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -36,17 +37,18 @@
 
 const EXPORTED_SYMBOLS = ['Weave'];
 
 let Weave = {};
 Components.utils.import("resource://services-sync/constants.js", Weave);
 let lazies = {
   "auth.js":              ['Auth', 'BrokenBasicAuthenticator',
                            'BasicAuthenticator', 'NoOpAuthenticator'],
-  "base_records/keys.js": ['PubKey', 'PrivKey', 'PubKeys', 'PrivKeys'],
+  "base_records/crypto.js":
+                          ["CollectionKeys", "BulkKeyBundle", "SyncKeyBundle"],
   "engines.js":           ['Engines', 'Engine', 'SyncEngine'],
   "engines/bookmarks.js": ['BookmarksEngine', 'BookmarksSharingManager'],
   "engines/clients.js":   ["Clients"],
   "engines/forms.js":     ["FormEngine"],
   "engines/history.js":   ["HistoryEngine"],
   "engines/prefs.js":     ["PrefsEngine"],
   "engines/passwords.js": ["PasswordEngine"],
   "engines/tabs.js":      ["TabEngine"],
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -16,16 +16,17 @@
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2007
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Dan Mills <thunder@mozilla.com>
  *  Myk Melez <myk@mozilla.org>
  *  Anant Narayanan <anant@kix.in>
+ *  Richard Newman <rnewman@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -45,20 +46,22 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 // how long we should wait before actually syncing on idle
 const IDLE_TIME = 5; // xxxmpc: in seconds, should be preffable
 
 // How long before refreshing the cluster
 const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes
 
+// How long a key to generate from an old passphrase.
+const PBKDF2_KEY_BYTES = 16;
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/auth.js");
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/base_records/wbo.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/ext/Sync.js");
 Cu.import("resource://services-sync/ext/Preferences.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/log4moz.js");
@@ -78,17 +81,16 @@ function WeaveSvc() {
   this._notify = Utils.notify("weave:service:");
 }
 WeaveSvc.prototype = {
 
   _lock: Utils.lock,
   _catch: Utils.catch,
   _locked: false,
   _loggedIn: false,
-  keyGenEnabled: true,
 
   get account() Svc.Prefs.get("account", this.username),
   set account(value) {
     if (value) {
       value = value.toLowerCase();
       Svc.Prefs.set("account", value);
     } else {
       Svc.Prefs.reset("account");
@@ -122,19 +124,20 @@ WeaveSvc.prototype = {
 
     // FIXME: need to also call this whenever the username pref changes
     this._updateCachedURLs();
   },
 
   get password() ID.get("WeaveID").password,
   set password(value) ID.get("WeaveID").password = value,
 
-  get passphrase() ID.get("WeaveCryptoID").password,
-  set passphrase(value) ID.get("WeaveCryptoID").password = value,
-  get passphraseUTF8() ID.get("WeaveCryptoID").passwordUTF8,
+  get passphrase() ID.get("WeaveCryptoID").keyStr,
+  set passphrase(value) ID.get("WeaveCryptoID").keyStr = value,
+  
+  get syncKeyBundle() ID.get("WeaveCryptoID"),
 
   get serverURL() Svc.Prefs.get("serverURL"),
   set serverURL(value) {
     // Only do work if it's actually changing
     if (value == this.serverURL)
       return;
 
     // A new server most likely uses a different cluster, so clear that
@@ -225,18 +228,17 @@ WeaveSvc.prototype = {
     let storageAPI = this.clusterURL + Svc.Prefs.get("storageAPI") + "/";
     this.userBaseURL = storageAPI + this.username + "/";
     this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
 
     // Generate and cache various URLs under the storage API for this user
     this.infoURL = this.userBaseURL + "info/collections";
     this.storageURL = this.userBaseURL + "storage/";
     this.metaURL = this.storageURL + "meta/global";
-    PubKeys.defaultKeyUri = this.storageURL + "keys/pubkey";
-    PrivKeys.defaultKeyUri = this.storageURL + "keys/privkey";
+    this.cryptoKeysURL = this.storageURL + "crypto/keys";
   },
 
   _checkCrypto: function WeaveSvc__checkCrypto() {
     let ok = false;
 
     try {
       let iv = Svc.Crypto.generateRandomIV();
       if (iv.length == 24)
@@ -286,17 +288,17 @@ WeaveSvc.prototype = {
     // Create Weave identities (for logging in, and for encryption)
     let id = ID.get("WeaveID");
     if (!id)
       id = ID.set("WeaveID", new Identity(PWDMGR_PASSWORD_REALM, this.username));
     Auth.defaultAuthenticator = new BasicAuthenticator(id);
 
     if (!ID.get("WeaveCryptoID"))
       ID.set("WeaveCryptoID",
-             new Identity(PWDMGR_PASSPHRASE_REALM, this.username));
+             new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, this.username));
 
     this._updateCachedURLs();
 
     let status = this._checkSetup();
     if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED)
       Svc.Obs.notify("weave:engine:start-tracking");
 
     // Applications can specify this preference if they want autoconnect
@@ -549,53 +551,194 @@ WeaveSvc.prototype = {
       if (this._setCluster()) {
         Svc.Prefs.set("lastClusterUpdate", cTime.toString());
         return true;
       }
     }
     return false;
   },
 
+  /**
+   * Perform the info fetch as part of a login or key fetch.
+   */
+  _fetchInfo: function _fetchInfo(url, logout) {
+    let infoURL = url || this.infoURL;
+    
+    let info = new Resource(infoURL).get();
+    if (!info.success) {
+      if (info.status == 401) {
+        if (logout) {
+          this.logout();
+          Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+        }
+      }
+      throw "aborting sync, failed to get collections";
+    }
+    return info;
+  },
+
+  verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) {
+    
+    this._log.debug("Fetching and verifying -- or generating -- symmetric keys.");
+    
+    // Don't allow empty/missing passphrase.
+    // Furthermore, we assume that our sync key is already upgraded,
+    // and fail if that assumption is invalidated.
+    
+    let syncKey = this.syncKeyBundle;
+    if (!syncKey) {
+      this._log.error("No sync key: cannot fetch symmetric keys.");
+      Status.login = LOGIN_FAILED_NO_PASSPHRASE;
+      Status.sync = CREDENTIALS_CHANGED;             // For want of a better option.
+      return false;
+    }
+    
+    // Not sure this validation is necessary now.
+    if (!Utils.isPassphrase(syncKey.keyStr)) {
+      this._log.warn("Sync key input is invalid: cannot fetch symmetric keys.");
+      Status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
+      Status.sync = CREDENTIALS_CHANGED;
+      return false;
+    }
+
+    try {
+      if (!infoResponse)
+        infoResponse = this._fetchInfo();    // Will throw an exception on failure.
+      
+      // This only applies when the server is already at version 4.
+      if (infoResponse.status != 200) {
+        this._log.warn("info/collections returned non-200 response. Failing key fetch.");
+        Status.login = LOGIN_FAILED_SERVER_ERROR;
+        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.");
+        
+        // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this.
+            
+        // Fetch storage/crypto/keys.
+        let cryptoKeys;
+        
+        if (infoCollections && ('crypto' in infoCollections)) {
+          try {
+            cryptoKeys = new CryptoWrapper("crypto", "keys");
+            let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
+            
+            if (cryptoResp.success) {
+              // On success, pass to CollectionKeys.
+              CollectionKeys.updateContents(syncKey, cryptoKeys);
+              return true;
+            }
+            else if (cryptoResp.status == 404) {
+              // On failure, ask CollectionKeys 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.
+              this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys.");
+              Status.login = LOGIN_FAILED_SERVER_ERROR;
+              return false;
+            }
+          }
+          catch (ex) {
+            this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys.");
+            // TODO: Um, what exceptions might we get here? Should we re-throw any?
+            
+            // One kind of exception: HMAC failure.
+            let hmacFail = "Record SHA256 HMAC mismatch: ";
+            if (ex && ex.substr && (ex.substr(0, hmacFail.length) == hmacFail)) {
+              Status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
+              Status.sync = CREDENTIALS_CHANGED;
+            }
+            else
+              // Assume that every other failure is network-related.
+              Status.login = LOGIN_FAILED_NETWORK_ERROR;
+            return false;
+          }
+        }
+        else {
+          this._log.info("... 'crypto' is not a reported collection. Generating new keys.");
+        }
+
+        if (!cryptoKeys) {
+          // Must have got a 404, or no reported collection.
+          // Better make some and upload them.
+          this.generateNewSymmetricKeys();
+          return true;
+        }
+        
+        // Last-ditch case.
+        return false;
+      }
+      else {
+        // No update needed: we're good!
+        return true;
+      }
+          
+    } catch (e) {
+      // This means no keys are present, or there's a network error.
+      return false;
+    }
+  },
+  
   verifyLogin: function verifyLogin()
     this._notify("verify-login", "", function() {
       // Make sure we have a cluster to verify against
       // this is a little weird, if we don't get a node we pretend
       // to succeed, since that probably means we just don't have storage
       if (this.clusterURL == "" && !this._setCluster()) {
         Status.sync = NO_SYNC_NODE_FOUND;
         Svc.Obs.notify("weave:service:sync:delayed");
         return true;
       }
 
       if (!this.username) {
+        this._log.warn("No username in verifyLogin.");
         Status.login = LOGIN_FAILED_NO_USERNAME;
         return false;
       }
 
       try {
+        // Fetch collection info on every startup.
         let test = new Resource(this.infoURL).get();
         switch (test.status) {
           case 200:
             // The user is authenticated.
+
+            // We have no way of verifying the passphrase right now,
+            // so wait until remoteSetup to do so.
+            // Just make the most trivial checks.
             if (!this.passphrase) {
+              this._log.warn("No passphrase in verifyLogin.");
               Status.login = LOGIN_FAILED_NO_PASSPHRASE;
               return false;
             }
 
-            // We also have a passphrase, so check it now.
-            if (!this._verifyPassphrase()) {
-              Status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
-              return false;
-            }
+            // Go ahead and do remote setup, so that we can determine 
+            // conclusively that our passphrase is correct.
+            if (this._remoteSetup()) {
 
-            // Username/password and passphrase all verified
+              // Username/password verified.
             Status.login = LOGIN_SUCCEEDED;
             return true;
+            }
+            
+            this._log.warn("Remote setup failed.");
+            // Remote setup must have failed.
+            return false;
 
           case 401:
+            this._log.warn("401: login failed.");
             // Login failed.  If the password contains non-ASCII characters,
             // perhaps the server password is an old low-byte only one?
             let id = ID.get('WeaveID');
             if (id.password != id.passwordUTF8) {
               let res = new Resource(this.infoURL);
               let auth = new BrokenBasicAuthenticator(id);
               res.authenticator = auth;
               test = res.get();
@@ -630,46 +773,32 @@ WeaveSvc.prototype = {
       catch (ex) {
         // Must have failed on some network issue
         this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
         Status.login = LOGIN_FAILED_NETWORK_ERROR;
         return false;
       }
     })(),
 
-  _verifyPassphrase: function _verifyPassphrase()
-    this._catch(this._notify("verify-passphrase", "", function() {
-      // Don't allow empty/missing passphrase
-      if (!this.passphrase)
-        return false;
-
-      try {
-        let pubkey = PubKeys.getDefaultKey();
-        let privkey = PrivKeys.get(pubkey.privateKeyUri);
-        let result = Svc.Crypto.verifyPassphrase(
-          privkey.payload.keyData, this.passphraseUTF8,
-          privkey.payload.salt, privkey.payload.iv
-        );
-        if (result)
-          return true;
-
-        // Passphrase validation failed. Perhaps because the keys are
-        // based on an old low-byte only passphrase?
-        result = Svc.Crypto.verifyPassphrase(
-          privkey.payload.keyData, this.passphrase,
-          privkey.payload.salt, privkey.payload.iv
-        );
-        if (result)
-          this._needUpdatedKeys = true;
-        return result;
-      } catch (e) {
-        // this means no keys are present (or there's a network error)
-        return true;
-      }
-    }))(),
+  generateNewSymmetricKeys:
+  function WeaveSvc_generateNewSymmetricKeys() {
+    this._log.info("Generating new keys....");
+    CollectionKeys.generateNewKeys();
+    let wbo = CollectionKeys.asWBO("crypto", "keys");
+    this._log.info("Encrypting new key bundle. Modified time is " + wbo.modified);
+    wbo.encrypt(this.syncKeyBundle);
+    
+    this._log.info("Uploading...");
+    let uploadRes = wbo.upload(this.cryptoKeysURL);
+    if (uploadRes.status >= 400) {
+      this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!");
+      throw new Error("Unable to upload symmetric keys.");
+    }
+    this._log.info("Got status " + uploadRes.status);
+  },
 
   changePassword: function WeaveSvc_changePassword(newpass)
     this._notify("changepwd", "", function() {
       let url = this.userAPI + this.username + "/password";
       try {
         let resp = new Resource(url).post(Utils.encodeUTF8(newpass));
         if (resp.status != 200) {
           this._log.debug("Password change failed: " + resp);
@@ -677,52 +806,50 @@ WeaveSvc.prototype = {
         }
       }
       catch(ex) {
         // Must have failed on some network issue
         this._log.debug("changePassword failed: " + Utils.exceptionStr(ex));
         return false;
       }
 
-      // Save the new password for requests and login manager
+      // Save the new password for requests and login manager.
       this.password = newpass;
       this.persistLogin();
       return true;
     })(),
 
   changePassphrase: function WeaveSvc_changePassphrase(newphrase)
     this._catch(this._notify("changepph", "", function() {
-      /* Wipe */
+      /* Wipe. */
       this.wipeServer();
-      PubKeys.clearCache();
-      PrivKeys.clearCache();
 
       this.logout();
 
-      /* Set this so UI is updated on next run */
+      /* Set this so UI is updated on next run. */
       this.passphrase = newphrase;
       this.persistLogin();
 
-      /* Login in sync: this also generates new keys */
+      /* Login and sync. This also generates new keys. */
       this.login();
       this.sync(true);
       return true;
     }))(),
 
   startOver: function() {
     // Set a username error so the status message shows "set up..."
     Status.login = LOGIN_FAILED_NO_USERNAME;
     this.logout();
-    // Reset all engines
+    // Reset all engines.
     this.resetClient();
-    // Reset Weave prefs
+    // Reset Weave prefs.
     this._ignorePrefObserver = true;
     Svc.Prefs.resetBranch("");
     this._ignorePrefObserver = false;
-    // set lastversion pref
+    
     Svc.Prefs.set("lastversion", WEAVE_VERSION);
     // Find weave logins and remove them.
     this.password = "";
     this.passphrase = "";
     Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(login) {
       Svc.Login.removeLogin(login);
     });
     Svc.Obs.notify("weave:service:start-over");
@@ -738,44 +865,45 @@ WeaveSvc.prototype = {
     }
   },
 
   _autoConnect: let (attempts = 0) function _autoConnect() {
     let reason = 
       Utils.mpLocked() ? "master password still locked"
                        : this._checkSync([kSyncNotLoggedIn, kFirstSyncChoiceNotMade]);
 
-    // Can't autoconnect if we're missing these values
+    // Can't autoconnect if we're missing these values.
     if (!reason) {
       if (!this.username || !this.password || !this.passphrase)
         return;
 
-      // Nothing more to do on a successful login
+      // Nothing more to do on a successful login.
       if (this.login())
         return;
     }
 
-    // Something failed, so try again some time later
+    // Something failed, so try again some time later.
     let interval = this._calculateBackoff(++attempts, 60 * 1000);
     this._log.debug("Autoconnect failed: " + (reason || Status.login) +
       "; retry in " + Math.ceil(interval / 1000) + " sec.");
     Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer");
   },
 
   persistLogin: function persistLogin() {
-    // Canceled master password prompt can prevent these from succeeding
+    // Canceled master password prompt can prevent these from succeeding.
     try {
       ID.get("WeaveID").persist();
       ID.get("WeaveCryptoID").persist();
     }
     catch(ex) {}
   },
 
   login: function WeaveSvc_login(username, password, passphrase)
-    this._catch(this._lock(this._notify("login", "", function() {
+    this._catch(this._lock("service.js: login", 
+          this._notify("login", "", function() {
       this._loggedIn = false;
       if (Svc.IO.offline)
         throw "Application is offline, login should not be called";
 
       let initialStatus = this._checkSetup();
       if (username)
         this.username = username;
       if (password)
@@ -794,37 +922,37 @@ WeaveSvc.prototype = {
 
       this._log.info("Logging in user " + this.username);
 
       if (!this.verifyLogin()) {
         // verifyLogin sets the failure states here.
         throw "Login failed: " + Status.login;
       }
 
-      // No need to try automatically connecting after a successful login
+      // No need to try automatically connecting after a successful login.
       if (this._autoTimer)
         this._autoTimer.clear();
 
       this._loggedIn = true;
-      // Try starting the sync timer now that we're logged in
+      // Try starting the sync timer now that we're logged in.
       this._checkSyncStatus();
       Svc.Prefs.set("autoconnect", true);
 
       return true;
     })))(),
 
   logout: function WeaveSvc_logout() {
-    // No need to do anything if we're already logged out
+    // No need to do anything if we're already logged out.
     if (!this._loggedIn)
       return;
 
     this._log.info("Logging out");
     this._loggedIn = false;
 
-    // Cancel the sync timer now that we're logged out
+    // Cancel the sync timer now that we're logged out.
     this._checkSyncStatus();
     Svc.Prefs.set("autoconnect", false);
 
     Svc.Obs.notify("weave:service:logout:finish");
   },
 
   _errorStr: function WeaveSvc__errorStr(code) {
     switch (code.toString()) {
@@ -871,17 +999,17 @@ WeaveSvc.prototype = {
           return "available";
         else if (data == "1")
           return "notAvailable";
       }
 
     }
     catch(ex) {}
 
-    // Convert to the error string, or default to generic on exception
+    // Convert to the error string, or default to generic on exception.
     return this._errorStr(data);
   },
 
   createAccount: function createAccount() {
     // Backwards compat with the Firefox UI. Change to signature to
     // (email, password, captchaChallenge, captchaResponse) once
     // bug 595066 has landed.
     let username, email, password, captchaChallenge, captchaResponse;
@@ -922,22 +1050,22 @@ WeaveSvc.prototype = {
     }
     catch(ex) {
       this._log.warn("Failed to create account: " + ex);
     }
 
     return error;
   },
 
-  // stuff we need to to after login, before we can really do
-  // anything (e.g. key setup)
-  _remoteSetup: function WeaveSvc__remoteSetup() {
+  // Stuff we need to do after login, before we can really do
+  // anything (e.g. key setup).
+  _remoteSetup: function WeaveSvc__remoteSetup(infoResponse) {
     let reset = false;
 
-    this._log.trace("Fetching global metadata record");
+    this._log.debug("Fetching global metadata record");
     let meta = Records.get(this.metaURL);
 
     let remoteVersion = (meta && meta.payload.storageVersion)?
       meta.payload.storageVersion : "";
 
     this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:",
       STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" "));
 
@@ -956,117 +1084,71 @@ WeaveSvc.prototype = {
         return false;
       }
 
       if (!meta)
         this._log.info("No metadata record, server wipe needed");
       if (meta && !meta.payload.syncID)
         this._log.warn("No sync id, server wipe needed");
 
-      if (!this.keyGenEnabled) {
-        this._log.info("...and key generation is disabled.  Not wiping. " +
-                       "Aborting sync.");
-        Status.sync = DESKTOP_VERSION_OUT_OF_DATE;
-        return false;
-      }
       reset = true;
+      
       this._log.info("Wiping server data");
       this._freshStart();
 
       if (status == 404)
-        this._log.info("Metadata record not found, server wiped to ensure " +
+        this._log.info("Metadata record not found, server was wiped to ensure " +
                        "consistency.");
       else // 200
         this._log.info("Wiped server; incompatible metadata: " + remoteVersion);
 
+      return true;
     }
     else if (remoteVersion > STORAGE_VERSION) {
       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.resetClient();
       this.syncID = meta.payload.syncID;
       this._log.debug("Clear cached values and take syncId: " + this.syncID);
 
-      // XXX Bug 531005 Wait long enough to allow potentially another concurrent
-      // sync to finish generating the keypair and uploading them
-      Sync.sleep(15000);
+      if (!this.upgradeSyncKey(meta.payload.syncID)) {
+        this._log.warn("Failed to upgrade sync key. Failing remote setup.");
+        return false;
+      }
+      
+      if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
+        this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
+        return false;
+      }
 
       // bug 545725 - re-verify creds and fail sanely
       if (!this.verifyLogin()) {
         Status.sync = CREDENTIALS_CHANGED;
         this._log.info("Credentials have changed, aborting sync and forcing re-login.");
         return false;
       }
-    }
 
-    let needKeys = true;
-    let pubkey = PubKeys.getDefaultKey();
-    if (!pubkey)
-      this._log.debug("Could not get public key");
-    else if (pubkey.keyData == null)
-      this._log.debug("Public key has no key data");
-    else {
-      // make sure we have a matching privkey
-      let privkey = PrivKeys.get(pubkey.privateKeyUri);
-      if (!privkey)
-        this._log.debug("Could not get private key");
-      else if (privkey.keyData == null)
-        this._log.debug("Private key has no key data");
-      else
         return true;
     }
-
-    if (needKeys) {
-      if (PubKeys.response.status != 404 && PrivKeys.response.status != 404) {
-        this._log.warn("Couldn't download keys from server, aborting sync");
-        this._log.debug("PubKey HTTP status: " + PubKeys.response.status);
-        this._log.debug("PrivKey HTTP status: " + PrivKeys.response.status);
-        this._checkServerError(PubKeys.response);
-        this._checkServerError(PrivKeys.response);
-        Status.sync = KEYS_DOWNLOAD_FAIL;
+    else {
+      if (!this.upgradeSyncKey(meta.payload.syncID)) {
+        this._log.warn("Failed to upgrade sync key. Failing remote setup.");
         return false;
       }
 
-      if (!this.keyGenEnabled) {
-        this._log.warn("Couldn't download keys from server, and key generation" +
-                       "is disabled.  Aborting sync");
-        Status.sync = NO_KEYS_NO_KEYGEN;
+      if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
+        this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
         return false;
       }
 
-      if (!reset) {
-        this._log.warn("Calling freshStart from !reset case.");
-        this._freshStart();
-        this._log.info("Server data wiped to ensure consistency due to missing keys");
-      }
-
-      let passphrase = ID.get("WeaveCryptoID");
-      if (passphrase.password) {
-        let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri,
-                                         PrivKeys.defaultKeyUri);
-        try {
-          // Upload and cache the keypair
-          PubKeys.uploadKeypair(keys);
-          PubKeys.set(keys.pubkey.uri, keys.pubkey);
-          PrivKeys.set(keys.privkey.uri, keys.privkey);
           return true;
-        } catch (e) {
-          Status.sync = KEYS_UPLOAD_FAIL;
-          this._log.error("Could not upload keys: " + Utils.exceptionStr(e));
-        }
-      } else {
-        Status.sync = SETUP_FAILED_NO_PASSPHRASE;
-        this._log.warn("Could not get encryption passphrase");
-      }
     }
-
-    return false;
   },
 
   /**
    * Determine if a sync should run.
    * 
    * @param ignore [optional]
    *        array of reasons to ignore when checking
    *
@@ -1115,83 +1197,25 @@ WeaveSvc.prototype = {
   _checkSyncStatus: function WeaveSvc__checkSyncStatus() {
     // Should we be syncing now, if not, cancel any sync timers and return
     // if we're in backoff, we'll schedule the next sync
     if (this._checkSync([kSyncBackoffNotMet])) {
       this._clearSyncTriggers();
       return;
     }
 
-    if (this._needUpdatedKeys)
-      this._updateKeysToUTF8Passphrase();
-
     // Only set the wait time to 0 if we need to sync right away
     let wait;
     if (this.globalScore > this.syncThreshold) {
       this._log.debug("Global Score threshold hit, triggering sync.");
       wait = 0;
     }
     this._scheduleNextSync(wait);
   },
 
-  _updateKeysToUTF8Passphrase: function _updateKeysToUTF8Passphrase() {
-    // Rewrap private key in UTF-8 encoded passphrase.
-    let pubkey = PubKeys.getDefaultKey();
-    let privkey = PrivKeys.get(pubkey.privateKeyUri);
-
-    this._log.debug("Rewrapping private key with UTF-8 encoded passphrase.");
-    let oldPrivKeyData = privkey.payload.keyData;
-    privkey.payload.keyData = Svc.Crypto.rewrapPrivateKey(
-      oldPrivKeyData, this.passphrase,
-      privkey.payload.salt, privkey.payload.iv, this.passphraseUTF8
-    );
-    let response = new Resource(privkey.uri).put(privkey);
-    if (!response.success) {
-      this._log("Uploading rewrapped private key failed!");
-      this._needUpdatedKeys = false;
-      return;
-    }
-
-    // Recompute HMAC for symmetric bulk keys based on UTF-8 encoded passphrase.
-    let oldHmacKey = Utils.makeHMACKey(this.passphrase);
-    let enginesToWipe = [];
-
-    for each (let engine in Engines.getAll()) {
-      let meta = CryptoMetas.get(engine.cryptoMetaURL);
-      if (!meta)
-        continue;
-
-      this._log.debug("Recomputing HMAC for key at " + engine.cryptoMetaURL
-                      + " with UTF-8 encoded passphrase.");
-      for each (key in meta.keyring) {
-        if (key.hmac != Utils.sha256HMAC(key.wrapped, oldHmacKey)) {
-          this._log.debug("Key SHA256 HMAC mismatch! Wiping server.");
-          enginesToWipe.push(engine.name);
-          meta = null;
-          break;
-        }
-        key.hmac = Utils.sha256HMAC(key.wrapped, meta.hmacKey);
-      }
-
-      if (!meta)
-        continue;
-
-      response = new Resource(meta.uri).put(meta);
-      if (!response.success) {
-        this._log.debug("Key upload failed: " + response);
-      }
-    }
-
-    if (enginesToWipe.length) {
-      this._log.debug("Wiping engines " + enginesToWipe.join(", "));
-      this.wipeRemote(enginesToWipe);
-    }
-    this._needUpdatedKeys = false;
-  },
-
   /**
    * Call sync() on an idle timer
    *
    * delay is optional
    */
   syncOnIdle: function WeaveSvc_syncOnIdle(delay) {
     // No need to add a duplicate idle observer
     if (this._idleTime)
@@ -1214,16 +1238,17 @@ WeaveSvc.prototype = {
         interval = this.nextSync - Date.now();
       // Use the bigger of default sync interval and backoff
       else
         interval = Math.max(this.syncInterval, Status.backoffInterval);
     }
 
     // Start the sync right away if we're already late
     if (interval <= 0) {
+      this._log.debug("Syncing as soon as we're idle.");
       this.syncOnIdle();
       return;
     }
 
     this._log.trace("Next sync in " + Math.ceil(interval / 1000) + " sec.");
     Utils.delay(function() this.syncOnIdle(), interval, this, "_syncTimer");
 
     // Save the next sync time in-case sync is disabled (logout/offline/etc.)
@@ -1327,17 +1352,20 @@ WeaveSvc.prototype = {
     let d = new Date(Date.now() + interval);
     this._log.config("Starting backoff, next sync at:" + d.toString());
   },
 
   /**
    * Sync up engines with the server.
    */
   sync: function sync()
-    this._catch(this._lock(this._notify("sync", "", function() {
+    this._catch(this._lock("service.js: sync", 
+                           this._notify("sync", "", function() {
+
+    this._log.info("In sync().");
 
     let syncStartTime = Date.now();
 
     Status.resetSync();
 
     // Make sure we should sync or record why we shouldn't
     let reason = this._checkSync();
     if (reason) {
@@ -1358,64 +1386,44 @@ WeaveSvc.prototype = {
     this._clearSyncTriggers();
     this.nextSync = 0;
     this.nextHeartbeat = 0;
 
     // reset backoff info, if the server tells us to continue backing off,
     // we'll handle that later
     Status.resetBackoff();
 
-    // Ping the server with a special info request once a day
+    // Ping the server with a special info request once a day.
     let infoURL = this.infoURL;
     let now = Math.floor(Date.now() / 1000);
     let lastPing = Svc.Prefs.get("lastPing", 0);
     if (now - lastPing > 86400) { // 60 * 60 * 24
       infoURL += "?v=" + WEAVE_VERSION;
       Svc.Prefs.set("lastPing", now);
     }
 
     // Figure out what the last modified time is for each collection
-    let info = new Resource(infoURL).get();
-    if (!info.success) {
-      if (info.status == 401) {
-        this.logout();
-        Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-      }
-      throw "aborting sync, failed to get collections";
-    }
-
+    let info = this._fetchInfo(infoURL, true);
     this.globalScore = 0;
 
     // Convert the response to an object and read out the modified times
     for each (let engine in [Clients].concat(Engines.getAll()))
       engine.lastModified = info.obj[engine.name] || 0;
 
-    // If the modified time of crypto records ever changes, clear the cache
-    if (info.obj.crypto != this.cryptoModified) {
-      this._log.debug("Clearing cached crypto records");
-      CryptoMetas.clearCache();
-      this.cryptoModified = info.obj.crypto;
-    }
-
-    // If the modified time of keys records ever changes, clear the cache
-    if (info.obj.keys != this.keysModified) {
-      this._log.debug("Clearing cached keys records");
-      PubKeys.clearCache();
-      PrivKeys.clearCache();
-      this.keysModified = info.obj.keys;
-    }
-
     // If the modified time of the meta record ever changes, clear the cache.
-    if (info.obj.meta != this.metaModified) {
-      this._log.debug("Clearing cached meta record.");
+    // ... unless meta is marked as new.
+    if ((info.obj.meta != this.metaModified) && !Records.get(this.metaURL).isNew) {
+      this._log.debug("Clearing cached meta record. metaModified is " +
+          JSON.stringify(this.metaModified) + ", setting to " +
+          JSON.stringify(info.obj.meta));
       Records.del(this.metaURL);
       this.metaModified = info.obj.meta;
     }
 
-    if (!(this._remoteSetup()))
+    if (!(this._remoteSetup(info)))
       throw "aborting sync, remote setup failed";
 
     // Make sure we have an up-to-date list of clients before sending commands
     this._log.trace("Refreshing client list");
     this._syncEngine(Clients);
 
     // Wipe data in the desired direction if necessary
     switch (Svc.Prefs.get("firstSync")) {
@@ -1434,17 +1442,17 @@ WeaveSvc.prototype = {
     if (Clients.localCommands) {
       try {
         if (!(this.processCommands())) {
           Status.sync = ABORT_SYNC_COMMAND;
           throw "aborting sync, process commands said so";
         }
 
         // Repeat remoteSetup in-case the commands forced us to reset
-        if (!(this._remoteSetup()))
+        if (!(this._remoteSetup(info)))
           throw "aborting sync, remote setup failed after processing commands";
       }
       finally {
         // Always immediately push back the local client (now without commands)
         this._syncEngine(Clients);
       }
     }
 
@@ -1459,17 +1467,17 @@ WeaveSvc.prototype = {
           this._log.info("Aborting sync");
           break;
         }
       }
 
       // Upload meta/global if any engines changed anything
       let meta = Records.get(this.metaURL);
       if (meta.isNew || meta.changed) {
-        new Resource(meta.uri).put(meta);
+        new Resource(this.metaURL).put(meta);
         delete meta.isNew;
         delete meta.changed;
       }
 
       if (this._syncError)
         throw "Some engines did not sync correctly";
       else {
         Svc.Prefs.set("lastSync", new Date().toString());
@@ -1577,35 +1585,91 @@ WeaveSvc.prototype = {
     }
     finally {
       // If this engine has more to fetch, remember that globally
       if (engine.toFetch != null && engine.toFetch.length > 0)
         Status.partial = true;
     }
   },
 
+  /**
+   * Silently fixes case issues.
+   */
+  syncKeyNeedsUpgrade: function syncKeyNeedsUpgrade() {
+    let p = this.passphrase;
+    
+    // Check whether it's already a key that we generated.
+    if (Utils.isPassphrase(p)) {
+      this._log.info("Sync key is up-to-date: no need to upgrade.");
+      return false;
+    }
+    
+    return true;
+  },
+                         
+  /**
+   * If we have a passphrase, rather than a 25-alphadigit sync key,
+   * use the provided sync ID to bootstrap it using PBKDF2.
+   * 
+   * Store the new 'passphrase' back into the identity manager.
+   * 
+   * We can check this as often as we want, because once it's done the
+   * check will no longer succeed. It only matters that it happens after
+   * we decide to bump the server storage version.
+   */
+  upgradeSyncKey: function upgradeSyncKey(syncID) {
+    let p = this.passphrase;
+    
+    // Check whether it's already a key that we generated.
+    if (!this.syncKeyNeedsUpgrade(p))
+      return true;
+    
+    // Otherwise, let's upgrade it.
+    // N.B., we persist the sync key without testing it first...
+    
+    let s = btoa(syncID);        // It's what WeaveCrypto expects. *sigh*
+    let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES);   // Base 32.
+    
+    if (!k) {
+      this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade.");
+      return false;
+    }
+    
+    this._log.info("Upgrading sync key...");
+    this.passphrase = k;
+    this._log.info("Saving upgraded sync key...");
+    this.persistLogin();
+    this._log.info("Done saving.");
+    return true;
+  },
+
   _freshStart: function WeaveSvc__freshStart() {
+    this._log.info("Fresh start. Resetting client and considering key upgrade.");
     this.resetClient();
+    this.upgradeSyncKey(this.syncID);
 
-    let meta = new WBORecord(this.metaURL);
+    let meta = new WBORecord("meta", "global");
     meta.payload.syncID = this.syncID;
     meta.payload.storageVersion = STORAGE_VERSION;
     meta.isNew = true;
 
     this._log.debug("New metadata record: " + JSON.stringify(meta.payload));
-    let resp = new Resource(meta.uri).put(meta);
+    let resp = new Resource(this.metaURL).put(meta);
     if (!resp.success)
       throw resp;
-    Records.set(meta.uri, meta);
+    Records.set(this.metaURL, meta);
 
     // Wipe everything we know about except meta because we just uploaded it
     let collections = [Clients].concat(Engines.getAll()).map(function(engine) {
       return engine.name;
     });
-    this.wipeServer(["crypto", "keys"].concat(collections));
+    this.wipeServer(collections);
+    
+    // Generate and upload new keys. Do this last so we don't wipe them...
+    this.generateNewSymmetricKeys();
   },
 
   /**
    * Check to see if this is a failure
    *
    */
   _checkServerError: function WeaveSvc__checkServerError(resp) {
     if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) {
@@ -1646,25 +1710,16 @@ WeaveSvc.prototype = {
       }
       for each (let name in collections) {
         let url = this.storageURL + name;
         let response = new Resource(url).delete();
         if (response.status != 200 && response.status != 404) {
           throw "Aborting wipeServer. Server responded with "
                 + response.status + " response for " + url;
         }
-
-        // Remove the crypto record from the server and local cache
-        let crypto = this.storageURL + "crypto/" + name;
-        response = new Resource(crypto).delete();
-        CryptoMetas.del(crypto);
-        if (response.status != 200 && response.status != 404) {
-          throw "Aborting wipeServer. Server responded with "
-                + response.status + " response for " + crypto;
-        }
       }
     })(),
 
   /**
    * Wipe all local user data.
    *
    * @param engines [optional]
    *        Array of engine names to wipe. If not given, all engines are used.
@@ -1724,18 +1779,17 @@ WeaveSvc.prototype = {
     this._catch(this._notify("reset-service", "", function() {
       // First drop old logs to track client resetting behavior
       this.clearLogs();
       this._log.info("Logs reinitialized for service reset");
 
       // Pretend we've never synced to the server and drop cached data
       this.syncID = "";
       Svc.Prefs.reset("lastSync");
-      for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records])
-        cache.clearCache();
+      Records.clearCache();
     }))(),
 
   /**
    * Reset the client by getting rid of any local server data and client data.
    *
    * @param engines [optional]
    *        Array of engine names to reset. If not given, all engines are used.
    */
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -74,39 +74,41 @@ let Status = {
     // Check whether we have a username without importing The World(tm).
     let prefs = Cc["@mozilla.org/preferences-service;1"]
                 .getService(Ci.nsIPrefService)
                 .getBranch(PREFS_BRANCH);
     let username;
     try {
       username = prefs.getCharPref("username");
     } catch(ex) {}
+    
     if (!username) {
       Status.login = LOGIN_FAILED_NO_USERNAME;
       return Status.service;
     }
 
     Cu.import("resource://services-sync/util.js");
     Cu.import("resource://services-sync/identity.js");
+    Cu.import("resource://services-sync/base_records/crypto.js");
     if (!Utils.mpLocked()) {
       let id = ID.get("WeaveID");
       if (!id)
         id = ID.set("WeaveID", new Identity(PWDMGR_PASSWORD_REALM, username));
 
       if (!id.password) {
         Status.login = LOGIN_FAILED_NO_PASSWORD;
         return Status.service;
       }
 
       id = ID.get("WeaveCryptoID");
       if (!id)
         id = ID.set("WeaveCryptoID",
-                    new Identity(PWDMGR_PASSPHRASE_REALM, username));
+                    new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, username));
 
-      if (!id.password) {
+      if (!id.keyStr) {
         Status.login = LOGIN_FAILED_NO_PASSPHRASE;
         return Status.service;
       }
     }
     return Status.service = STATUS_OK;
   },
 
   resetBackoff: function resetBackoff() {
--- a/services/sync/modules/stores.js
+++ b/services/sync/modules/stores.js
@@ -81,17 +81,17 @@ Store.prototype = {
   update: function Store_update(record) {
     throw "override update in a subclass";
   },
 
   itemExists: function Store_itemExists(id) {
     throw "override itemExists in a subclass";
   },
 
-  createRecord: function Store_createRecord(id, uri) {
+  createRecord: function Store_createRecord(id, collection, id) {
     throw "override createRecord in a subclass";
   },
 
   changeItemID: function Store_changeItemID(oldID, newID) {
     throw "override changeItemID in a subclass";
   },
 
   getAllIDs: function Store_getAllIDs() {
--- a/services/sync/modules/type_records/bookmark.js
+++ b/services/sync/modules/type_records/bookmark.js
@@ -40,22 +40,22 @@ const EXPORTED_SYMBOLS = ["PlacesItem", 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function PlacesItem(uri, type) {
-  CryptoWrapper.call(this, uri);
+function PlacesItem(collection, id, type) {
+  CryptoWrapper.call(this, collection, id);
   this.type = type || "item";
 }
 PlacesItem.prototype = {
-  decrypt: function PlacesItem_decrypt(passphrase, keyUri) {
+  decrypt: function PlacesItem_decrypt() {
     // Do the normal CryptoWrapper decrypt, but change types before returning
     let clear = CryptoWrapper.prototype.decrypt.apply(this, arguments);
 
     // Convert the abstract places item to the actual object type
     if (!this.deleted)
       this.__proto__ = this.getTypeObject(this.type).prototype;
 
     return clear;
@@ -83,68 +83,68 @@ PlacesItem.prototype = {
 
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.PlacesItem",
 };
 
 Utils.deferGetSet(PlacesItem, "cleartext", ["hasDupe", "parentid", "parentName",
   "predecessorid", "type"]);
 
-function Bookmark(uri, type) {
-  PlacesItem.call(this, uri, type || "bookmark");
+function Bookmark(collection, id, type) {
+  PlacesItem.call(this, collection, id, type || "bookmark");
 }
 Bookmark.prototype = {
   __proto__: PlacesItem.prototype,
   _logName: "Record.Bookmark",
 };
 
 Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description",
   "loadInSidebar", "tags", "keyword"]);
 
-function BookmarkMicsum(uri) {
-  Bookmark.call(this, uri, "microsummary");
+function BookmarkMicsum(collection, id) {
+  Bookmark.call(this, collection, id, "microsummary");
 }
 BookmarkMicsum.prototype = {
   __proto__: Bookmark.prototype,
   _logName: "Record.BookmarkMicsum",
 };
 
 Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorUri", "staticTitle"]);
 
-function BookmarkQuery(uri) {
-  Bookmark.call(this, uri, "query");
+function BookmarkQuery(collection, id) {
+  Bookmark.call(this, collection, id, "query");
 }
 BookmarkQuery.prototype = {
   __proto__: Bookmark.prototype,
   _logName: "Record.BookmarkQuery",
 };
 
 Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName"]);
 
-function BookmarkFolder(uri, type) {
-  PlacesItem.call(this, uri, type || "folder");
+function BookmarkFolder(collection, id, type) {
+  PlacesItem.call(this, collection, id, type || "folder");
 }
 BookmarkFolder.prototype = {
   __proto__: PlacesItem.prototype,
   _logName: "Record.Folder",
 };
 
 Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title"]);
 
-function Livemark(uri) {
-  BookmarkFolder.call(this, uri, "livemark");
+function Livemark(collection, id) {
+  BookmarkFolder.call(this, collection, id, "livemark");
 }
 Livemark.prototype = {
   __proto__: BookmarkFolder.prototype,
   _logName: "Record.Livemark",
 };
 
 Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
 
-function BookmarkSeparator(uri) {
-  PlacesItem.call(this, uri, "separator");
+function BookmarkSeparator(collection, id) {
+  PlacesItem.call(this, collection, id, "separator");
 }
 BookmarkSeparator.prototype = {
   __proto__: PlacesItem.prototype,
   _logName: "Record.Separator",
 };
 
 Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
--- a/services/sync/modules/type_records/clients.js
+++ b/services/sync/modules/type_records/clients.js
@@ -39,17 +39,17 @@ const EXPORTED_SYMBOLS = ["ClientsRec"];
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function ClientsRec(uri) {
-  CryptoWrapper.call(this, uri);
+function ClientsRec(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 ClientsRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.Clients",
 };
 
 Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands"]);
--- a/services/sync/modules/type_records/forms.js
+++ b/services/sync/modules/type_records/forms.js
@@ -39,17 +39,17 @@ const EXPORTED_SYMBOLS = ['FormRec'];
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function FormRec(uri) {
-  CryptoWrapper.call(this, uri);
+function FormRec(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 FormRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.Form",
 };
 
 Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]);
--- a/services/sync/modules/type_records/history.js
+++ b/services/sync/modules/type_records/history.js
@@ -39,17 +39,17 @@ const EXPORTED_SYMBOLS = ['HistoryRec'];
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function HistoryRec(uri) {
-  CryptoWrapper.call(this, uri);
+function HistoryRec(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 HistoryRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.History",
 };
 
 Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]);
--- a/services/sync/modules/type_records/passwords.js
+++ b/services/sync/modules/type_records/passwords.js
@@ -39,18 +39,18 @@ const EXPORTED_SYMBOLS = ['LoginRec'];
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function LoginRec(uri) {
-  CryptoWrapper.call(this, uri);
+function LoginRec(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 LoginRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.Login",
 };
 
 Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL",
   "httpRealm", "username", "password", "usernameField", "passwordField"]);
--- a/services/sync/modules/type_records/prefs.js
+++ b/services/sync/modules/type_records/prefs.js
@@ -39,17 +39,17 @@ const EXPORTED_SYMBOLS = ['PrefRec'];
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function PrefRec(uri) {
-  CryptoWrapper.call(this, uri);
+function PrefRec(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 PrefRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.Pref",
 };
 
 Utils.deferGetSet(PrefRec, "cleartext", ["value"]);
--- a/services/sync/modules/type_records/tabs.js
+++ b/services/sync/modules/type_records/tabs.js
@@ -39,17 +39,17 @@ const EXPORTED_SYMBOLS = ['TabSetRecord'
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/base_records/crypto.js");
 Cu.import("resource://services-sync/util.js");
 
-function TabSetRecord(uri) {
-  CryptoWrapper.call(this, uri);
+function TabSetRecord(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 TabSetRecord.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Record.Tabs",
 };
 
 Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -103,21 +103,22 @@ let Utils = {
   },
 
   /**
    * Wrap a function to call lock before calling the function then unlock.
    *
    * @usage MyObj._lock = Utils.lock;
    *        MyObj.foo = function() { this._lock(func)(); }
    */
-  lock: function Utils_lock(func) {
+  lock: function lock(label, func) {
     let thisArg = this;
     return function WrappedLock() {
-      if (!thisArg.lock())
-        throw "Could not acquire lock";
+      if (!thisArg.lock()) {
+        throw "Could not acquire lock. Label: \"" + label + "\".";
+      }
 
       try {
         return func.call(thisArg);
       }
       finally {
         thisArg.unlock();
       }
     };
@@ -326,18 +327,31 @@ let Utils = {
 
     // Split the defer into each dot part for each level to dereference
     let parts = defer.split(".");
     let deref = function(base) Utils.deref(base, parts);
 
     let prot = obj.prototype;
 
     // Create a getter if it doesn't exist yet
-    if (!prot.__lookupGetter__(prop))
-      prot.__defineGetter__(prop, function() deref(this)[prop]);
+    if (!prot.__lookupGetter__(prop)) {
+      // Yes, this should be a one-liner, but there are errors if it's not
+      // broken out. *sigh*
+      // Errors are these:
+      // JavaScript strict warning: resource://services-sync/util.js, line 304: reference to undefined property deref(this)[prop]
+      // JavaScript strict warning: resource://services-sync/util.js, line 304: reference to undefined property deref(this)[prop]
+      let f = function() {
+        let d = deref(this);
+        if (!d)
+          return undefined;
+        let out = d[prop];
+        return out;
+      }
+      prot.__defineGetter__(prop, f);
+    }
 
     // Create a setter if it doesn't exist yet
     if (!prot.__lookupSetter__(prop))
       prot.__defineSetter__(prop, function(val) deref(this)[prop] = val);
   },
 
   /**
    * Dereference an array of properties starting from a base object
@@ -545,16 +559,31 @@ let Utils = {
   },
 
   bytesAsHex: function bytesAsHex(bytes) {
     // Convert each hashed byte into 2-hex strings then combine them
     return [("0" + byte.charCodeAt().toString(16)).slice(-2)
             for each (byte in bytes)].join("");
   },
 
+  _sha256: function _sha256(message) {
+    let hasher = Cc["@mozilla.org/security/hash;1"].
+      createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    return Utils.digest(message, hasher);
+  },
+
+  sha256: function sha256(message) {
+    return Utils.bytesAsHex(Utils._sha256(message));
+  },
+
+  sha256Base64: function (message) {
+    return btoa(Utils._sha256(message));
+  },
+
   _sha1: function _sha1(message) {
     let hasher = Cc["@mozilla.org/security/hash;1"].
       createInstance(Ci.nsICryptoHash);
     hasher.init(hasher.SHA1);
     return Utils.digest(message, hasher);
   },
 
   sha1: function sha1(message) {
@@ -575,16 +604,20 @@ let Utils = {
   /**
    * Produce an HMAC hasher.
    */
   makeHMACHasher: function makeHMACHasher() {
     return Cc["@mozilla.org/security/hmac;1"]
              .createInstance(Ci.nsICryptoHMAC);
   },
 
+  sha1Base64: function (message) {
+    return btoa(Utils._sha1(message));
+  },
+
   /**
    * Generate a sha1 HMAC for a message, not UTF-8 encoded,
    * and a given nsIKeyObject.
    * Optionally provide an existing hasher, which will be 
    * initialized and reused.
    */
   sha1HMACBytes: function sha1HMACBytes(message, key, hasher) {
     let h = hasher || this.makeHMACHasher();
@@ -592,41 +625,71 @@ let Utils = {
     
     // No UTF-8 encoding for you, sunshine.
     let bytes = [b.charCodeAt() for each (b in message)];
     h.update(bytes, bytes.length);
     return h.finish(false);
   },
   
   /**
-   * Generate a sha256 HMAC for a string message and a given nsIKeyObject
+   * Generate a sha256 HMAC for a string message and a given nsIKeyObject.
+   * Optionally provide an existing hasher, which will be
+   * initialized and reused.
+   *
+   * Returns hex output.
    */
-  sha256HMAC: function sha256HMAC(message, key) {
-    let hasher = this.makeHMACHasher();
-    hasher.init(hasher.SHA256, key);
-    return Utils.bytesAsHex(Utils.digest(message, hasher));
+  sha256HMAC: function sha256HMAC(message, key, hasher) {
+    let h = hasher || this.makeHMACHasher();
+    h.init(h.SHA256, key);
+    return Utils.bytesAsHex(Utils.digest(message, h));
   },
   
   
   /**
+   * Generate a sha256 HMAC for a string message, not UTF-8 encoded,
+   * and a given nsIKeyObject.
+   * Optionally provide an existing hasher, which will be
+   * initialized and reused.
+   */
+  sha256HMACBytes: function sha256HMACBytes(message, key, hasher) {
+    let h = hasher || this.makeHMACHasher();
+    h.init(h.SHA256, key);
+
+    // No UTF-8 encoding for you, sunshine.
+    let bytes = [b.charCodeAt() for each (b in message)];
+    h.update(bytes, bytes.length);
+    return h.finish(false);
+  },
+
+  byteArrayToString: function byteArrayToString(bytes) {
+    return [String.fromCharCode(byte) for each (byte in bytes)].join("");
+  },
+  
+  /**
    * PBKDF2 implementation in Javascript.
    * 
    * The arguments to this function correspond to items in 
    * PKCS #5, v2.0 pp. 9-10 
    * 
    * P: the passphrase, an octet string:              e.g., "secret phrase"
    * S: the salt, an octet string:                    e.g., "DNXPzPpiwn"
    * c: the number of iterations, a positive integer: e.g., 4096
    * dkLen: the length in octets of the destination 
    *        key, a positive integer:                  e.g., 16
    *        
    * The output is an octet string of length dkLen, which you
    * can encode as you wish.
    */
   pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen) {
+    
+    // We don't have a default in the algo itself, as NSS does.
+    // Use the constant.
+    if (!dkLen)
+      dkLen = SYNC_KEY_DECODED_LENGTH;
+    
     /* For HMAC-SHA-1 */
     const HLEN = 20;
     
     function F(PK, S, c, i, h) {
     
       function XOR(a, b, isA) {
         if (a.length != b.length) {
           return false;
@@ -639,24 +702,16 @@ let Utils = {
           } else {
             val[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
           }
         }
 
         return val;
       }
     
-      function arrayToString(arr) {
-        let ret = '';
-        for (let i = 0; i < arr.length; i++) {
-          ret += String.fromCharCode(arr[i]);
-        }
-        return ret;
-      }
-      
       let ret;
       let U = [];
 
       /* Encode i into 4 octets: _INT */
       let I = [];
       I[0] = String.fromCharCode((i >> 24) & 0xff);
       I[1] = String.fromCharCode((i >> 16) & 0xff);
       I[2] = String.fromCharCode((i >> 8) & 0xff);
@@ -664,17 +719,17 @@ let Utils = {
 
       U[0] = Utils.sha1HMACBytes(S + I.join(''), PK, h);
       for (let j = 1; j < c; j++) {
         U[j] = Utils.sha1HMACBytes(U[j - 1], PK, h);
       }
 
       ret = U[0];
       for (j = 1; j < c; j++) {
-        ret = arrayToString(XOR(ret, U[j]));
+        ret = Utils.byteArrayToString(XOR(ret, U[j]));
       }
 
       return ret;
     }
     
     let l = Math.ceil(dkLen / HLEN);
     let r = dkLen - ((l - 1) * HLEN);
 
@@ -693,16 +748,102 @@ let Utils = {
     }
     ret += T[l - 1].substr(0, r);
 
     return ret;
   },
 
 
   /**
+   * Base32 decode (RFC 4648) a string.
+   */
+  decodeBase32: function decodeBase32(str) {
+    const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+    let padChar = str.indexOf("=");
+    let chars = (padChar == -1) ? str.length : padChar;
+    let bytes = Math.floor(chars * 5 / 8);
+    let blocks = Math.ceil(chars / 8);
+
+    // Process a chunk of 5 bytes / 8 characters.
+    // The processing of this is known in advance,
+    // so avoid arithmetic!
+    function processBlock(ret, cOffset, rOffset) {
+      let c, val;
+
+      // N.B., this relies on
+      //   undefined | foo == foo.
+      function accumulate(val) {
+        ret[rOffset] |= val;
+      }
+
+      function advance() {
+        c  = str[cOffset++];
+        if (!c || c == "" || c == "=") // Easier than range checking.
+          throw "Done";                // Will be caught far away.
+        val = key.indexOf(c);
+        if (val == -1)
+          throw "Unknown character in base32: " + c;
+      }
+
+      // Handle a left shift, restricted to bytes.
+      function left(octet, shift)
+        (octet << shift) & 0xff;
+
+      advance();
+      accumulate(left(val, 3));
+      advance();
+      accumulate(val >> 2);
+      ++rOffset;
+      accumulate(left(val, 6));
+      advance();
+      accumulate(left(val, 1));
+      advance();
+      accumulate(val >> 4);
+      ++rOffset;
+      accumulate(left(val, 4));
+      advance();
+      accumulate(val >> 1);
+      ++rOffset;
+      accumulate(left(val, 7));
+      advance();
+      accumulate(left(val, 2));
+      advance();
+      accumulate(val >> 3);
+      ++rOffset;
+      accumulate(left(val, 5));
+      advance();
+      accumulate(val);
+      ++rOffset;
+    }
+
+    // Our output. Define to be explicit (and maybe the compiler will be smart).
+    let ret  = new Array(bytes);
+    let i    = 0;
+    let cOff = 0;
+    let rOff = 0;
+
+    for (; i < blocks; ++i) {
+      try {
+        processBlock(ret, cOff, rOff);
+      } catch (ex) {
+        // Handle the detection of padding.
+        if (ex == "Done")
+          break;
+        throw ex;
+      }
+      cOff += 8;
+      rOff += 5;
+    }
+
+    // Slice in case our shift overflowed to the right.
+    return Utils.byteArrayToString(ret.slice(0, bytes));
+  },
+
+  /**
    * Base32 encode (RFC 4648) a string
    */
   encodeBase32: function encodeBase32(bytes) {
     const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
     let quanta = Math.floor(bytes.length / 5);
     let leftover = bytes.length % 5;
 
     // Pad the last quantum with zeros so the length is a multiple of 5.
@@ -736,16 +877,95 @@ let Utils = {
         return ret.slice(0, -3) + "===";
       case 4:
         return ret.slice(0, -1) + "=";
       default:
         return ret;
     }
   },
 
+  /**
+   * Turn RFC 4648 base32 into our own user-friendly version.
+   *   ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
+   * becomes
+   *   abcdefghijk8mn9pqrstuvwxyz234567
+   */
+  base32ToFriendly: function base32ToFriendly(input) {
+    return input.toLowerCase()
+                .replace("l", '8', "g")
+                .replace("o", '9', "g");
+  },
+
+  base32FromFriendly: function base32FromFriendly(input) {
+    return input.toUpperCase()
+                .replace("8", 'L', "g")
+                .replace("9", 'O', "g");
+  },
+
+
+  /**
+   * Key manipulation.
+   */
+
+  // Return an octet string in friendly base32 *with no trailing =*.
+  encodeKeyBase32: function encodeKeyBase32(keyData) {
+    return Utils.base32ToFriendly(
+             Utils.encodeBase32(keyData))
+           .slice(0, SYNC_KEY_ENCODED_LENGTH);
+  },
+
+  decodeKeyBase32: function decodeKeyBase32(encoded) {
+    return Utils.decodeBase32(
+             Utils.base32FromFriendly(
+               Utils.normalizePassphrase(encoded)))
+           .slice(0, SYNC_KEY_DECODED_LENGTH);
+  },
+
+  base64Key: function base64Key(keyData) {
+    return btoa(keyData);
+  },
+
+  deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase, salt, keyLength, forceJS) {
+    if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) {
+      return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength);
+    }
+    else {
+      // Fall back to JS implementation.
+      // 4096 is hardcoded in WeaveCrypto, so do so here.
+      return Utils.pbkdf2Generate(passphrase, atob(salt), 4096, keyLength);
+    }
+  },
+
+  /**
+   * N.B., salt should be base64 encoded, even though we have to decode
+   * it later!
+   */
+  derivePresentableKeyFromPassphrase : function derivePresentableKeyFromPassphrase(passphrase, salt, keyLength, forceJS) {
+    let k = Utils.deriveKeyFromPassphrase(passphrase, salt, keyLength, forceJS);
+    return Utils.encodeKeyBase32(k);
+  },
+
+  /**
+   * N.B., salt should be base64 encoded, even though we have to decode
+   * it later!
+   */
+  deriveEncodedKeyFromPassphrase : function deriveEncodedKeyFromPassphrase(passphrase, salt, keyLength, forceJS) {
+    let k = Utils.deriveKeyFromPassphrase(passphrase, salt, keyLength, forceJS);
+    return Utils.base64Key(k);
+  },
+
+  /**
+   * Take a base64-encoded 128-bit AES key, returning it as five groups of five
+   * uppercase alphanumeric characters, separated by hyphens.
+   * A.K.A. base64-to-base32 encoding.
+   */
+  presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) {
+    return Utils.encodeKeyBase32(atob(encodedKey));
+  },
+
   makeURI: function Weave_makeURI(URIString) {
     if (!URIString)
       return null;
     try {
       return Svc.IO.newURI(URIString, null, null);
     } catch (e) {
       let log = Log4Moz.repository.getLogger("Service.Util");
       log.debug("Could not create URI: " + Utils.exceptionStr(e));
@@ -979,46 +1199,105 @@ let Utils = {
       str = unicodeConverter.ConvertToUnicode(str);
       return str + unicodeConverter.Finish();
     } catch(ex) {
       return null;
     }
   },
 
   /**
-   * Generate 20 random characters a-z
+   * Generate 26 characters.
    */
-  generatePassphrase: function() {
+  generatePassphrase: function generatePassphrase() {
+    // Note that this is a different base32 alphabet to the one we use for
+    // other tasks. It's lowercase, uses different letters, and needs to be
+    // decoded with decodeKeyBase32, not just decodeBase32.
     let rng = Cc["@mozilla.org/security/random-generator;1"]
                 .createInstance(Ci.nsIRandomGenerator);
-    let bytes = rng.generateRandomBytes(20);
-    return [String.fromCharCode(97 + Math.floor(byte * 26 / 256))
-            for each (byte in bytes)].join("");
+    let bytes = rng.generateRandomBytes(16);
+    return Utils.encodeKeyBase32(Utils.byteArrayToString(bytes));
+  },
+
+  /**
+   * The following are the methods supported for UI use:
+   *
+   * * isPassphrase:
+   *     determines whether a string is either a normalized or presentable
+   *     passphrase.
+   * * hyphenatePassphrase:
+   *     present a normalized passphrase for display. This might actually
+   *     perform work beyond just hyphenation; sorry.
+   * * hyphenatePartialPassphrase:
+   *     present a fragment of a normalized passphrase for display.
+   * * normalizePassphrase:
+   *     take a presentable passphrase and reduce it to a normalized
+   *     representation for storage. normalizePassphrase can safely be called
+   *     on normalized input.
+   */
+
+  isPassphrase: function(s) {
+    if (s) {
+      return /^[abcdefghijkmnpqrstuvwxyz23456789]{26}$/.test(Utils.normalizePassphrase(s));
+    }
+    return false;
   },
 
   /**
-   * Hyphenate a 20 character passphrase in 4 groups of 5.
+   * Hyphenate a passphrase (26 characters) into groups.
+   * abbbbccccddddeeeeffffggggh
+   * =>
+   * a-bbbbc-cccdd-ddeee-effff-ggggh
    */
-  hyphenatePassphrase: function(passphrase) {
-    return passphrase.slice(0, 5) + '-'
-         + passphrase.slice(5, 10) + '-'
-         + passphrase.slice(10, 15) + '-'
-         + passphrase.slice(15, 20);
+  hyphenatePassphrase: function hyphenatePassphrase(passphrase) {
+    // For now, these are the same.
+    return Utils.hyphenatePartialPassphrase(passphrase, true);
   },
 
-  /**
-   * Remove hyphens as inserted by hyphenatePassphrase().
-   */
-  normalizePassphrase: function(pp) {
-    if (pp.length == 23 && pp[5] == '-' && pp[11] == '-' && pp[17] == '-')
-      return pp.slice(0, 5) + pp.slice(6, 11)
-           + pp.slice(12, 17) + pp.slice(18, 23);
+  hyphenatePartialPassphrase: function hyphenatePartialPassphrase(passphrase, omitTrailingDash) {
+    if (!passphrase)
+      return null;
+
+    // Get the raw data input. Just base32.
+    let data = passphrase.toLowerCase().replace(/[^abcdefghijkmnpqrstuvwxyz23456789]/g, "");
+
+    // This is the neatest way to do this.
+    if ((data.length == 1) && !omitTrailingDash)
+      return data + "-";
+
+    // Hyphenate it.
+    let y = data.substr(0,1);
+    let z = data.substr(1).replace(/(.{1,5})/g, "-$1");
+
+    // Correct length? We're done.
+    if ((z.length == 30) || omitTrailingDash)
+      return y + z;
+
+    // Add a trailing dash if appropriate.
+    return (y + z.replace(/([^-]{5})$/, "$1-")).substr(0, SYNC_KEY_HYPHENATED_LENGTH);
+  },
+
+  normalizePassphrase: function normalizePassphrase(pp) {
+    // Short var name... have you seen the lines below?!
+    pp = pp.toLowerCase();
+    if (pp.length == 31 && [1, 7, 13, 19, 25].every(function(i) pp[i] == '-'))
+      return pp.slice(0, 1) + pp.slice(2, 7)
+             + pp.slice(8, 13) + pp.slice(14, 19)
+             + pp.slice(20, 25) + pp.slice(26, 31);
     return pp;
   },
 
+  // WeaveCrypto returns bad base64 strings. Truncate excess padding
+  // and decode.
+  // See Bug 562431, comment 4.
+  safeAtoB: function safeAtoB(b64) {
+    let len = b64.length;
+    let over = len % 4;
+    return over ? atob(b64.substr(0, len - over)) : atob(b64);
+  },
+
   /*
    * Calculate the strength of a passphrase provided by the user
    * according to the NIST algorithm (NIST 800-63 Appendix A.1).
    */
   passphraseStrength: function passphraseStrength(value) {
     let bits = 0;
 
     // The entropy of the first character is taken to be 4 bits.
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,8 +1,10 @@
+var btoa;
+
 // initialize nss
 let ch = Cc["@mozilla.org/security/hash;1"].
          createInstance(Ci.nsICryptoHash);
 
 let ds = Cc["@mozilla.org/file/directory_service;1"]
   .getService(Ci.nsIProperties);
 
 let provider = {
@@ -67,17 +69,17 @@ function FakeTimerService() {
       }
       return false;
     }
   };
 
   Utils.makeTimerForCall = self.makeTimerForCall;
 };
 
-Cu.import("resource://services-sync/log4moz.js");
+btoa = Cu.import("resource://services-sync/log4moz.js").btoa;
 function getTestLogger(component) {
   return Log4Moz.repository.getLogger("Testing");
 }
 
 function initTestLogging(level) {
   function LogStats() {
     this.errorsLogged = 0;
   }
@@ -231,64 +233,43 @@ FakeCryptoService.prototype = {
   encrypt: function(aClearText, aSymmetricKey, aIV) {
     return aClearText;
   },
 
   decrypt: function(aCipherText, aSymmetricKey, aIV) {
     return aCipherText;
   },
 
-  generateKeypair: function(aPassphrase, aSalt, aIV,
-                            aEncodedPublicKey, aWrappedPrivateKey) {
-      aEncodedPublicKey.value = aPassphrase;
-      aWrappedPrivateKey.value = aPassphrase;
-  },
-
   generateRandomKey: function() {
-    return "fake-symmetric-key-" + this.counter++;
+    return btoa("fake-symmetric-key-" + this.counter++);
   },
 
   generateRandomIV: function() {
     // A base64-encoded IV is 24 characters long
-    return "fake-fake-fake-random-iv";
+    return btoa("fake-fake-fake-random-iv");
+  },
+
+  expandData : function expandData(data, len) {
+    return data;
+  },
+
+  deriveKeyFromPassphrase : function (passphrase, salt, keyLength) {
+    return "some derived key string composed of bytes";
   },
 
   generateRandomBytes: function(aByteCount) {
     return "not-so-random-now-are-we-HA-HA-HA! >:)".slice(aByteCount);
   },
-
-  wrapSymmetricKey: function(aSymmetricKey, aEncodedPublicKey) {
-    return aSymmetricKey;
-  },
-
-  unwrapSymmetricKey: function(aWrappedSymmetricKey, aWrappedPrivateKey,
-                               aPassphrase, aSalt, aIV) {
-    if (!this.verifyPassphrase(aWrappedPrivateKey, aPassphrase)) {
-      throw Components.Exception("Unwrapping the private key failed",
-                                 Cr.NS_ERROR_FAILURE);
-    }
-    return aWrappedSymmetricKey;
-  },
-
-  rewrapPrivateKey: function(aWrappedPrivateKey, aPassphrase, aSalt, aIV,
-                             aNewPassphrase) {
-    return aNewPassphrase;
-  },
-
-  verifyPassphrase: function(aWrappedPrivateKey, aPassphrase, aSalt, aIV) {
-    return aWrappedPrivateKey == aPassphrase;
-  }
-
 };
 
 
 function SyncTestingInfrastructure(engineFactory) {
   let __fakePasswords = {
     'Mozilla Services Password': {foo: "bar"},
-    'Mozilla Services Encryption Passphrase': {foo: "passphrase"}
+    'Mozilla Services Encryption Passphrase': {foo: "a-abcde-abcde-abcde-abcde-abcde"}
   };
 
   let __fakePrefs = {
     "encryption" : "none",
     "log.logger.service.crypto" : "Debug",
     "log.logger.service.engine" : "Debug",
     "log.logger.async" : "Debug",
     "xmpp.enabled" : false
--- a/services/sync/tests/unit/test_bookmark_predecessor.js
+++ b/services/sync/tests/unit/test_bookmark_predecessor.js
@@ -18,27 +18,27 @@ function run_test() {
     return bmk;
   }
 
   _("Creating a couple bookmarks that create holes");
   let first = insert(5);
   let second = insert(10);
 
   _("Making sure the record created for the first has no predecessor");
-  let pos5 = store.createRecord("pos5", baseuri + "pos5");
+  let pos5 = store.createRecord("pos5");
   do_check_eq(pos5.predecessorid, undefined);
 
   _("Making sure the second record has the first as its predecessor");
-  let pos10 = store.createRecord("pos10", baseuri + "pos10");
+  let pos10 = store.createRecord("pos10");
   do_check_eq(pos10.predecessorid, "pos5");
 
   _("Make sure the index of item gets fixed");
   do_check_eq(Svc.Bookmark.getItemIndex(first), 0);
   do_check_eq(Svc.Bookmark.getItemIndex(second), 1);
 
   _("Make sure things that are in unsorted don't set the predecessor");
   insert(0, Svc.Bookmark.unfiledBookmarksFolder);
   insert(1, Svc.Bookmark.unfiledBookmarksFolder);
-  do_check_eq(store.createRecord("pos0", baseuri + "pos0").predecessorid,
+  do_check_eq(store.createRecord("pos0").predecessorid,
               undefined);
-  do_check_eq(store.createRecord("pos1", baseuri + "pos1").predecessorid,
+  do_check_eq(store.createRecord("pos1").predecessorid,
               undefined);
 }
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -0,0 +1,43 @@
+Cu.import("resource://services-sync/base_records/crypto.js");
+Cu.import("resource://services-sync/type_records/bookmark.js");
+Cu.import("resource://services-sync/auth.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-sync/util.js");
+  
+function prepareBookmarkItem(collection, id) {
+  let b = new Bookmark(collection, id);
+  b.cleartext.stuff = "my payload here";
+  return b;
+}
+
+function run_test() {
+  let keyBundle = ID.set("WeaveCryptoID", new SyncKeyBundle(null, "john@example.com"));
+  keyBundle.keyStr = "abcdeabcdeabcdeabcdeabcdea";
+  
+  CollectionKeys.generateNewKeys();
+  
+  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";
+  let placesItem = new PlacesItem("bookmarks", "foo", "bookmark");
+  let bookmarkItem = prepareBookmarkItem("bookmarks", "foo");
+  
+  log.info("Checking getTypeObject");
+  do_check_eq(placesItem.getTypeObject(placesItem.type), Bookmark);
+  do_check_eq(bookmarkItem.getTypeObject(bookmarkItem.type), Bookmark);
+  
+  bookmarkItem.encrypt(keyBundle);
+  log.info("Ciphertext is " + bookmarkItem.ciphertext);
+  do_check_true(bookmarkItem.ciphertext != null);
+  
+  log.info("Decrypting the record");
+
+  let payload = bookmarkItem.decrypt(keyBundle);
+  do_check_eq(payload.stuff, "my payload here");
+  do_check_eq(bookmarkItem.getTypeObject(bookmarkItem.type), Bookmark);
+  do_check_neq(payload, bookmarkItem.payload); // wrap.data.payload is the encrypted one
+}
--- a/services/sync/tests/unit/test_bookmark_store.js
+++ b/services/sync/tests/unit/test_bookmark_store.js
@@ -30,27 +30,27 @@ function run_test() {
     do_check_eq(Svc.Bookmark.getItemGUID(id), fxrecord.id);
     do_check_eq(Svc.Bookmark.getItemType(id), Svc.Bookmark.TYPE_BOOKMARK);
     do_check_eq(Svc.Bookmark.getItemTitle(id), fxrecord.title);
     do_check_eq(Svc.Bookmark.getFolderIdForItem(id),
                 Svc.Bookmark.toolbarFolder);
     do_check_eq(Svc.Bookmark.getKeywordForBookmark(id), fxrecord.keyword);
 
     _("Have the store create a new record object. Verify that it has the same data.");
-    let newrecord = store.createRecord(fxrecord.id, "http://fake/uri");
+    let newrecord = store.createRecord(fxrecord.id);
     for each (let property in ["type", "bmkUri", "title", "keyword",
                                "parentName", "parentid"])
       do_check_eq(newrecord[property], fxrecord[property]);      
 
     _("The calculated sort index is based on frecency data.");
     do_check_true(newrecord.sortindex >= 150);
 
     _("Folders have high sort index to ensure they're synced first.");
     let folder_id = Svc.Bookmark.createFolder(Svc.Bookmark.toolbarFolder,
                                               "Test Folder", 0);
     let folder_guid = Svc.Bookmark.getItemGUID(folder_id);
-    let folder_record = store.createRecord(folder_guid, "http://fake/uri");
+    let folder_record = store.createRecord(folder_guid);
     do_check_eq(folder_record.sortindex, 1000000);
   } finally {
     _("Clean up.");
     store.wipe();
   }
 }
--- a/services/sync/tests/unit/test_clients_escape.js
+++ b/services/sync/tests/unit/test_clients_escape.js
@@ -1,62 +1,54 @@
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 
 function run_test() {
   _("Set up test fixtures.");
   ID.set('WeaveID', new Identity('Some Identity', 'foo'));
   Svc.Prefs.set("clusterURL", "http://fakebase/");
   let baseUri = "http://fakebase/1.0/foo/storage/";
   let pubUri = baseUri + "keys/pubkey";
   let privUri = baseUri + "keys/privkey";
 
-  let passphrase = ID.set("WeaveCryptoID", new Identity());
-  passphrase.password = "passphrase";
-
-  _("Setting up fake pub/priv keypair and symkey for encrypt/decrypt");
-  PubKeys.defaultKeyUri = baseUri + "keys/pubkey";
-  let {pubkey, privkey} = PubKeys.createKeypair(passphrase, pubUri, privUri);
-  PubKeys.set(pubUri, pubkey);
-  PrivKeys.set(privUri, privkey);
-
-  let cryptoMeta = new CryptoMeta(Clients.cryptoMetaURL);
-  cryptoMeta.addUnwrappedKey(pubkey, Svc.Crypto.generateRandomKey());
-  CryptoMetas.set(Clients.cryptoMetaURL, cryptoMeta);
+  let keyBundle = ID.set("WeaveCryptoID",
+                         new SyncKeyBundle(null, "john@example.com", "abcdeabcdeabcdeabcdeabcdea"));
 
   try {
     _("Test that serializing client records results in uploadable ascii");
     Clients.localID = "ascii";
     Clients.localName = "wéävê";
 
     _("Make sure we have the expected record");
     let record = Clients._createRecord("ascii");
     do_check_eq(record.id, "ascii");
     do_check_eq(record.name, "wéävê");
 
-    record.encrypt(passphrase);
+    _("Encrypting record...");
+    record.encrypt(keyBundle);
+    _("Encrypted.");
+    
     let serialized = JSON.stringify(record);
     let checkCount = 0;
     _("Checking for all ASCII:", serialized);
     Array.forEach(serialized, function(ch) {
       let code = ch.charCodeAt(0);
       _("Checking asciiness of '", ch, "'=", code);
       do_check_true(code < 128);
       checkCount++;
     });
 
     _("Processed", checkCount, "characters out of", serialized.length);
     do_check_eq(checkCount, serialized.length);
 
     _("Making sure the record still looks like it did before");
-    record.decrypt(passphrase, Clients.cryptoMetaURL);
+    record.decrypt(keyBundle);
     do_check_eq(record.id, "ascii");
     do_check_eq(record.name, "wéävê");
 
     _("Sanity check that creating the record also gives the same");
     record = Clients._createRecord("ascii");
     do_check_eq(record.id, "ascii");
     do_check_eq(record.name, "wéävê");
   } finally {
--- a/services/sync/tests/unit/test_collection_inc_get.js
+++ b/services/sync/tests/unit/test_collection_inc_get.js
@@ -1,25 +1,28 @@
 _("Make sure Collection can correctly incrementally parse GET requests");
 Cu.import("resource://services-sync/base_records/collection.js");
 Cu.import("resource://services-sync/base_records/wbo.js");
 
 function run_test() {
-  let coll = new Collection("http://fake/uri", WBORecord);
+  let base = "http://fake/";
+  let coll = new Collection("http://fake/uri/", WBORecord);
   let stream = { _data: "" };
   let called, recCount, sum;
 
   _("Not-JSON, string payloads are strings");
   called = false;
   stream._data = '{"id":"hello","payload":"world"}\n';
   coll.recordHandler = function(rec) {
     called = true;
     _("Got record:", JSON.stringify(rec));
+    rec.collection = "uri";           // This would be done by an engine, so do it here.
+    do_check_eq(rec.collection, "uri");
     do_check_eq(rec.id, "hello");
-    do_check_eq(rec.uri.spec, "http://fake/uri/hello");
+    do_check_eq(rec.uri(base).spec, "http://fake/uri/hello");
     do_check_eq(rec.payload, "world");
   };
   coll._onProgress.call(stream);
   do_check_eq(stream._data, '');
   do_check_true(called);
   _("\n");
 
 
@@ -43,32 +46,33 @@ function run_test() {
   sum = 0;
   stream._data = '{"id":"hundred","payload":"{\\"value\\":100}"}\n{"id":"ten","payload":"{\\"value\\":10}"}\n{"id":"one","payload":"{\\"value\\":1}"}\n';
   coll.recordHandler = function(rec) {
     called = true;
     _("Got record:", JSON.stringify(rec));
     recCount++;
     sum += rec.payload.value;
     _("Incremental status: count", recCount, "sum", sum);
+    rec.collection = "uri";
     switch (recCount) {
       case 1:
         do_check_eq(rec.id, "hundred");
-        do_check_eq(rec.uri.spec, "http://fake/uri/hundred");
+        do_check_eq(rec.uri(base).spec, "http://fake/uri/hundred");
         do_check_eq(rec.payload.value, 100);
         do_check_eq(sum, 100);
         break;
       case 2:
         do_check_eq(rec.id, "ten");
-        do_check_eq(rec.uri.spec, "http://fake/uri/ten");
+        do_check_eq(rec.uri(base).spec, "http://fake/uri/ten");
         do_check_eq(rec.payload.value, 10);
         do_check_eq(sum, 110);
         break;
       case 3:
         do_check_eq(rec.id, "one");
-        do_check_eq(rec.uri.spec, "http://fake/uri/one");
+        do_check_eq(rec.uri(base).spec, "http://fake/uri/one");
         do_check_eq(rec.payload.value, 1);
         do_check_eq(sum, 111);
         break;
       default:
         do_throw("unexpected number of record counts", recCount);
         break;
     }
   };
--- a/services/sync/tests/unit/test_forms_store.js
+++ b/services/sync/tests/unit/test_forms_store.js
@@ -23,23 +23,23 @@ function run_test() {
   for (let _id in store.getAllIDs()) {
     if (id == "")
       id = _id;
     else
       do_throw("Should have only gotten one!");
   }
   do_check_true(store.itemExists(id));
 
-  let rec = store.createRecord(id, baseuri + id);
+  let rec = store.createRecord(id);
   _("Got record for id", id, rec);
   do_check_eq(rec.name, "name!!");
   do_check_eq(rec.value, "value??");
 
   _("Create a non-existent id for delete");
-  do_check_true(store.createRecord("deleted!!", baseuri + "deleted!!").deleted);
+  do_check_true(store.createRecord("deleted!!").deleted);
 
   _("Try updating.. doesn't do anything yet");
   store.update({});
 
   _("Remove all entries");
   store.wipe();
   for (let id in store.getAllIDs()) {
     do_throw("Shouldn't get any ids!");
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -78,21 +78,21 @@ function run_test() {
 
     _("Verify that the entry exists.");
     let ids = [id for (id in store.getAllIDs())];
     do_check_eq(ids.length, 1);
     fxguid = ids[0];
     do_check_true(store.itemExists(fxguid));
 
     _("If we query a non-existent record, it's marked as deleted.");
-    let record = store.createRecord("non-existent", "http://fake/uri");
+    let record = store.createRecord("non-existent");
     do_check_true(record.deleted);
 
     _("Verify createRecord() returns a complete record.");
-    record = store.createRecord(fxguid, "http://fake/urk");
+    record = store.createRecord(fxguid);
     do_check_eq(record.histUri, fxuri.spec);
     do_check_eq(record.title, "Get Firefox!");
     do_check_eq(record.visits.length, 1);
     do_check_eq(record.visits[0].date, TIMESTAMP1);
     do_check_eq(record.visits[0].type, Ci.nsINavHistoryService.TRANSITION_LINK);
 
     _("Let's modify the record and have the store update the database.");
     let secondvisit = {date: TIMESTAMP2,
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_keys.js
@@ -0,0 +1,129 @@
+var btoa;
+
+Cu.import("resource://services-sync/base_records/crypto.js");
+Cu.import("resource://services-sync/constants.js");
+btoa = Cu.import("resource://services-sync/util.js").btoa;
+
+function test_keymanager() {
+  let testKey = "ababcdefabcdefabcdefabcdef";
+  
+  let username = "john@example.com";
+  
+  // Decode the key here to mirror what generateEntry will do,
+  // but pass it encoded into the KeyBundle call below.
+  
+  let sha256inputE = Utils.makeHMACKey("" + HMAC_INPUT + username + "\x01");
+  let encryptKey = Utils.sha256HMACBytes(Utils.decodeKeyBase32(testKey), sha256inputE);
+  
+  let sha256inputH = Utils.makeHMACKey(encryptKey + HMAC_INPUT + username + "\x02");
+  let hmacKey = Utils.sha256HMACBytes(Utils.decodeKeyBase32(testKey), sha256inputH);
+  
+  // Encryption key is stored in base64 for WeaveCrypto convenience.
+  do_check_eq(btoa(encryptKey), new SyncKeyBundle(null, username, testKey).encryptionKey);
+  do_check_eq(hmacKey,          new SyncKeyBundle(null, username, testKey).hmacKey);
+  
+  // Test with the same KeyBundle for both.
+  let obj = new SyncKeyBundle(null, username, testKey);
+  do_check_eq(hmacKey, obj.hmacKey);
+  do_check_eq(btoa(encryptKey), obj.encryptionKey);
+}
+
+function do_check_keypair_eq(a, b) {
+  do_check_eq(2, a.length);
+  do_check_eq(2, b.length);
+  do_check_eq(a[0], b[0]);
+  do_check_eq(a[1], b[1]);
+}
+
+function test_collections_manager() {
+  let log = Log4Moz.repository.getLogger("Test");
+  Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
+  
+  let keyBundle = ID.set("WeaveCryptoID",
+      new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, "john@example.com", "a-bbbbb-ccccc-ddddd-eeeee-fffff"));
+  
+  /*
+   * Build a test version of storage/crypto/keys.
+   * Encrypt it with the sync key.
+   * Pass it into the CollectionKeyManager.
+   */
+  
+  log.info("Building storage keys...");
+  let storage_keys = new CryptoWrapper("crypto", "keys");
+  let default_key64 = Svc.Crypto.generateRandomKey();
+  let default_hmac64 = Svc.Crypto.generateRandomKey();
+  let bookmarks_key64 = Svc.Crypto.generateRandomKey();
+  let bookmarks_hmac64 = Svc.Crypto.generateRandomKey();
+  
+  storage_keys.cleartext = {
+    "default": [default_key64, default_hmac64],
+    "collections": {"bookmarks": [bookmarks_key64, bookmarks_hmac64]},
+  };
+  storage_keys.modified = Date.now()/1000;
+  storage_keys.id = "keys";
+  
+  log.info("Encrypting storage keys...");
+  
+  // 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.");
+  
+  // updateContents decrypts the object, but it also returns the payload
+  // for us to use.
+  let payload = CollectionKeys.updateContents(keyBundle, storage_keys);
+  
+  _("CK: " + JSON.stringify(CollectionKeys._collections));
+  
+  // Test that the CollectionKeyManager returns a similar WBO.
+  let wbo = CollectionKeys.asWBO("crypto", "keys");
+  
+  _("WBO: " + JSON.stringify(wbo));
+  
+  // Check the individual contents.
+  do_check_eq(wbo.collection, "crypto");
+  do_check_eq(wbo.id, "keys");
+  do_check_eq(storage_keys.modified, wbo.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);
+  
+  /*
+   * Test that we get the right keys out when we ask for
+   * a collection's tokens.
+   */
+  let b1 = new BulkKeyBundle(null, "bookmarks");
+  b1.keyPair = [bookmarks_key64, bookmarks_hmac64];
+  let b2 = CollectionKeys.keyForCollection("bookmarks");
+  do_check_keypair_eq(b1.keyPair, b2.keyPair);
+  
+  b1 = new BulkKeyBundle(null, "[default]");
+  b1.keyPair = [default_key64, default_hmac64];
+  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));
+  info_collections["crypto"] = 5000;
+  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));
+  
+  CollectionKeys._lastModified = null;
+  do_check_true(CollectionKeys.updateNeeded({}));
+}
+
+function run_test() {
+  test_keymanager();
+  test_collections_manager();
+}
--- a/services/sync/tests/unit/test_load_modules.js
+++ b/services/sync/tests/unit/test_load_modules.js
@@ -1,13 +1,12 @@
 const modules = [
                  "auth.js",
                  "base_records/collection.js",
                  "base_records/crypto.js",
-                 "base_records/keys.js",
                  "base_records/wbo.js",
                  "constants.js",
                  "engines/bookmarks.js",
                  "engines/clients.js",
                  "engines/forms.js",
                  "engines/history.js",
                  "engines/passwords.js",
                  "engines/prefs.js",
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -1,135 +1,156 @@
+Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/auth.js");
 Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 
-let keys, cryptoMeta, cryptoWrap;
-
-function pubkey_handler(metadata, response) {
-  let obj = {id: "pubkey",
-             modified: keys.pubkey.modified,
-             payload: JSON.stringify(keys.pubkey.payload)};
-  return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response);
-}
-
-function privkey_handler(metadata, response) {
-  let obj = {id: "privkey",
-             modified: keys.privkey.modified,
-             payload: JSON.stringify(keys.privkey.payload)};
-  return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response);
-}
+let cryptoWrap;
 
 function crypted_resource_handler(metadata, response) {
   let obj = {id: "resource",
              modified: cryptoWrap.modified,
              payload: JSON.stringify(cryptoWrap.payload)};
   return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response);
 }
 
-function crypto_meta_handler(metadata, response) {
-  let obj = {id: "steam",
-             modified: cryptoMeta.modified,
-             payload: JSON.stringify(cryptoMeta.payload)};
-  return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response);
+function prepareCryptoWrap(collection, id) {
+  let w = new CryptoWrapper();
+  w.cleartext.stuff = "my payload here";
+  w.collection = collection;
+  w.id = id;
+  return w;
 }
 
 function run_test() {
   let server;
   do_test_pending();
 
-  let passphrase = ID.set("WeaveCryptoID", new Identity());
-  passphrase.password = "passphrase";
+  let keyBundle = ID.set("WeaveCryptoID", new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, "john@example.com"));
+  keyBundle.keyStr = "a-abcde-abcde-abcde-abcde-abcde";
 
   try {
     let log = Log4Moz.repository.getLogger("Test");
     Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
     log.info("Setting up server and authenticator");
 
-    server = httpd_setup({"/keys/pubkey": pubkey_handler,
-                          "/keys/privkey": privkey_handler,
-                          "/steam/resource": crypted_resource_handler,
-                          "/crypto/steam": crypto_meta_handler});
+    server = httpd_setup({"/steam/resource": crypted_resource_handler});
 
     let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
     Auth.defaultAuthenticator = auth;
 
-    log.info("Generating keypair + symmetric key");
-
-    PubKeys.defaultKeyUri = "http://localhost:8080/keys/pubkey";
-    keys = PubKeys.createKeypair(passphrase,
-                                 "http://localhost:8080/keys/pubkey",
-                                 "http://localhost:8080/keys/privkey");
-    let crypto = Svc.Crypto;
-    keys.symkey = crypto.generateRandomKey();
-    keys.wrappedkey = crypto.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData);
-
-    log.info("Setting up keyring");
-
-    cryptoMeta = new CryptoMeta("http://localhost:8080/crypto/steam", auth);
-    cryptoMeta.addUnwrappedKey(keys.pubkey, keys.symkey);
-    CryptoMetas.set(cryptoMeta.uri, cryptoMeta);
-
     log.info("Creating a record");
 
     let cryptoUri = "http://localhost:8080/crypto/steam";
-    cryptoWrap = new CryptoWrapper("http://localhost:8080/steam/resource");
-    cryptoWrap.encryption = cryptoUri;
-    do_check_eq(cryptoWrap.encryption, cryptoUri);
-    do_check_eq(cryptoWrap.payload.encryption, "../crypto/steam");
+    cryptoWrap = prepareCryptoWrap("steam", "resource");
+    
+    log.info("cryptoWrap: " + cryptoWrap.toString());
 
     log.info("Encrypting a record");
 
-    cryptoWrap.cleartext.stuff = "my payload here";
-    cryptoWrap.encrypt(passphrase);
+    cryptoWrap.encrypt(keyBundle);
+    log.info("Ciphertext is " + cryptoWrap.ciphertext);
+    do_check_true(cryptoWrap.ciphertext != null);
+    
     let firstIV = cryptoWrap.IV;
 
     log.info("Decrypting the record");
 
-    let payload = cryptoWrap.decrypt(passphrase, cryptoUri);
+    let payload = cryptoWrap.decrypt(keyBundle);
     do_check_eq(payload.stuff, "my payload here");
     do_check_neq(payload, cryptoWrap.payload); // wrap.data.payload is the encrypted one
 
+    log.info("Make sure multiple decrypts cause failures");
+    let error = "";
+    try {
+      payload = cryptoWrap.decrypt(keyBundle);
+    }
+    catch(ex) {
+      error = ex;
+    }
+    do_check_eq(error, "No ciphertext: nothing to decrypt?");
+
     log.info("Re-encrypting the record with alternate payload");
 
     cryptoWrap.cleartext.stuff = "another payload";
-    cryptoWrap.encrypt(passphrase);
+    cryptoWrap.encrypt(keyBundle);
     let secondIV = cryptoWrap.IV;
-    payload = cryptoWrap.decrypt(passphrase, cryptoUri);
+    payload = cryptoWrap.decrypt(keyBundle);
     do_check_eq(payload.stuff, "another payload");
 
     log.info("Make sure multiple encrypts use different IVs");
     do_check_neq(firstIV, secondIV);
 
     log.info("Make sure differing ids cause failures");
-    cryptoWrap.encrypt(passphrase);
+    cryptoWrap.encrypt(keyBundle);
     cryptoWrap.data.id = "other";
-    let error = "";
+    error = "";
     try {
-      cryptoWrap.decrypt(passphrase, cryptoUri);
+      cryptoWrap.decrypt(keyBundle);
     }
     catch(ex) {
       error = ex;
     }
     do_check_eq(error, "Record id mismatch: resource,other");
 
     log.info("Make sure wrong hmacs cause failures");
-    cryptoWrap.encrypt(passphrase);
+    cryptoWrap.encrypt(keyBundle);
     cryptoWrap.hmac = "foo";
     error = "";
     try {
-      cryptoWrap.decrypt(passphrase, cryptoUri);
+      cryptoWrap.decrypt(keyBundle);
     }
     catch(ex) {
       error = ex;
     }
-    do_check_eq(error, "Record SHA256 HMAC mismatch: foo");
+    do_check_eq(error.substr(0, 32), "Record SHA256 HMAC mismatch: foo");
+
+    // Checking per-collection keys and default key handling.
+    
+    CollectionKeys.generateNewKeys();
+    let bu = "http://localhost:8080/storage/bookmarks/foo";
+    let bookmarkItem = prepareCryptoWrap("bookmarks", "foo");
+    bookmarkItem.encrypt();
+    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");
+    
+    // Per-collection keys.
+    // Generate a key for "bookmarks".
+    CollectionKeys.generateNewKeys(["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();
+    do_check_true(bookmarkItem.ciphertext != null);
+    
+    _("Default key is " + CollectionKeys._default.keyStr);
+    _("Bookmarks key is " + CollectionKeys.keyForCollection("bookmarks").keyStr);
+    _("Bookmarks key is " + CollectionKeys._collections["bookmarks"].keyStr);
+    
+    // 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);
+    } 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,
+        "my payload here");
 
     log.info("Done!");
   }
   finally {
     server.stop(do_test_finished);
   }
 }
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_records_crypto_generateEntry.js
@@ -0,0 +1,26 @@
+let atob = Cu.import("resource://services-sync/util.js").atob;
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/base_records/crypto.js");
+
+/**
+ * Testing the SHA256-HMAC key derivation process against st3fan's implementation
+ * in Firefox Home.
+ */
+function run_test() {
+  
+  // Test the production of keys from a sync key.
+  let bundle = new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, "st3fan", "q7ynpwq7vsc9m34hankbyi3s3i");
+  
+  // These should be compared to the results from Home, as they once were.
+  let e = "3fe2d3743fe03d4f460ce2405ec189e68dfd7e42c97d50fab9bda3761263cc87";
+  let h = "bf05f720423d297e8fd55faee7cdeaf32aa15cfb6e56115268c9c326b999795a";
+  
+  // The encryption key is stored as base64 for handing off to WeaveCrypto.
+  let realE = Utils.bytesAsHex(atob(bundle.encryptionKey));
+  let realH = Utils.bytesAsHex(bundle.hmacKey);
+  
+  _("Real E: " + realE);
+  _("Real H: " + realH);
+  do_check_eq(realH, h);
+  do_check_eq(realE, e);
+}
deleted file mode 100644
--- a/services/sync/tests/unit/test_records_cryptometa.js
+++ /dev/null
@@ -1,48 +0,0 @@
-Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/identity.js");
-
-function run_test() {
-  let passphrase = ID.set("WeaveCryptoID", new Identity());
-  passphrase.password = "passphrase";
-
-  _("Generating keypair to encrypt/decrypt symkeys");
-  let {pubkey, privkey} = PubKeys.createKeypair(
-    passphrase,
-    "http://site/pubkey",
-    "http://site/privkey"
-  );
-  PubKeys.set(pubkey.uri, pubkey);
-  PrivKeys.set(privkey.uri, privkey);
-
-  _("Generating a crypto meta with a random key");
-  let crypto = new CryptoMeta("http://site/crypto");
-  let symkey = Svc.Crypto.generateRandomKey();
-  crypto.addUnwrappedKey(pubkey, symkey);
-
-  _("Verifying correct HMAC by getting the key");
-  crypto.getKey(privkey, passphrase);
-
-  _("Generating a new crypto meta as the previous caches the unwrapped key");
-  let crypto = new CryptoMeta("http://site/crypto");
-  let symkey = Svc.Crypto.generateRandomKey();
-  crypto.addUnwrappedKey(pubkey, symkey);
-
-  _("Changing the HMAC to force a mismatch");
-  let relUri = crypto.uri.getRelativeSpec(pubkey.uri);
-  let goodHMAC = crypto.keyring[relUri].hmac;
-  crypto.keyring[relUri].hmac = "failme!";
-  let error = "";
-  try {
-    crypto.getKey(privkey, passphrase);
-  }
-  catch(ex) {
-    error = ex;
-  }
-  do_check_eq(error, "Key SHA256 HMAC mismatch: failme!");
-
-  _("Switching back to the correct HMAC and trying again");
-  crypto.keyring[relUri].hmac = goodHMAC;
-  crypto.getKey(privkey, passphrase);
-}
deleted file mode 100644
--- a/services/sync/tests/unit/test_records_keys.js
+++ /dev/null
@@ -1,108 +0,0 @@
-Cu.import("resource://services-sync/base_records/keys.js");
-Cu.import("resource://services-sync/auth.js");
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/util.js");
-
-function pubkey_handler(metadata, response) {
-  let obj = {id: "asdf-1234-asdf-1234",
-             modified: "2454725.98283",
-             payload: JSON.stringify({type: "pubkey",
-                                   privateKeyUri: "http://localhost:8080/privkey",
-                                   keyData: "asdfasdfasf..."})};
-  return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response);
-}
-
-function privkey_handler(metadata, response) {
-  let obj = {id: "asdf-1234-asdf-1234-2",
-             modified: "2454725.98283",
-             payload: JSON.stringify({type: "privkey",
-                                   publicKeyUri: "http://localhost:8080/pubkey",
-                                   keyData: "asdfasdfasf..."})};
-  return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response);
-}
-
-function test_get() {
-  let log = Log4Moz.repository.getLogger("Test");
-  Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
-
-  log.info("Setting up authenticator");
-
-  let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
-  Auth.defaultAuthenticator = auth;
-
-  log.info("Getting a public key");
-
-  let pubkey = PubKeys.get("http://localhost:8080/pubkey");
-  do_check_eq(pubkey.data.payload.type, "pubkey");
-  do_check_eq(PubKeys.response.status, 200);
-
-  log.info("Getting matching private key");
-
-  let privkey = PrivKeys.get(pubkey.privateKeyUri);
-  do_check_eq(privkey.data.payload.type, "privkey");
-  do_check_eq(PrivKeys.response.status, 200);
-
-  log.info("Done!");
-}
-
-
-function test_createKeypair() {
-  let passphrase = "moneyislike$\u20ac\u00a5\u5143";
-  let id = ID.set('foo', new Identity('foo', 'luser'));
-  id.password = passphrase;
-
-  _("Key pair requires URIs for both keys.");
-  let error;
-  try {
-    let result = PubKeys.createKeypair(id);
-  } catch(ex) {
-    error = ex;
-  }
-  do_check_eq(error, "Missing or null parameter 'pubkeyUri'.");
-
-  error = undefined;
-  try {
-    let result = PubKeys.createKeypair(id, "http://host/pub/key");
-  } catch(ex) {
-    error = ex;
-  }
-  do_check_eq(error, "Missing or null parameter 'privkeyUri'.");
-
-  _("Generate a key pair.");
-  let result = PubKeys.createKeypair(id, "http://host/pub/key", "http://host/priv/key");
-
-  _("Check that salt and IV are of correct length.");
-  // 16 bytes = 24 base64 encoded characters
-  do_check_eq(result.privkey.salt.length, 24);
-  do_check_eq(result.privkey.iv.length, 24);
-
-  _("URIs are set.");
-  do_check_eq(result.pubkey.uri.spec, "http://host/pub/key");
-  do_check_eq(result.pubkey.privateKeyUri.spec, "http://host/priv/key");
-  do_check_eq(result.pubkey.payload.privateKeyUri, "../priv/key");
-
-  do_check_eq(result.privkey.uri.spec, "http://host/priv/key");
-  do_check_eq(result.privkey.publicKeyUri.spec, "http://host/pub/key");
-  do_check_eq(result.privkey.payload.publicKeyUri, "../pub/key");
-
-  _("UTF8 encoded passphrase was used.");
-  do_check_true(Svc.Crypto.verifyPassphrase(result.privkey.keyData,
-                                            Utils.encodeUTF8(passphrase),
-                                            result.privkey.salt,
-                                            result.privkey.payload.iv));
-}
-
-function run_test() {
-  do_test_pending();
-  let server;
-  try {
-    server = httpd_setup({"/pubkey": pubkey_handler,
-                          "/privkey": privkey_handler});
-
-    test_get();
-    test_createKeypair();
-  } finally {
-    server.stop(do_test_finished);
-  }
-}
--- a/services/sync/tests/unit/test_records_wbo.js
+++ b/services/sync/tests/unit/test_records_wbo.js
@@ -46,34 +46,35 @@ function run_test() {
     let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
     Auth.defaultAuthenticator = auth;
 
     log.info("Getting a WBO record");
 
     let res = new Resource("http://localhost:8080/record");
     let resp = res.get();
 
-    let rec = new WBORecord("http://localhost:8080/record");
+    let rec = new WBORecord("coll", "record");
     rec.deserialize(res.data);
     do_check_eq(rec.id, "asdf-1234-asdf-1234"); // NOT "record"!
 
-    rec.uri = res.uri;
-    do_check_eq(rec.id, "record"); // NOT "asdf-1234-asdf-1234"!
-
     do_check_eq(rec.modified, 2454725.98283);
     do_check_eq(typeof(rec.payload), "object");
     do_check_eq(rec.payload.cheese, "roquefort");
     do_check_eq(resp.status, 200);
 
     log.info("Getting a WBO record using the record manager");
 
     let rec2 = Records.get("http://localhost:8080/record2");
     do_check_eq(rec2.id, "record2");
     do_check_eq(rec2.modified, 2454725.98284);
     do_check_eq(typeof(rec2.payload), "object");
     do_check_eq(rec2.payload.cheese, "gruyere");
     do_check_eq(Records.response.status, 200);
 
+    // Testing collection extraction.
+    log.info("Extracting collection.");
+    let rec3 = new WBORecord("tabs", "foo");   // Create through constructor.
+    do_check_eq(rec3.collection, "tabs");
     log.info("Done!");
   }
   catch (e) { do_throw(e); }
   finally { server.stop(do_test_finished); }
 }
--- a/services/sync/tests/unit/test_service_attributes.js
+++ b/services/sync/tests/unit/test_service_attributes.js
@@ -1,9 +1,8 @@
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 
 function test_identities() {
   _("Account related Service properties correspond to preference settings and update other object properties upon being set.");
@@ -57,51 +56,43 @@ function test_identities() {
   } finally {
     Svc.Prefs.resetBranch("");
   }
 }
 
 function test_urls() {
   _("URL related Service properties corresopnd to preference settings.");
   try {
-    do_check_eq(PubKeys.defaultKeyUri, undefined);
-    do_check_eq(PrivKeys.defaultKeyUri, undefined);
     do_check_true(!!Service.serverURL); // actual value may change
     do_check_eq(Service.clusterURL, "");
     do_check_eq(Service.userBaseURL, undefined);
     do_check_eq(Service.infoURL, undefined);
     do_check_eq(Service.storageURL, undefined);
     do_check_eq(Service.metaURL, undefined);
 
     _("The 'clusterURL' attribute updates preferences and cached URLs.");
     Service.username = "johndoe";
 
     // Since we don't have a cluster URL yet, these will still not be defined.
     do_check_eq(Service.infoURL, undefined);
     do_check_eq(Service.userBaseURL, undefined);
     do_check_eq(Service.storageURL, undefined);
     do_check_eq(Service.metaURL, undefined);
-    do_check_eq(PubKeys.defaultKeyUri, undefined);
-    do_check_eq(PrivKeys.defaultKeyUri, undefined);
 
     Service.serverURL = "http://weave.server/";
     Service.clusterURL = "http://weave.cluster/";
     do_check_eq(Svc.Prefs.get("clusterURL"), "http://weave.cluster/");
 
     do_check_eq(Service.userBaseURL, "http://weave.cluster/1.0/johndoe/");
     do_check_eq(Service.infoURL,
                 "http://weave.cluster/1.0/johndoe/info/collections");
     do_check_eq(Service.storageURL,
                 "http://weave.cluster/1.0/johndoe/storage/");
     do_check_eq(Service.metaURL,
                 "http://weave.cluster/1.0/johndoe/storage/meta/global");
-    do_check_eq(PubKeys.defaultKeyUri,
-                "http://weave.cluster/1.0/johndoe/storage/keys/pubkey");
-    do_check_eq(PrivKeys.defaultKeyUri,
-                "http://weave.cluster/1.0/johndoe/storage/keys/privkey");
 
     _("The 'miscURL' and 'userURL' attributes can be relative to 'serverURL' or absolute.");
     Svc.Prefs.set("miscURL", "relative/misc/");
     Svc.Prefs.set("userURL", "relative/user/");
     do_check_eq(Service.miscAPI,
                 "http://weave.server/relative/misc/1.0/");
     do_check_eq(Service.userAPI,
                 "http://weave.server/relative/user/1.0/");
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -22,29 +22,37 @@ function login_handler(request, response
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   do_test_pending();
   let server = httpd_setup({
     "/1.0/johndoe/info/collections": login_handler,
-    "/1.0/janedoe/info/collections": login_handler
+    "/1.0/janedoe/info/collections": login_handler,
+      
+    // We need these handlers because we test login, and login
+    // is where keys are generated or fetched.
+    // TODO: have Jane fetch her keys, not generate them...
+    "/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
+    "/1.0/johndoe/storage/meta/global": new ServerWBO().handler(),
+    "/1.0/janedoe/storage/crypto/keys": new ServerWBO().handler(),
+    "/1.0/janedoe/storage/meta/global": new ServerWBO().handler()
   });
 
   try {
     Service.serverURL = "http://localhost:8080/";
     Service.clusterURL = "http://localhost:8080/";
     Svc.Prefs.set("autoconnect", false);
 
     _("Force the initial state.");
     Status.service = STATUS_OK;
     do_check_eq(Status.service, STATUS_OK);
 
-    _("Try logging in. It wont' work because we're not configured yet.");
+    _("Try logging in. It won't work because we're not configured yet.");
     Service.login();
     do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
     do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME);
     do_check_false(Service.isLoggedIn);
     do_check_false(Svc.Prefs.get("autoconnect"));
 
     _("Try again with username and password set.");
     Service.username = "johndoe";
deleted file mode 100644
--- a/services/sync/tests/unit/test_service_passphraseUTF8.js
+++ /dev/null
@@ -1,150 +0,0 @@
-Cu.import("resource://services-sync/base_records/keys.js");
-Cu.import("resource://services-sync/main.js");
-
-function run_test() {
-  initTestLogging("Debug");
-
-  // Test fixtures:
-
-  // A private key with a unicode passphrase generated with Sync 1.4
-  // and the corresponding public key.
-  let pubkeyUri = "http://localhost:8080/1.0/foo/storage/keys/pubkey";
-  let privkeyUri = "http://localhost:8080/1.0/foo/storage/keys/privkey";
-
-  // a bunch of currency symbols
-  let passphrase = "moneyislike$\u20ac\u00a5\u5143";
-  let lowbyteonly = "moneyislike$\u00ac\u00a5\u0043";
-
-  let sync14privkey = {
-    salt: "Huk0JwJoJBXeKNioTZBQZA==",
-    iv: "bHPlRmSFfkUe5IcfjnoNTA==",
-    keyData: "MxAragjVoBL7erecOAaEgq/qSyLgVBFTHdEXpnP1tlEseobFsM+4z5Xhnxbows5+aL1nu50BNwr4Aao9A6Gky/Eeoo4vmWPGlQTaJgX4DDI3IrW6aXkoNzzJqvdYF2EbttnMz/x6JcXhX3KOmBgXl4bz2AgOwwyH1KCYTYhQ8PLB/BKh8vSR7gycwAqOC7OgaH1iAs4TMqy53DIlYGOdQKMxgsbQoRNGEIycBboMc35/e8PROVrp1LIUHniGtPj13J2BEi7sPGFswZbxOSnS+Zu1B8X1HVf3xlpKLGPsrD5ZWrtX/6hZS0tKR2M+ec1vIRYTGevAmICp+f6vdlp27bT9nAlSFwwHKodysNj3SyNQ2Dj6JuBkqSrjk9nV/73/8af8TF6NXZkCH9dRuI+gKrbzK1u6ikrg3ayQRzChLhqt4UvvkufMs2Pu8dIaZBgo1XSQiE+spMdsj/KEinyFeD3AnWi2WwesMSFrs0zWLHxHMgVJw0L6XXpTWPohf/uZ+O5fjULFZoyJ/Jr2AB67shm1wxkIaNIPACg9tjYstF2oyA6LsLUt1ctEXhoj/GjvnZqPXl9GGR1ElKbncKBOl9hxV4l1SEu70w/IVcluDhBIT3Fu1jX96TL+4UeRf3Qev84aY+7xSX2+OnWAFTMb6BsOcJmQXNh6PQ59eiv+AfwlxPedrWnP69+m/JfT5YvvzcEF6eiQKswOLid+O0aiHvqlhH/yxqHh5PjeGnNIWwtHO56d4tdfQsPGGF+dWGPIIw5hrG+GbdgA6nleIrZIEmxkyhVFikqSJLeFxB11+pF7Vk1wANgYPoxAh5T3CiCkX+k0KiJodqt+DThmmcgevdW3yOilmOPRkeYSlkfH9wFt+LnrJ5/LAglyOmudwc+amT6LYMUEp++NzPwk1Lx18hwXe7CQDvFaqvGB/XLQIPXqbCSdNVdRc3yADk5cA4caiOG37i2Qb/q6laJ3kjawSvSI43nkk4akGd1Gt533L1Ip1r70Tm1iV4nyeAO84CPgRPnDLR1KHxRNJZejVE4ouSIR/94veHRUDEFPrsae4JJlXVG1urmyPHXDdrWmquMkyjzbs+YB8JT1eFtT2Wqu9W8NZyMP0R57btNuXrw1qZm5QK5MmrCfqjGzx2dlv0Zito6CtQePlVjMq+gdprqB71Pl98WzvRNXZtK0uxVZjMJSLPqI1Zc9ju/lHgZgarsIFlKTGPcoqGnlUex2n8JBn9/sdiit1YLIFUMsrd7myyC6KaBFCoaFqOyFQjqD1phPBIM8fn5qp8vdsOwBHxIwfGet5UUa9jtrhu0f8ZT1FgYF2Add9bHxh8mlzsrr0dBMYC6/87hfHkcj08hRZgfG4glDlgzjVHGa4Z3KoyhdBmbFH8GRHp6BxjwdJNn9eUkT3bbelphU/wsIplwMDWbAT2fdphiwCYxH87sAiBR44PsQqOnHuzC36IE7Hes+t4f6iUzHF0yDX7txB0b069dHXMW9crXIAGzKMPcBaMn9ojeX6Y5rRfMLCN8nMQa3Bd1xU5/bgpxbsSnOgybIuHPWxMHksbQlj5x+PsLFQLYwAF5/PLjHqrDPcnybh6cJYjMI46vYJXY+A20L14YjRbqsDkO5nlJxBmV2n4s8RPpmLGo="
-  };
-  let sync14pubkey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyuTplSewMZI+iEuW2tQsEI06ySCht2c7oS5GHQkmVjFW3EWZwBtV+XFhq4T/XM/bhVKHYEQ8h74V14i3zFG/1+g8oQprhls59LMX0W6Q4YZqJQhw4egoS74ZkEQWSCZpQQlpCPfO339OdSUAEHl6Gn63CO06aBrsSWZm4Ue6PtoBdUMjl9Wf6ig0DKAlBxTB3XOXeGzsgnWOMcjyPloSY+LH7gf7LT+79FDCvAX0K85Qyii2kN2ScEzAgGMrGFlK8f7CBBNnjR9RKiOZT0+EzX5DgH8FfBBbbZDgGYcU0bKS262XrmnSbMzhgEl2/bH2w4xRnNdRaE0Dk+Ggi37/SwIDAQAB";
-
-  // A symmetric key that was wrapped with the above public key
-  let symkeyData = "Vyrj8pzynsZ6+9LzLJx+myDogLMOboaBMrOi1I3Xa9tO/x5mkoGs7owQbsK1eTbfW3WUAsTpGaozzSmOR27QbVFxksSS38OZsVkT5SAmkwQ/MDPB4A6kJ7fedyTru8o/2O/XGMEQM3Qp5ApE/DJkITWPAx1U3K1f21vV7Mo4dkKkgHCNjsTMplbcA77+EyRLBaQtMaBuJhUi91RLSwKzqpVU0MdmWxgjhv+7+hd76idlaxNDMsAk1n13rYI6Sv6aQ1jqTBBsD7YEa1sShJadB9NxOnl2sugPuEqqnQN6Emb8R2Ok59NzoY+Zhk4n3QkoDeWgJSa96sQbzVa3M88ZVg==";
-  let symkey = {keyring:{}};
-  symkey.keyring[pubkeyUri] = {
-    wrapped: symkeyData,
-    hmac: "f0f5f4e4ee08dfed196455496e67af97fcf80cbc425ce8e753591023d3194635"
-  };
-
-  // Hook these keys into the Weave API.
-  let privkey = new PrivKey(privkeyUri);
-  privkey.salt = sync14privkey.salt;
-  privkey.iv = sync14privkey.iv;
-  privkey.keyData = sync14privkey.keyData;
-  privkey.publicKeyUri = pubkeyUri;
-  PrivKeys.set(privkeyUri, privkey);
-
-  let pubkey = new PubKey(pubkeyUri);
-  pubkey.keyData = sync14pubkey;
-  pubkey.privateKeyUri = privkeyUri;
-  PubKeys.defaultKeyUri = pubkeyUri;
-  PubKeys.set(pubkeyUri, pubkey);
-
-  Weave.Service.username = "foo";
-  Weave.Service.clusterURL = "http://localhost:8080/";
-
-  // Set up an engine whose bulk key we need to reupload.
-  function SteamEngine() {
-    Weave.SyncEngine.call(this, "Steam");
-  }
-  SteamEngine.prototype = Weave.SyncEngine.prototype;
-  Weave.Engines.register(SteamEngine);
-
-  // Set up an engine whose bulk key won't have the right HMAC, so we
-  // need to wipe it.
-  function PetrolEngine() {
-    Weave.SyncEngine.call(this, "Petrol");
-  }
-  PetrolEngine.prototype = Weave.SyncEngine.prototype;
-  Weave.Engines.register(PetrolEngine);
-  let petrol_symkey = Weave.Utils.deepCopy(symkey);
-  petrol_symkey.keyring[pubkeyUri].hmac = "definitely-not-the-right-HMAC";
-
-  // Set up the server
-  let server_privkey = new ServerWBO('privkey');
-  let server_steam_key = new ServerWBO('steam', symkey);
-  let server_petrol_key = new ServerWBO('petrol', petrol_symkey);
-  let server_petrol_coll = new ServerCollection({
-    'obj': new ServerWBO('obj', {somedata: "that's going", toget: "wiped"})
-  });
-
-  do_test_pending();
-  let server = httpd_setup({
-    // Need these to make Weave.Service.wipeRemote() etc. happy
-    "/1.0/foo/storage/meta/global": new ServerWBO('global', {}).handler(),
-    "/1.0/foo/storage/crypto/clients": new ServerWBO('clients', {}).handler(),
-    "/1.0/foo/storage/clients": new ServerCollection().handler(),
-
-    // Records to reupload
-    "/1.0/foo/storage/keys/privkey": server_privkey.handler(),
-    "/1.0/foo/storage/crypto/steam": server_steam_key.handler(),
-
-    // Records that are going to be wiped
-    "/1.0/foo/storage/crypto/petrol": server_petrol_key.handler(),
-    "/1.0/foo/storage/petrol": server_petrol_coll.handler()
-  });
-
-  try {
-    _("The original key can be decoded with both the low byte only and the full unicode passphrase.");
-    do_check_true(Weave.Svc.Crypto.verifyPassphrase(sync14privkey.keyData,
-                                                    passphrase,
-                                                    sync14privkey.salt,
-                                                    sync14privkey.iv));
-    do_check_true(Weave.Svc.Crypto.verifyPassphrase(sync14privkey.keyData,
-                                                    lowbyteonly,
-                                                    sync14privkey.salt,
-                                                    sync14privkey.iv));
-
-    _("An obviously different passphrase will not work.");
-    Weave.Service.passphrase = "something completely different";
-    do_check_false(Weave.Service._verifyPassphrase());
-    do_check_eq(server_privkey.payload, undefined);
-
-    _("The right unicode passphrase will work, even though the key was generated with a low-byte only passphrase.");
-    Weave.Service.passphrase = passphrase;
-    do_check_true(Weave.Service._verifyPassphrase());
-
-    _("The _needUpdatedKeys flag is set.");
-    do_check_true(Weave.Service._needUpdatedKeys);
-
-    _("We can now call updateKeysToUTF8Passphrase to trigger an upload of a new privkey.");
-    Weave.Service._updateKeysToUTF8Passphrase();
-    do_check_true(!!server_privkey.payload);
-    do_check_eq(server_privkey.data.keyData, privkey.keyData);
-
-    _("The new key can't be decoded with the raw passphrase but the UTF8 encoded one.");
-    do_check_false(Weave.Svc.Crypto.verifyPassphrase(
-      server_privkey.data.keyData, passphrase,
-      server_privkey.data.salt, server_privkey.data.iv)
-    );
-
-    do_check_true(Weave.Svc.Crypto.verifyPassphrase(
-      server_privkey.data.keyData, Weave.Utils.encodeUTF8(passphrase),
-      server_privkey.data.salt, server_privkey.data.iv)
-    );
-
-    _("The 'steam' bulk key has been reuploaded (though only HMAC changes).");
-    let server_wrapped_key = server_steam_key.data.keyring[pubkeyUri];
-    do_check_eq(server_wrapped_key.wrapped, symkeyData);
-    let hmacKey = Weave.Svc.KeyFactory.keyFromString(
-        Ci.nsIKeyObject.HMAC, Weave.Utils.encodeUTF8(passphrase));
-    let hmac = Weave.Utils.sha256HMAC(symkeyData, hmacKey);
-    do_check_eq(server_wrapped_key.hmac, hmac);
-
-    _("The 'petrol' bulk key had an incorrect HMAC to begin with, so it and all the data from that engine has been wiped.");
-    do_check_eq(server_petrol_key.payload, undefined);
-    do_check_eq(server_petrol_coll.wbos.obj.payload, undefined);
-
-    _("The _needUpdatedKeys flag is no longer set.");
-    do_check_false(Weave.Service._needUpdatedKeys);
-
-  } finally {
-    if (server)
-      server.stop(do_test_finished);
-    Weave.Svc.Prefs.resetBranch("");
-  }
-}
--- a/services/sync/tests/unit/test_service_passwordUTF8.js
+++ b/services/sync/tests/unit/test_service_passwordUTF8.js
@@ -47,25 +47,29 @@ function change_password(request, respon
     body = status = "Unauthorized";
   }
   response.setStatusLine(request.httpVersion, statusCode, status);
   response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
   response.bodyOutputStream.write(body, body.length);
 }
 
 function run_test() {
+  initTestLogging("Trace");
+  
   do_test_pending();
   let server = httpd_setup({
     "/1.0/johndoe/info/collections": info_collections,
+    "/1.0/johndoe/storage/meta/global": new ServerWBO().handler(),
+    "/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
     "/user/1.0/johndoe/password": change_password
   });
 
   Service.username = "johndoe";
   Service.password = JAPANESE;
-  Service.passphrase = "Must exist, but contents irrelevant.";
+  Service.passphrase = "cantentsveryrelevantabbbb";
   Service.serverURL = "http://localhost:8080/";
 
   try {
     _("Try to log in with the password.");
     server_password = "foobar";
     do_check_false(Service.verifyLogin());
     do_check_eq(server_password, "foobar");
 
--- a/services/sync/tests/unit/test_service_persistLogin.js
+++ b/services/sync/tests/unit/test_service_persistLogin.js
@@ -1,16 +1,19 @@
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/constants.js");
 
 function run_test() {
   try {
+    // Ensure we have a blank slate to start.
+    Weave.Svc.Login.removeAllLogins();
+    
     Weave.Service.username = "johndoe";
     Weave.Service.password = "ilovejane";
-    Weave.Service.passphrase = "my preciousss";
+    Weave.Service.passphrase = "abbbbbcccccdddddeeeeefffff";
 
     _("Confirm initial environment is empty.");
     let logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null,
                                         PWDMGR_PASSWORD_REALM);
     do_check_eq(logins.length, 0);
     logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null,
                                         PWDMGR_PASSPHRASE_REALM);
     do_check_eq(logins.length, 0);
@@ -25,15 +28,15 @@ function run_test() {
     do_check_eq(logins[0].username, "johndoe");
     do_check_eq(logins[0].password, "ilovejane");
 
     _("The passphrase has been persisted in the login service.");
     logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null,
                                         PWDMGR_PASSPHRASE_REALM);
     do_check_eq(logins.length, 1);
     do_check_eq(logins[0].username, "johndoe");
-    do_check_eq(logins[0].password, "my preciousss");
+    do_check_eq(logins[0].password, "abbbbbcccccdddddeeeeefffff");
 
   } finally {
     Weave.Svc.Prefs.resetBranch("");
     Weave.Svc.Login.removeAllLogins();
   }
 }
--- a/services/sync/tests/unit/test_service_sync_401.js
+++ b/services/sync/tests/unit/test_service_sync_401.js
@@ -15,16 +15,18 @@ function login_handler(request, response
 }
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   do_test_pending();
   let server = httpd_setup({
+    "/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
+    "/1.0/johndoe/storage/meta/global": new ServerWBO().handler(),
     "/1.0/johndoe/info/collections": login_handler
   });
 
   const GLOBAL_SCORE = 42;
 
   try {
     _("Set up test fixtures.");
     Weave.Service.serverURL = "http://localhost:8080/";
--- a/services/sync/tests/unit/test_service_sync_checkServerError.js
+++ b/services/sync/tests/unit/test_service_sync_checkServerError.js
@@ -1,15 +1,16 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
+
 Cu.import("resource://services-sync/util.js");
-
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/base_records/crypto.js");
 
 initTestLogging();
 
 function CatapultEngine() {
   SyncEngine.call(this, "Catapult");
 }
 CatapultEngine.prototype = {
   __proto__: SyncEngine.prototype,
@@ -18,35 +19,33 @@ CatapultEngine.prototype = {
     throw this.exception;
   }
 };
 
 function sync_httpd_setup() {
   let handlers = {};
   handlers["/1.0/johndoe/info/collections"]
       = (new ServerWBO("collections", {})).handler(),
-  handlers["/1.0/johndoe/storage/keys/pubkey"]
-      = (new ServerWBO("pubkey")).handler();
-  handlers["/1.0/johndoe/storage/keys/privkey"]
-      = (new ServerWBO("privkey")).handler();
   handlers["/1.0/johndoe/storage/clients"]
       = (new ServerCollection()).handler();
   handlers["/1.0/johndoe/storage/crypto"]
       = (new ServerCollection()).handler();
+  handlers["/1.0/johndoe/storage/crypto/keys"]
+      = (new ServerWBO("keys", {})).handler();
   handlers["/1.0/johndoe/storage/crypto/clients"]
       = (new ServerWBO("clients", {})).handler();
   handlers["/1.0/johndoe/storage/meta/global"]
       = (new ServerWBO("global", {})).handler();
   return httpd_setup(handlers);
 }
 
 function setUp() {
   Service.username = "johndoe";
   Service.password = "ilovejane";
-  Service.passphrase = "sekrit";
+  Service.passphrase = "aabcdeabcdeabcdeabcdeabcde";
   Service.clusterURL = "http://localhost:8080/";
   new FakeCryptoService();
 }
 
 function test_backoff500() {
   _("Test: HTTP 500 sets backoff status.");
   let server = sync_httpd_setup();
   do_test_pending();
@@ -54,16 +53,21 @@ function test_backoff500() {
 
   Engines.register(CatapultEngine);
   let engine = Engines.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 500};
 
   try {
     do_check_false(Status.enforceBackoff);
+    
+    // Forcibly create and upload keys here -- otherwise we don't get to the 500!
+    CollectionKeys.generateNewKeys();
+    do_check_true(CollectionKeys.asWBO().upload("http://localhost:8080/1.0/johndoe/storage/crypto/keys").success);
+    
     Service.login();
     Service.sync();
     do_check_true(Status.enforceBackoff);
   } finally {
     server.stop(do_test_finished);
     Engines.unregister("catapult");
     Status.resetBackoff();
     Service.startOver();
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -1,26 +1,24 @@
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/base_records/wbo.js");      // For Records.
+Cu.import("resource://services-sync/base_records/crypto.js");   // For CollectionKeys.
 Cu.import("resource://services-sync/log4moz.js");
 
 function run_test() {
   if (DISABLE_TESTS_BUG_604565)
     return;
 
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   let guidSvc = new FakeGUIDService();
-  let cryptoSvc = new FakeCryptoService();
-
-  let keys = new ServerCollection({privkey: new ServerWBO('privkey'),
-                                   pubkey: new ServerWBO('pubkey')});
-  let crypto = new ServerCollection({keys: new ServerWBO('keys'),
-                                     clients: new ServerWBO('clients')});
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
 
   let collections = {};
   function info_collections(request, response) {
     let body = JSON.stringify(collections);
     response.setStatusLine(request.httpVersion, 200, "OK");
     response.bodyOutputStream.write(body, body.length);
@@ -31,80 +29,92 @@ function run_test() {
     return function() {
       wbo.wasCalled = true;
       handler.apply(this, arguments);
     };
   }
 
   do_test_pending();
   let server = httpd_setup({
-    "/1.0/johndoe/storage/keys": keys.handler(),
-    "/1.0/johndoe/storage/keys/pubkey": wasCalledHandler(keys.wbos.pubkey),
-    "/1.0/johndoe/storage/keys/privkey": wasCalledHandler(keys.wbos.privkey),
-
-    "/1.0/johndoe/storage/crypto": crypto.handler(),
-    "/1.0/johndoe/storage/crypto/keys": crypto.wbos.keys.handler(),
-    "/1.0/johndoe/storage/crypto/clients": crypto.wbos.clients.handler(),
-
+    "/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
     "/1.0/johndoe/storage/clients": clients.handler(),
     "/1.0/johndoe/storage/meta/global": wasCalledHandler(meta_global),
     "/1.0/johndoe/info/collections": info_collections
   });
 
   try {
     _("Log in.");
     Weave.Service.serverURL = "http://localhost:8080/";
     Weave.Service.clusterURL = "http://localhost:8080/";
+    
+    _("Checking Status.sync with no credentials.");
+    Weave.Service.verifyAndFetchSymmetricKeys();
+    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+    do_check_eq(Status.login, LOGIN_FAILED_INVALID_PASSPHRASE);
+
     Weave.Service.login("johndoe", "ilovejane", "foo");
     do_check_true(Weave.Service.isLoggedIn);
 
+    _("Checking that remoteSetup returns true when credentials have changed.");
+    Records.get(Weave.Service.metaURL).payload.syncID = "foobar";
+    do_check_true(Weave.Service._remoteSetup());
+    
     _("Do an initial sync.");
     let beforeSync = Date.now()/1000;
     Weave.Service.sync();
 
-    _("Verify that the meta record and keys was uploaded.");
+    _("Checking that remoteSetup returns true.");
+    do_check_true(Weave.Service._remoteSetup());
+
+    _("Verify that the meta record was uploaded.");
     do_check_eq(meta_global.data.syncID, Weave.Service.syncID);
     do_check_eq(meta_global.data.storageVersion, Weave.STORAGE_VERSION);
     do_check_eq(meta_global.data.engines.clients.version, Weave.Clients.version);
     do_check_eq(meta_global.data.engines.clients.syncID, Weave.Clients.syncID);
-    do_check_true(!!keys.wbos.privkey.payload);
-    do_check_true(!!keys.wbos.pubkey.payload);
 
     _("Set the collection info hash so that sync() will remember the modified times for future runs.");
     collections = {meta: Weave.Clients.lastSync,
-                   keys: Weave.Clients.lastSync,
-                   clients: Weave.Clients.lastSync,
-                   crypto: Weave.Clients.lastSync};
+                   clients: Weave.Clients.lastSync};
     Weave.Service.sync();
 
-    _("Sync again and verify that meta/global and keys weren't downloaded again");
+    _("Sync again and verify that meta/global wasn't downloaded again");
     meta_global.wasCalled = false;
-    keys.wbos.pubkey.wasCalled = false;
-    keys.wbos.privkey.wasCalled = false;
     Weave.Service.sync();
     do_check_false(meta_global.wasCalled);
-    do_check_false(keys.wbos.pubkey.wasCalled);
-    do_check_false(keys.wbos.privkey.wasCalled);
 
     _("Fake modified records. This will cause a redownload, but not reupload since it hasn't changed.");
     collections.meta += 42;
-    collections.keys += 23;
     meta_global.wasCalled = false;
-    keys.wbos.pubkey.wasCalled = false;
-    keys.wbos.privkey.wasCalled = false;
 
     let metaModified = meta_global.modified;
-    let pubkeyModified = keys.wbos.pubkey.modified;
-    let privkeyModified = keys.wbos.privkey.modified;
 
     Weave.Service.sync();
     do_check_true(meta_global.wasCalled);
-    do_check_true(keys.wbos.privkey.wasCalled);
-    do_check_true(keys.wbos.pubkey.wasCalled);
     do_check_eq(metaModified, meta_global.modified);
-    do_check_eq(privkeyModified, keys.wbos.privkey.modified);
-    do_check_eq(pubkeyModified, keys.wbos.pubkey.modified);
+
+    _("Checking bad passphrases.");
+    let pp = Weave.Service.passphrase;
+    Weave.Service.passphrase = "notvalid";
+    do_check_false(Weave.Service.verifyAndFetchSymmetricKeys());
+    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+    do_check_eq(Status.login, LOGIN_FAILED_INVALID_PASSPHRASE);
+    Weave.Service.passphrase = pp;
+    do_check_true(Weave.Service.verifyAndFetchSymmetricKeys());
+    
+    // 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 b = new BulkKeyBundle();
+    b.generateRandom();
+    collections.crypto = keys.modified = 100 + (Date.now()/1000);  // Future modification time.
+    keys.encrypt(b);
+    keys.upload(Weave.Service.cryptoKeysURL);
+    
+    do_check_false(Weave.Service.verifyAndFetchSymmetricKeys());
+    do_check_eq(Status.login, LOGIN_FAILED_INVALID_PASSPHRASE);
 
   } finally {
     Weave.Svc.Prefs.resetBranch("");
     server.stop(do_test_finished);
   }
 }
--- a/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
+++ b/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
@@ -1,13 +1,12 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/base_records/wbo.js");
 
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
 
 initTestLogging();
 
 function SteamEngine() {
@@ -29,48 +28,40 @@ StirlingEngine.prototype = {
   __proto__: SteamEngine.prototype,
   // This engine's enabled state is the same as the SteamEngine's.
   get prefName() "steam"
 };
 Engines.register(StirlingEngine);
 
 
 function sync_httpd_setup(handlers) {
+  let collections = {};
+  handlers["/1.0/johndoe/storage/crypto/keys"] = new ServerWBO().handler(),
   handlers["/1.0/johndoe/info/collections"]
-      = (new ServerWBO("collections", {})).handler(),
-  handlers["/1.0/johndoe/storage/keys/pubkey"]
-      = (new ServerWBO("pubkey")).handler();
-  handlers["/1.0/johndoe/storage/keys/privkey"]
-      = (new ServerWBO("privkey")).handler();
+      = (new ServerWBO("collections", collections)).handler(),
   handlers["/1.0/johndoe/storage/clients"]
       = (new ServerCollection()).handler();
-  handlers["/1.0/johndoe/storage/crypto"]
-      = (new ServerCollection()).handler();
-  handlers["/1.0/johndoe/storage/crypto/clients"]
-      = (new ServerWBO("clients", {})).handler();
   return httpd_setup(handlers);
 }
 
 function setUp() {
   Service.username = "johndoe";
   Service.password = "ilovejane";
   Service.passphrase = "sekrit";
   Service.clusterURL = "http://localhost:8080/";
   new FakeCryptoService();
-  createAndUploadKeypair();
 }
 
 const PAYLOAD = 42;
 
 function test_newAccount() {
   _("Test: New account does not disable locally enabled engines.");
   let engine = Engines.get("steam");
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global": new ServerWBO("global", {}).handler(),
-    "/1.0/johndoe/storage/crypto/steam": new ServerWBO("steam", {}).handler(),
     "/1.0/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Engine is enabled from the beginning.");
     Service._ignorePrefObserver = true;
@@ -93,17 +84,16 @@ function test_enabledLocally() {
   _("Test: Engine is disabled on remote clients and enabled locally");
   Service.syncID = "abcdefghij";
   let engine = Engines.get("steam");
   let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
                                          storageVersion: STORAGE_VERSION,
                                          engines: {}});
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global": metaWBO.handler(),
-    "/1.0/johndoe/storage/crypto/steam": new ServerWBO("steam", {}).handler(),
     "/1.0/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Enable engine locally.");
     engine.enabled = true;
@@ -128,21 +118,19 @@ function test_disabledLocally() {
   Service.syncID = "abcdefghij";
   let engine = Engines.get("steam");
   let metaWBO = new ServerWBO("global", {
     syncID: Service.syncID,
     storageVersion: STORAGE_VERSION,
     engines: {steam: {syncID: engine.syncID,
                       version: engine.version}}
   });
-  let steamCrypto = new ServerWBO("steam", PAYLOAD);
   let steamCollection = new ServerWBO("steam", PAYLOAD);
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global": metaWBO.handler(),
-    "/1.0/johndoe/storage/crypto/steam": steamCrypto.handler(),
     "/1.0/johndoe/storage/steam": steamCollection.handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Disable engine locally.");
     Service._ignorePrefObserver = true;
@@ -154,17 +142,16 @@ function test_disabledLocally() {
     Weave.Service.login();
     Weave.Service.sync();
 
     _("Meta record no longer contains engine.");
     do_check_false(!!metaWBO.data.engines.steam);
 
     _("Server records are wiped.");
     do_check_eq(steamCollection.payload, undefined);
-    do_check_eq(steamCrypto.payload, undefined);
 
     _("Engine continues to be disabled.");
     do_check_false(engine.enabled);
   } finally {
     server.stop(do_test_finished);
     Service.startOver();
   }
 }
@@ -176,17 +163,16 @@ function test_enabledRemotely() {
   let metaWBO = new ServerWBO("global", {
     syncID: Service.syncID,
     storageVersion: STORAGE_VERSION,
     engines: {steam: {syncID: engine.syncID,
                       version: engine.version}}
   });
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global": metaWBO.handler(),
-    "/1.0/johndoe/storage/crypto/steam": new ServerWBO("steam", {}).handler(),
     "/1.0/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Engine is disabled.");
     do_check_false(engine.enabled);
@@ -210,17 +196,16 @@ function test_disabledRemotely() {
   _("Test: Engine is enabled locally and disabled on a remote client");
   Service.syncID = "abcdefghij";
   let engine = Engines.get("steam");
   let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
                                          storageVersion: STORAGE_VERSION,
                                          engines: {}});
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global": metaWBO.handler(),
-    "/1.0/johndoe/storage/crypto/steam": new ServerWBO("steam", {}).handler(),
     "/1.0/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Enable engine locally.");
     Service._ignorePrefObserver = true;
@@ -247,19 +232,17 @@ function test_dependentEnginesEnabledLoc
   Service.syncID = "abcdefghij";
   let steamEngine = Engines.get("steam");
   let stirlingEngine = Engines.get("stirling");
   let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
                                          storageVersion: STORAGE_VERSION,
                                          engines: {}});
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global": metaWBO.handler(),
-    "/1.0/johndoe/storage/crypto/steam": new ServerWBO("steam", {}).handler(),
     "/1.0/johndoe/storage/steam": new ServerWBO("steam", {}).handler(),
-    "/1.0/johndoe/storage/crypto/stirling": new ServerWBO("stirling", {}).handler(),
     "/1.0/johndoe/storage/stirling": new ServerWBO("stirling", {}).handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Enable engine locally. Doing it on one is enough.");
     steamEngine.enabled = true;
@@ -290,25 +273,21 @@ function test_dependentEnginesDisabledLo
     syncID: Service.syncID,
     storageVersion: STORAGE_VERSION,
     engines: {steam: {syncID: steamEngine.syncID,
                       version: steamEngine.version},
               stirling: {syncID: stirlingEngine.syncID,
                          version: stirlingEngine.version}}
   });
 
-  let steamCrypto = new ServerWBO("steam", PAYLOAD);
   let steamCollection = new ServerWBO("steam", PAYLOAD);
-  let stirlingCrypto = new ServerWBO("stirling", PAYLOAD);
   let stirlingCollection = new ServerWBO("stirling", PAYLOAD);
   let server = sync_httpd_setup({
     "/1.0/johndoe/storage/meta/global":     metaWBO.handler(),
-    "/1.0/johndoe/storage/crypto/steam":    steamCrypto.handler(),
     "/1.0/johndoe/storage/steam":           steamCollection.handler(),
-    "/1.0/johndoe/storage/crypto/stirling": stirlingCrypto.handler(),
     "/1.0/johndoe/storage/stirling":        stirlingCollection.handler()
   });
   do_test_pending();
   setUp();
 
   try {
     _("Disable engines locally. Doing it on one is enough.");
     Service._ignorePrefObserver = true;
@@ -323,19 +302,17 @@ function test_dependentEnginesDisabledLo
     Weave.Service.sync();
 
     _("Meta record no longer contains engines.");
     do_check_false(!!metaWBO.data.engines.steam);
     do_check_false(!!metaWBO.data.engines.stirling);
 
     _("Server records are wiped.");
     do_check_eq(steamCollection.payload, undefined);
-    do_check_eq(steamCrypto.payload, undefined);
     do_check_eq(stirlingCollection.payload, undefined);
-    do_check_eq(stirlingCrypto.payload, undefined);
 
     _("Engines continue to be disabled.");
     do_check_false(steamEngine.enabled);
     do_check_false(stirlingEngine.enabled);
   } finally {
     server.stop(do_test_finished);
     Service.startOver();
   }
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -35,16 +35,18 @@ function service_unavailable(request, re
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   do_test_pending();
   let server = httpd_setup({
     "/api/1.0/johndoe/info/collections": login_handler,
     "/api/1.0/janedoe/info/collections": service_unavailable,
+    "/api/1.0/johndoe/storage/meta/global": new ServerWBO().handler(),
+    "/api/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
     "/user/1.0/johndoe/node/weave": send(200, "OK", "http://localhost:8080/api/")
   });
 
   try {
     Service.serverURL = "http://localhost:8080/";
 
     _("Force the initial state.");
     Status.service = STATUS_OK;
--- a/services/sync/tests/unit/test_service_wipeServer.js
+++ b/services/sync/tests/unit/test_service_wipeServer.js
@@ -1,11 +1,10 @@
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/resource.js");
 
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
 
 function FakeCollection() {
   this.deleted = false;
 }
@@ -25,172 +24,59 @@ FakeCollection.prototype = {
 };
 
 function serviceUnavailable(request, response) {
   let body = "Service Unavailable";
   response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
   response.bodyOutputStream.write(body, body.length);
 }
 
-function createAndUploadKeypair() {
-  let keys = PubKeys.createKeypair(ID.get("WeaveCryptoID"),
-                                   PubKeys.defaultKeyUri,
-                                   PrivKeys.defaultKeyUri);
-  PubKeys.uploadKeypair(keys);
-}
-
-function createAndUploadSymKey(url) {
-  let symkey = Svc.Crypto.generateRandomKey();
-  let pubkey = PubKeys.getDefaultKey();
-  let meta = new CryptoMeta(url);
-  meta.addUnwrappedKey(pubkey, symkey);
-  let res = new Resource(meta.uri);
-  res.put(meta);
-  CryptoMetas.set(url, meta);
-}
-
 function setUpTestFixtures() {
   let cryptoService = new FakeCryptoService();
 
   Service.clusterURL = "http://localhost:8080/";
   Service.username = "johndoe";
   Service.passphrase = "secret";
-
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/johndoe/storage/crypto/steam");
-  createAndUploadSymKey("http://localhost:8080/1.0/johndoe/storage/crypto/petrol");
-  createAndUploadSymKey("http://localhost:8080/1.0/johndoe/storage/crypto/diesel");
 }
 
-function test_withCollectionList_failOnCrypto() {
-  _("Service.wipeServer() deletes collections given as argument and aborts if a collection delete fails.");
-
-  let steam_coll = new FakeCollection();
-  let petrol_coll = new FakeCollection();
-  let diesel_coll = new FakeCollection();
-  let crypto_steam = new ServerWBO('steam');
-  let crypto_diesel = new ServerWBO('diesel');
-
-  let server = httpd_setup({
-    "/1.0/johndoe/storage/keys/pubkey": (new ServerWBO('pubkey')).handler(),
-    "/1.0/johndoe/storage/keys/privkey": (new ServerWBO('privkey')).handler(),
-    "/1.0/johndoe/storage/steam": steam_coll.handler(),
-    "/1.0/johndoe/storage/petrol": petrol_coll.handler(),
-    "/1.0/johndoe/storage/diesel": diesel_coll.handler(),
-    "/1.0/johndoe/storage/crypto/steam": crypto_steam.handler(),
-    "/1.0/johndoe/storage/crypto/petrol": serviceUnavailable,
-    "/1.0/johndoe/storage/crypto/diesel": crypto_diesel.handler()
-  });
-  do_test_pending();
-
-  try {
-    setUpTestFixtures();
-
-    _("Confirm initial environment.");
-    do_check_false(steam_coll.deleted);
-    do_check_false(petrol_coll.deleted);
-    do_check_false(diesel_coll.deleted);
-
-    do_check_true(crypto_steam.payload != undefined);
-    do_check_true(crypto_diesel.payload != undefined);
-
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel"));
-
-    _("wipeServer() will happily ignore the non-existent collection, delete the 'steam' collection and abort after an receiving an error on the 'petrol' collection's symkey.");
-    let error;
-    try {
-      Service.wipeServer(["non-existent", "steam", "petrol", "diesel"]);
-    } catch(ex) {
-      error = ex;
-    }
-    _("wipeServer() threw this exception: " + error);
-    do_check_true(error != undefined);
-
-    _("wipeServer stopped deleting after encountering an error with the 'petrol' collection's symkey, thus only 'steam' and 'petrol' have been deleted.");
-    do_check_true(steam_coll.deleted);
-    do_check_true(petrol_coll.deleted);
-    do_check_false(diesel_coll.deleted);
-
-    do_check_true(crypto_steam.payload == undefined);
-    do_check_true(crypto_diesel.payload != undefined);
-
-    do_check_false(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam"));
-    do_check_false(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel"));
-
-  } finally {
-    server.stop(do_test_finished);
-    Svc.Prefs.resetBranch("");
-    CryptoMetas.clearCache();
-  }
-}
-
-function test_withCollectionList_failOnCollection() {
+function test_withCollectionList_fail() {
   _("Service.wipeServer() deletes collections given as argument.");
 
   let steam_coll = new FakeCollection();
   let diesel_coll = new FakeCollection();
-  let crypto_steam = new ServerWBO('steam');
-  let crypto_petrol = new ServerWBO('petrol');
-  let crypto_diesel = new ServerWBO('diesel');
 
   let server = httpd_setup({
-    "/1.0/johndoe/storage/keys/pubkey": (new ServerWBO('pubkey')).handler(),
-    "/1.0/johndoe/storage/keys/privkey": (new ServerWBO('privkey')).handler(),
     "/1.0/johndoe/storage/steam": steam_coll.handler(),
     "/1.0/johndoe/storage/petrol": serviceUnavailable,
-    "/1.0/johndoe/storage/diesel": diesel_coll.handler(),
-    "/1.0/johndoe/storage/crypto/steam": crypto_steam.handler(),
-    "/1.0/johndoe/storage/crypto/petrol": crypto_petrol.handler(),
-    "/1.0/johndoe/storage/crypto/diesel": crypto_diesel.handler()
+    "/1.0/johndoe/storage/diesel": diesel_coll.handler()
   });
   do_test_pending();
 
   try {
     setUpTestFixtures();
 
     _("Confirm initial environment.");
     do_check_false(steam_coll.deleted);
     do_check_false(diesel_coll.deleted);
 
-    do_check_true(crypto_steam.payload != undefined);
-    do_check_true(crypto_petrol.payload != undefined);
-    do_check_true(crypto_diesel.payload != undefined);
-
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel"));
-
     _("wipeServer() will happily ignore the non-existent collection, delete the 'steam' collection and abort after an receiving an error on the 'petrol' collection.");
     let error;
     try {
       Service.wipeServer(["non-existent", "steam", "petrol", "diesel"]);
     } catch(ex) {
       error = ex;
     }
     _("wipeServer() threw this exception: " + error);
     do_check_true(error != undefined);
 
     _("wipeServer stopped deleting after encountering an error with the 'petrol' collection, thus only 'steam' has been deleted.");
     do_check_true(steam_coll.deleted);
     do_check_false(diesel_coll.deleted);
 
-    do_check_true(crypto_steam.payload == undefined);
-    do_check_true(crypto_petrol.payload != undefined);
-    do_check_true(crypto_diesel.payload != undefined);
-
-    do_check_false(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/steam"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/petrol"));
-    do_check_true(CryptoMetas.contains("http://localhost:8080/1.0/johndoe/storage/crypto/diesel"));
-
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
-    CryptoMetas.clearCache();
   }
 }
 
 function run_test() {
-  test_withCollectionList_failOnCollection();
-  test_withCollectionList_failOnCrypto();
+  test_withCollectionList_fail();
 }
--- a/services/sync/tests/unit/test_status_checkSetup.js
+++ b/services/sync/tests/unit/test_status_checkSetup.js
@@ -30,16 +30,16 @@ function run_test() {
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
     Status.resetSync();
 
     _("checkSetup() created a WeaveCryptoID identity");
     id = ID.get("WeaveCryptoID");
     do_check_true(!!id);
 
     _("Let's provide a passphrase");
-    id.password = "chickeninacan";
+    id.keyStr = "a-bcdef-abcde-acbde-acbde-acbde";
     do_check_eq(Status.checkSetup(), STATUS_OK);
     Status.resetSync();
 
   } finally {
     Svc.Prefs.resetBranch("");
   }
 }
--- a/services/sync/tests/unit/test_syncengine.js
+++ b/services/sync/tests/unit/test_syncengine.js
@@ -9,18 +9,16 @@ var syncTesting = new SyncTestingInfrast
 
 function test_url_attributes() {
   _("SyncEngine url attributes");
   Svc.Prefs.set("clusterURL", "https://cluster/");
   let engine = makeSteamEngine();
   try {
     do_check_eq(engine.storageURL, "https://cluster/1.0/foo/storage/");
     do_check_eq(engine.engineURL, "https://cluster/1.0/foo/storage/steam");
-    do_check_eq(engine.cryptoMetaURL,
-                "https://cluster/1.0/foo/storage/crypto/steam");
     do_check_eq(engine.metaURL, "https://cluster/1.0/foo/storage/meta/global");
   } finally {
     Svc.Prefs.resetBranch("");
   }
 }
 
 function test_syncID() {
   _("SyncEngine.syncID corresponds to preference");
@@ -92,38 +90,31 @@ function test_resetClient() {
 }
 
 function test_wipeServer() {
   _("SyncEngine.wipeServer deletes server data and resets the client.");
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   let engine = makeSteamEngine();
 
   const PAYLOAD = 42;
-  let steamCrypto = new ServerWBO("steam", PAYLOAD);
   let steamCollection = new ServerWBO("steam", PAYLOAD);
   let server = httpd_setup({
-    "/1.0/foo/storage/crypto/steam": steamCrypto.handler(),
     "/1.0/foo/storage/steam": steamCollection.handler()
   });
   do_test_pending();
 
   try {
     // Some data to reset.
     engine.lastSync = 123.45;
 
     _("Wipe server data and reset client.");
-    engine.wipeServer(true);
+    engine.wipeServer();
     do_check_eq(steamCollection.payload, undefined);
     do_check_eq(engine.lastSync, 0);
 
-    _("We passed a truthy arg earlier in which case it doesn't wipe the crypto collection.");
-    do_check_eq(steamCrypto.payload, PAYLOAD);
-    engine.wipeServer();
-    do_check_eq(steamCrypto.payload, undefined);
-
   } finally {
     server.stop(do_test_finished);
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
     Svc.Prefs.resetBranch("");
   }
 }
 
 function run_test() {
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -1,27 +1,26 @@
 Cu.import("resource://services-sync/base_records/crypto.js");
-Cu.import("resource://services-sync/base_records/keys.js");
 Cu.import("resource://services-sync/base_records/wbo.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/stores.js");
 Cu.import("resource://services-sync/trackers.js");
 Cu.import("resource://services-sync/util.js");
 
 /*
  * A fake engine implementation.
  * 
  * Complete with record, store, and tracker implementations.
  */
 
-function SteamRecord(uri) {
-  CryptoWrapper.call(this, uri);
+function SteamRecord(collection, id) {
+  CryptoWrapper.call(this, collection, id);
 }
 SteamRecord.prototype = {
   __proto__: CryptoWrapper.prototype
 };
 Utils.deferGetSet(SteamRecord, "cleartext", ["denomination"]);
 
 function SteamStore() {
   Store.call(this, "Steam");
@@ -41,18 +40,18 @@ SteamStore.prototype = {
   update: function Store_update(record) {
     this.items[record.id] = record.denomination;
   },
 
   itemExists: function Store_itemExists(id) {
     return (id in this.items);
   },
 
-  createRecord: function(id, uri) {
-    var record = new SteamRecord(uri);
+  createRecord: function(id, collection) {
+    var record = new SteamRecord(collection, id);
     record.denomination = this.items[id] || "Data for new record: " + id;
     return record;
   },
 
   changeItemID: function(oldID, newID) {
     this.items[newID] = this.items[oldID];
     delete this.items[oldID];
   },
@@ -106,20 +105,16 @@ var syncTesting = new SyncTestingInfrast
 
 /*
  * Test setup helpers
  */
 
 function sync_httpd_setup(handlers) {
   handlers["/1.0/foo/storage/meta/global"]
       = (new ServerWBO('global', {})).handler();
-  handlers["/1.0/foo/storage/keys/pubkey"]
-      = (new ServerWBO('pubkey')).handler();
-  handlers["/1.0/foo/storage/keys/privkey"]
-      = (new ServerWBO('privkey')).handler();
   return httpd_setup(handlers);
 }
 
 // Turn WBO cleartext into "encrypted" payload as it goes over the wire
 function encryptPayload(cleartext) {
   if (typeof cleartext == "object") {
     cleartext = JSON.stringify(cleartext);
   }
@@ -141,163 +136,63 @@ function encryptPayload(cleartext) {
  * - _uploadOutgoing()
  * - _syncFinish()
  * 
  * In the spirit of unit testing, these are tested individually for
  * different scenarios below.
  */
 
 function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
-  _("SyncEngine._syncStartup resets sync and wipes server data if there's no or an oudated global record");
+  _("SyncEngine._syncStartup resets sync and wipes server data if there's no or an outdated global record");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
 
   // Some server side data that's going to be wiped
   let collection = new ServerCollection();
   collection.wbos.flying = new ServerWBO(
       'flying', encryptPayload({id: 'flying',
                                 denomination: "LNER Class A3 4472"}));
   collection.wbos.scotsman = new ServerWBO(
       'scotsman', encryptPayload({id: 'scotsman',
                                   denomination: "Flying Scotsman"}));
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
 
   let engine = makeSteamEngine();
   engine._store.items = {rekolok: "Rekonstruktionslokomotive"};
   try {
 
     // Confirm initial environment
-    do_check_eq(crypto_steam.payload, undefined);
     do_check_eq(engine._tracker.changedIDs["rekolok"], undefined);
     let metaGlobal = Records.get(engine.metaURL);
     do_check_eq(metaGlobal.payload.engines, undefined);
     do_check_true(!!collection.wbos.flying.payload);
     do_check_true(!!collection.wbos.scotsman.payload);
 
     engine.lastSync = Date.now() / 1000;
     engine.lastSyncLocal = Date.now();
+    
+    // Trying to prompt a wipe -- we no longer track CryptoMeta per engine,
+    // so it has nothing to check.
     engine._syncStartup();
 
     // The meta/global WBO has been filled with data about the engine
     let engineData = metaGlobal.payload.engines["steam"];
     do_check_eq(engineData.version, engine.version);
     do_check_eq(engineData.syncID, engine.syncID);
 
     // Sync was reset and server data was wiped
     do_check_eq(engine.lastSync, 0);
     do_check_eq(collection.wbos.flying.payload, undefined);
     do_check_eq(collection.wbos.scotsman.payload, undefined);
 
-    // Bulk key was uploaded
-    do_check_true(!!crypto_steam.payload);
-    do_check_true(!!crypto_steam.data.keyring);
-
-  } finally {
-    server.stop(do_test_finished);
-    Svc.Prefs.resetBranch("");
-    Records.clearCache();
-    CryptoMetas.clearCache();
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
-  }
-}
-
-function test_syncStartup_metaGet404() {
-  _("SyncEngine._syncStartup resets sync and wipes server data if the symmetric key is missing 404");
-
-  Svc.Prefs.set("clusterURL", "http://localhost:8080/");
-  Svc.Prefs.set("username", "foo");
-
-  // A symmetric key with an incorrect HMAC
-  let crypto_steam = new ServerWBO("steam");
-
-  // A proper global record with matching version and syncID
-  let engine = makeSteamEngine();
-  let global = new ServerWBO("global",
-                             {engines: {steam: {version: engine.version,
-                                                syncID: engine.syncID}}});
-
-  // Some server side data that's going to be wiped
-  let collection = new ServerCollection();
-  collection.wbos.flying = new ServerWBO(
-      "flying", encryptPayload({id: "flying",
-                                denomination: "LNER Class A3 4472"}));
-  collection.wbos.scotsman = new ServerWBO(
-      "scotsman", encryptPayload({id: "scotsman",
-                                  denomination: "Flying Scotsman"}));
-
-  let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
-      "/1.0/foo/storage/steam": collection.handler()
-  });
-  do_test_pending();
-  createAndUploadKeypair();
-
-  try {
-
-    _("Confirm initial environment");
-    do_check_false(!!crypto_steam.payload);
-    do_check_true(!!collection.wbos.flying.payload);
-    do_check_true(!!collection.wbos.scotsman.payload);
-
-    engine.lastSync = Date.now() / 1000;
-    engine.lastSyncLocal = Date.now();
-    engine._syncStartup();
-
-    _("Sync was reset and server data was wiped");
-    do_check_eq(engine.lastSync, 0);
-    do_check_eq(collection.wbos.flying.payload, undefined);
-    do_check_eq(collection.wbos.scotsman.payload, undefined);
-
-    _("New bulk key was uploaded");
-    let key = crypto_steam.data.keyring["../keys/pubkey"];
-    do_check_eq(key.wrapped, "fake-symmetric-key-0");
-    do_check_eq(key.hmac, "fake-symmetric-key-0                                            ");
-
-  } finally {
-    server.stop(do_test_finished);
-    Svc.Prefs.resetBranch("");
-    Records.clearCache();
-    CryptoMetas.clearCache();
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
-  }
-}
-
-function test_syncStartup_failedMetaGet() {
-  _("SyncEngine._syncStartup non-404 failures for getting cryptometa should stop sync");
-
-  Svc.Prefs.set("clusterURL", "http://localhost:8080/");
-  Svc.Prefs.set("username", "foo");
-  let server = httpd_setup({
-    "/1.0/foo/storage/crypto/steam": function(request, response) {
-      response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
-      response.bodyOutputStream.write("Fail!", 5);
-    }
-  });
-  do_test_pending();
-
-  let engine = makeSteamEngine();
-  try {
-
-    _("Getting the cryptometa will fail and should set the appropriate failure");
-    let error;
-    try {
-      engine._syncStartup();
-    } catch (ex) {
-      error = ex;
-    }
-    do_check_eq(error.failureCode, ENGINE_METARECORD_DOWNLOAD_FAIL);
-
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
@@ -334,241 +229,148 @@ function test_syncStartup_serverHasNewer
 }
 
 
 function test_syncStartup_syncIDMismatchResetsClient() {
   _("SyncEngine._syncStartup resets sync if syncIDs don't match");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
-  let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler()
-  });
+  let server = sync_httpd_setup({});
   do_test_pending();
 
   // global record with a different syncID than our engine has
   let engine = makeSteamEngine();
   let global = new ServerWBO('global',
                              {engines: {steam: {version: engine.version,
                                                 syncID: 'foobar'}}});
   server.registerPathHandler("/1.0/foo/storage/meta/global", global.handler());
 
-  createAndUploadKeypair();
-
   try {
 
     // Confirm initial environment
     do_check_eq(engine.syncID, 'fake-guid-0');
-    do_check_eq(crypto_steam.payload, undefined);
     do_check_eq(engine._tracker.changedIDs["rekolok"], undefined);
 
     engine.lastSync = Date.now() / 1000;
     engine.lastSyncLocal = Date.now();
     engine._syncStartup();
 
     // The engine has assumed the server's syncID 
     do_check_eq(engine.syncID, 'foobar');
 
     // Sync was reset
     do_check_eq(engine.lastSync, 0);
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
-  }
-}
-
-
-function test_syncStartup_badKeyWipesServerData() {
-  _("SyncEngine._syncStartup resets sync and wipes server data if there's something wrong with the symmetric key");
-
-  Svc.Prefs.set("clusterURL", "http://localhost:8080/");
-  Svc.Prefs.set("username", "foo");
-
-  // A symmetric key with an incorrect HMAC
-  let crypto_steam = new ServerWBO('steam');
-  crypto_steam.payload = JSON.stringify({
-    keyring: {
-      "http://localhost:8080/1.0/foo/storage/keys/pubkey": {
-        wrapped: Svc.Crypto.generateRandomKey(),
-        hmac: "this-hmac-is-incorrect"
-      }
-    }
-  });
-
-  // A proper global record with matching version and syncID
-  let engine = makeSteamEngine();
-  let global = new ServerWBO('global',
-                             {engines: {steam: {version: engine.version,
-                                                syncID: engine.syncID}}});
-
-  // Some server side data that's going to be wiped
-  let collection = new ServerCollection();
-  collection.wbos.flying = new ServerWBO(
-      'flying', encryptPayload({id: 'flying',
-                                denomination: "LNER Class A3 4472"}));
-  collection.wbos.scotsman = new ServerWBO(
-      'scotsman', encryptPayload({id: 'scotsman',
-                                  denomination: "Flying Scotsman"}));
-
-  let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
-      "/1.0/foo/storage/steam": collection.handler()
-  });
-  do_test_pending();
-  createAndUploadKeypair();
-
-  try {
-
-    // Confirm initial environment
-    let key = crypto_steam.data.keyring["http://localhost:8080/1.0/foo/storage/keys/pubkey"];
-    do_check_eq(key.wrapped, "fake-symmetric-key-0");
-    do_check_eq(key.hmac, "this-hmac-is-incorrect");
-    do_check_true(!!collection.wbos.flying.payload);
-    do_check_true(!!collection.wbos.scotsman.payload);
-
-    engine.lastSync = Date.now() / 1000;
-    engine.lastSyncLocal = Date.now();
-    engine._syncStartup();
-
-    // Sync was reset and server data was wiped
-    do_check_eq(engine.lastSync, 0);
-    do_check_eq(collection.wbos.flying.payload, undefined);
-    do_check_eq(collection.wbos.scotsman.payload, undefined);
-
-    // New bulk key was uploaded
-    key = crypto_steam.data.keyring["../keys/pubkey"];
-    do_check_eq(key.wrapped, "fake-symmetric-key-1");
-    do_check_eq(key.hmac, "fake-symmetric-key-1                                            ");
-
-  } finally {
-    server.stop(do_test_finished);
-    Svc.Prefs.resetBranch("");
-    Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_processIncoming_emptyServer() {
   _("SyncEngine._processIncoming working with an empty server backend");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
   let collection = new ServerCollection();
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
 
   let engine = makeSteamEngine();
   try {
 
     // Merely ensure that this code path is run without any errors
     engine._processIncoming();
     do_check_eq(engine.lastSync, 0);
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_processIncoming_createFromServer() {
   _("SyncEngine._processIncoming creates new records from server data");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
+  
+  CollectionKeys.generateNewKeys();
 
   // Some server records that will be downloaded
   let collection = new ServerCollection();
   collection.wbos.flying = new ServerWBO(
       'flying', encryptPayload({id: 'flying',
                                 denomination: "LNER Class A3 4472"}));
   collection.wbos.scotsman = new ServerWBO(
       'scotsman', encryptPayload({id: 'scotsman',
                                   denomination: "Flying Scotsman"}));
 
   // Two pathological cases involving relative URIs gone wrong.
   collection.wbos['../pathological'] = new ServerWBO(
       '../pathological', encryptPayload({id: '../pathological',
                                          denomination: "Pathological Case"}));
-  let wrong_keyuri = encryptPayload({id: "wrong_keyuri",
-                                     denomination: "Wrong Key URI"});
-  wrong_keyuri.encryption = "../../crypto/steam";
-  collection.wbos["wrong_keyuri"] = new ServerWBO("wrong_keyuri", wrong_keyuri);
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler(),
       "/1.0/foo/storage/steam/flying": collection.wbos.flying.handler(),
       "/1.0/foo/storage/steam/scotsman": collection.wbos.scotsman.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
 
   let engine = makeSteamEngine();
   let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
   meta_global.payload.engines = {steam: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
 
     // Confirm initial environment
     do_check_eq(engine.lastSync, 0);
     do_check_eq(engine.lastModified, null);
     do_check_eq(engine._store.items.flying, undefined);
     do_check_eq(engine._store.items.scotsman, undefined);
     do_check_eq(engine._store.items['../pathological'], undefined);
-    do_check_eq(engine._store.items.wrong_keyuri, undefined);
 
     engine._syncStartup();
     engine._processIncoming();
 
     // Timestamps of last sync and last server modification are set.
     do_check_true(engine.lastSync > 0);
     do_check_true(engine.lastModified > 0);
 
     // Local records have been created from the server data.
     do_check_eq(engine._store.items.flying, "LNER Class A3 4472");
     do_check_eq(engine._store.items.scotsman, "Flying Scotsman");
     do_check_eq(engine._store.items['../pathological'], "Pathological Case");
-    do_check_eq(engine._store.items.wrong_keyuri, "Wrong Key URI");
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_processIncoming_reconcile() {
   _("SyncEngine._processIncoming updates local records");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
   let collection = new ServerCollection();
 
   // This server record is newer than the corresponding client one,
   // so it'll update its data.
   collection.wbos.newrecord = new ServerWBO(
       'newrecord', encryptPayload({id: 'newrecord',
                                    denomination: "New stuff..."}));
 
@@ -607,22 +409,19 @@ function test_processIncoming_reconcile(
   // This record is marked as deleted, so we're expecting the client
   // record to be removed.
   collection.wbos.nukeme = new ServerWBO(
       'nukeme', encryptPayload({id: 'nukeme',
                                 denomination: "Nuke me!",
                                 deleted: true}));
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
 
   let engine = makeSteamEngine();
   engine._store.items = {newerserver: "New data, but not as new as server!",
                          olderidentical: "Older but identical",
                          updateclient: "Got data?",
                          original: "Original Entry",
                          long_original: "Long Original Entry",
                          nukeme: "Nuke me!"};
@@ -674,17 +473,16 @@ function test_processIncoming_reconcile(
 
     // The 'nukeme' record marked as deleted is removed.
     do_check_eq(engine._store.items.nukeme, undefined);
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_processIncoming_mobile_batchSize() {
   _("SyncEngine._processIncoming doesn't fetch everything at once on mobile clients");
 
@@ -708,22 +506,19 @@ function test_processIncoming_mobile_bat
     let id = 'record-no-' + i;
     let payload = encryptPayload({id: id, denomination: "Record No. " + i});
     let wbo = new ServerWBO(id, payload);
     wbo.modified = Date.now()/1000 - 60*(i+10);
     collection.wbos[id] = wbo;
   }
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
 
   let engine = makeSteamEngine();
   let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
   meta_global.payload.engines = {steam: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
 
@@ -752,41 +547,37 @@ function test_processIncoming_mobile_bat
       else
         do_check_eq(collection.get_log[i+1].ids.length, 234 % MOBILE_BATCH_SIZE);
     }
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_uploadOutgoing_toEmptyServer() {
   _("SyncEngine._uploadOutgoing uploads new records to server");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
   let collection = new ServerCollection();
   collection.wbos.flying = new ServerWBO('flying');
   collection.wbos.scotsman = new ServerWBO('scotsman');
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler(),
       "/1.0/foo/storage/steam/flying": collection.wbos.flying.handler(),
       "/1.0/foo/storage/steam/scotsman": collection.wbos.scotsman.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
+  CollectionKeys.generateNewKeys();
 
   let engine = makeSteamEngine();
   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);
 
@@ -817,40 +608,35 @@ function test_uploadOutgoing_toEmptyServ
 
     // The 'flying' record wasn't marked so it wasn't uploaded
     do_check_eq(collection.wbos.flying.payload, undefined);
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_uploadOutgoing_failed() {
   _("SyncEngine._uploadOutgoing doesn't clear the tracker of objects that failed to upload.");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
   let collection = new ServerCollection();
   // We only define the "flying" WBO on the server, not the "scotsman"
   // and "peppercorn" ones.
   collection.wbos.flying = new ServerWBO('flying');
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
 
   let engine = makeSteamEngine();
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine._store.items = {flying: "LNER Class A3 4472",
                          scotsman: "Flying Scotsman",
                          peppercorn: "Peppercorn Class"};
   // Mark these records as changed 
   const FLYING_CHANGED = 12345;
@@ -887,28 +673,26 @@ function test_uploadOutgoing_failed() {
     // they weren't cleared from the tracker.
     do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
     do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
   _("SyncEngine._uploadOutgoing uploads in batches of MAX_UPLOAD_RECORDS");
 
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
-  let crypto_steam = new ServerWBO('steam');
   let collection = new ServerCollection();
 
   // Let's count how many times the client posts to the server
   var noOfUploads = 0;
   collection.post = (function(orig) {
     return function() {
       noOfUploads++;
       return orig.apply(this, arguments);
@@ -924,22 +708,19 @@ function test_uploadOutgoing_MAX_UPLOAD_
     collection.wbos[id] = new ServerWBO(id);
   }
 
   let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
   meta_global.payload.engines = {steam: {version: engine.version,
                                          syncID: engine.syncID}};
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
 
   try {
 
     // Confirm initial environment
     do_check_eq(noOfUploads, 0);
 
     engine._syncStartup();
     engine._uploadOutgoing();
@@ -951,17 +732,16 @@ function test_uploadOutgoing_MAX_UPLOAD_
 
     // Ensure that the uploads were performed in batches of MAX_UPLOAD_RECORDS
     do_check_eq(noOfUploads, Math.ceil(234/MAX_UPLOAD_RECORDS));
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
 
 function test_syncFinish_noDelete() {
   _("SyncEngine._syncFinish resets tracker's score");
   let engine = makeSteamEngine();
@@ -1100,18 +880,17 @@ function test_sync_partialUpload() {
 
   let crypto_steam = new ServerWBO('steam');
   let collection = new ServerCollection();
   let server = sync_httpd_setup({
       "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
+  CollectionKeys.generateNewKeys();
 
   let engine = makeSteamEngine();
   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) {
@@ -1162,36 +941,37 @@ function test_sync_partialUpload() {
       else
         do_check_false(id in engine._tracker.changedIDs);
     }
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
     Records.clearCache();
-    CryptoMetas.clearCache();
     syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
   }
 }
 
-function test_canDecrypt_noCryptoMeta() {
-  _("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key.");
+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.");
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
 
+  // Wipe CollectionKeys so we can test the desired scenario.
+  CollectionKeys.setContents({"collections": {}, "default": null});
+
   let collection = new ServerCollection();
   collection.wbos.flying = new ServerWBO(
       'flying', encryptPayload({id: 'flying',
                                 denomination: "LNER Class A3 4472"}));
 
   let server = sync_httpd_setup({
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
 
   let engine = makeSteamEngine();
   try {
 
     do_check_false(engine.canDecrypt());
 
   } finally {
     server.stop(do_test_finished);
@@ -1201,29 +981,28 @@ function test_canDecrypt_noCryptoMeta() 
   }
 }
 
 function test_canDecrypt_true() {
   _("SyncEngine.canDecrypt returns true if the engine can decrypt the items on the server.");
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
 
-  let crypto_steam = new ServerWBO('steam');
+  // Set up CollectionKeys, as service.js does.
+  CollectionKeys.generateNewKeys();
+  
   let collection = new ServerCollection();
   collection.wbos.flying = new ServerWBO(
       'flying', encryptPayload({id: 'flying',
                                 denomination: "LNER Class A3 4472"}));
 
   let server = sync_httpd_setup({
-      "/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
       "/1.0/foo/storage/steam": collection.handler()
   });
   do_test_pending();
-  createAndUploadKeypair();
-  createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
 
   let engine = makeSteamEngine();
   try {
 
     do_check_true(engine.canDecrypt());
 
   } finally {
     server.stop(do_test_finished);
@@ -1234,27 +1013,24 @@ function test_canDecrypt_true() {
 }
 
 
 function run_test() {
   if (DISABLE_TESTS_BUG_604565)
     return;
 
   test_syncStartup_emptyOrOutdatedGlobalsResetsSync();
-  test_syncStartup_metaGet404();
-  test_syncStartup_failedMetaGet();
   test_syncStartup_serverHasNewerVersion();
   test_syncStartup_syncIDMismatchResetsClient();
-  test_syncStartup_badKeyWipesServerData();
   test_processIncoming_emptyServer();
   test_processIncoming_createFromServer();
   test_processIncoming_reconcile();
   test_processIncoming_mobile_batchSize();
   test_uploadOutgoing_toEmptyServer();
   test_uploadOutgoing_failed();
   test_uploadOutgoing_MAX_UPLOAD_RECORDS();
   test_syncFinish_noDelete();
   test_syncFinish_deleteByIds();
   test_syncFinish_deleteLotsInBatches();
   test_sync_partialUpload();
-  test_canDecrypt_noCryptoMeta();
+  test_canDecrypt_noCryptoKeys();
   test_canDecrypt_true();
 }
--- a/services/sync/tests/unit/test_tab_store.js
+++ b/services/sync/tests/unit/test_tab_store.js
@@ -96,23 +96,23 @@ function test_createRecord() {
   // get some values before testing
   fakeSessionSvc("http://foo.com");
   let tabs = store.getAllTabs();
   let tabsize = JSON.stringify(tabs[0]).length;
   let numtabs = Math.ceil(20000./77.);
 
   _("create a record");
   fakeSessionSvc("http://foo.com");
-  record = store.createRecord("fake-guid", "http://fake.uri/");
+  record = store.createRecord("fake-guid");
   do_check_true(record instanceof TabSetRecord);
   do_check_eq(record.tabs.length, 1);
 
   _("create a big record");
   fakeSessionSvc("http://foo.com", numtabs);
-  record = store.createRecord("fake-guid", "http://fake.uri/");
+  record = store.createRecord("fake-guid");
   do_check_true(record instanceof TabSetRecord);
   do_check_eq(record.tabs.length, 256);
 }
 
 function run_test() {
   test_create();
   test_getAllTabs();
   test_createRecord();
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_utils_atob.js
@@ -0,0 +1,8 @@
+Cu.import("resource://services-sync/util.js");
+
+function run_test() {
+  let data = ["Zm9vYmE=", "Zm9vYmE==", "Zm9vYmE==="];
+  for (let d in data) {
+    do_check_eq(Utils.safeAtoB(data[d]), "fooba");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_utils_deriveKey.js
@@ -0,0 +1,61 @@
+let cryptoSvc;
+try {
+  Components.utils.import("resource://services-crypto/WeaveCrypto.js");
+  cryptoSvc = new WeaveCrypto();
+} catch (ex) {
+  // Fallback to binary WeaveCrypto
+  cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]
+                .getService(Ci.IWeaveCrypto);
+}
+
+Cu.import("resource://services-sync/util.js");
+
+function run_test() {
+  var iv = cryptoSvc.generateRandomIV();
+  var der_passphrase = "secret phrase";
+  var der_salt = "RE5YUHpQcGl3bg==";   // btoa("DNXPzPpiwn")
+  
+  _("Testing deriveKeyFromPassphrase. Input is \"" + der_passphrase + "\", \"" + der_salt + "\" (base64-encoded).");
+  
+  // Test friendly-ing.
+  do_check_eq("abcdefghijk8mn9pqrstuvwxyz234567",
+              Utils.base32ToFriendly("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"));
+  do_check_eq("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
+              Utils.base32FromFriendly(
+                Utils.base32ToFriendly("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")));
+  
+  // Test translation.
+  do_check_false(Utils.isPassphrase("o-5wmnu-o5tqc-7lz2h-amkbw-izqzi"));  // Wrong charset.
+  do_check_false(Utils.isPassphrase("O-5WMNU-O5TQC-7LZ2H-AMKBW-IZQZI"));  // Wrong charset.
+  do_check_true(Utils.isPassphrase("9-5wmnu-95tqc-78z2h-amkbw-izqzi"));
+  do_check_true(Utils.isPassphrase("9-5WMNU-95TQC-78Z2H-AMKBW-IZQZI"));   // isPassphrase normalizes.
+  do_check_true(Utils.isPassphrase(
+      Utils.normalizePassphrase("9-5WMNU-95TQC-78Z2H-AMKBW-IZQZI")));
+    
+  // Base64. We don't actually use this in anger, particularly not with a 32-byte key.
+  var der_key = Utils.deriveEncodedKeyFromPassphrase(der_passphrase, der_salt);
+  _("Derived key in base64: " + der_key);
+  do_check_eq(cryptoSvc.decrypt(cryptoSvc.encrypt("bacon", der_key, iv), der_key, iv), "bacon");
+  
+  // Test the equivalence of our NSS and JS versions.
+  // Will only work on FF4, of course.
+  do_check_eq(
+      Utils.deriveEncodedKeyFromPassphrase(der_passphrase, der_salt, 16, false),
+      Utils.deriveEncodedKeyFromPassphrase(der_passphrase, der_salt, 16, true));
+  
+  // Base64, 16-byte output.
+  var der_key = Utils.deriveEncodedKeyFromPassphrase(der_passphrase, der_salt, 16);
+  _("Derived key in base64: " + der_key);
+  do_check_eq("d2zG0d2cBfXnRwMUGyMwyg==", der_key);
+  do_check_eq(cryptoSvc.decrypt(cryptoSvc.encrypt("bacon", der_key, iv), der_key, iv), "bacon");
+
+  // Base32. Again, specify '16' to avoid it generating a 256-bit key string.
+  var b32key = Utils.derivePresentableKeyFromPassphrase(der_passphrase, der_salt, 16);
+  var hyphenated = Utils.hyphenatePassphrase(b32key);
+  do_check_true(Utils.isPassphrase(b32key));
+  
+  _("Derived key in base32: " + b32key);
+  do_check_eq(b32key.length, 26);
+  do_check_eq(hyphenated.length, 31);  // 1 char, plus 5 groups of 5, hyphenated = 5 + (5*5) + 1 = 31.
+  do_check_eq(hyphenated, "9-5wmnu-95tqc-78z2h-amkbw-izqzi");
+}
--- a/services/sync/tests/unit/test_utils_encodeBase32.js
+++ b/services/sync/tests/unit/test_utils_encodeBase32.js
@@ -1,15 +1,58 @@
 Cu.import("resource://services-sync/util.js");
 
 function run_test() {
+  // Testing byte array manipulation.
+  do_check_eq("FOOBAR", Utils.byteArrayToString([70, 79, 79, 66, 65, 82]));
+  do_check_eq("", Utils.byteArrayToString([]));
+  
+  _("Testing encoding...");
   // Test vectors from RFC 4648
   do_check_eq(Utils.encodeBase32(""), "");
   do_check_eq(Utils.encodeBase32("f"), "MY======");
   do_check_eq(Utils.encodeBase32("fo"), "MZXQ====");
   do_check_eq(Utils.encodeBase32("foo"), "MZXW6===");
   do_check_eq(Utils.encodeBase32("foob"), "MZXW6YQ=");
   do_check_eq(Utils.encodeBase32("fooba"), "MZXW6YTB");
   do_check_eq(Utils.encodeBase32("foobar"), "MZXW6YTBOI======");
 
   do_check_eq(Utils.encodeBase32("Bacon is a vegetable."),
               "IJQWG33OEBUXGIDBEB3GKZ3FORQWE3DFFY======");
+
+  _("Checking assumptions...");
+  for (let i = 0; i <= 255; ++i)
+    do_check_eq(undefined | i, i);
+
+  _("Testing decoding...");
+  do_check_eq(Utils.decodeBase32(""), "");
+  do_check_eq(Utils.decodeBase32("MY======"), "f");
+  do_check_eq(Utils.decodeBase32("MZXQ===="), "fo");
+  do_check_eq(Utils.decodeBase32("MZXW6YTB"), "fooba");
+  do_check_eq(Utils.decodeBase32("MZXW6YTBOI======"), "foobar");
+
+  // Same with incorrect or missing padding.
+  do_check_eq(Utils.decodeBase32("MZXW6YTBOI=="), "foobar");
+  do_check_eq(Utils.decodeBase32("MZXW6YTBOI"), "foobar");
+
+  let encoded = Utils.encodeBase32("Bacon is a vegetable.");
+  _("Encoded to " + JSON.stringify(encoded));
+  do_check_eq(Utils.decodeBase32(encoded), "Bacon is a vegetable.");
+
+  // Test failure.
+  let err;
+  try {
+    Utils.decodeBase32("000");
+  } catch (ex) {
+    err = ex;
+  }
+  do_check_eq(err, "Unknown character in base32: 0");
+  
+  // Testing our own variant.
+  do_check_eq(Utils.encodeKeyBase32("foobarbafoobarba"), "mzxw6ytb9jrgcztpn5rgc4tcme");
+  do_check_eq(Utils.decodeKeyBase32("mzxw6ytb9jrgcztpn5rgc4tcme"), "foobarbafoobarba");
+  do_check_eq(
+      Utils.encodeKeyBase32("\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"),
+      "aeaqcaibaeaqcaibaeaqcaibae");
+  do_check_eq(
+      Utils.decodeKeyBase32("aeaqcaibaeaqcaibaeaqcaibae"),
+      "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01");
 }
--- a/services/sync/tests/unit/test_utils_lock.js
+++ b/services/sync/tests/unit/test_utils_lock.js
@@ -1,11 +1,18 @@
 _("Make sure lock prevents calling with a shared lock");
 Cu.import("resource://services-sync/util.js");
 
+// Utility that we only use here.
+
+function do_check_begins(thing, startsWith) {
+  if (!(thing && thing.indexOf && (thing.indexOf(startsWith) == 0)))
+    do_throw(thing + " doesn't begin with " + startsWith);
+}
+
 function run_test() {
   let ret, rightThis, didCall;
   let state, lockState, lockedState, unlockState;
   let obj = {
     _lock: Utils.lock,
     lock: function() {
       lockState = ++state;
       if (this._locked) {
@@ -15,23 +22,25 @@ function run_test() {
       this._locked = true;
       return true;
     },
     unlock: function() {
       unlockState = ++state;
       this._locked = false;
     },
 
-    func: function() this._lock(function() {
+    func: function() this._lock("Test utils lock",
+    function() {
       rightThis = this == obj;
       didCall = true;
       return 5;
     })(),
 
-    throwy: function() this._lock(function() {
+    throwy: function() this._lock("Test utils lock throwy",
+    function() {
       rightThis = this == obj;
       didCall = true;
       this.throwy();
     })()
   };
 
   _("Make sure a normal call will call and return");
   rightThis = didCall = false;
@@ -47,17 +56,18 @@ function run_test() {
   _("Make sure code that calls locked code throws");
   ret = null;
   rightThis = didCall = false;
   try {
     ret = obj.throwy();
     do_throw("throwy internal call should have thrown!");
   }
   catch(ex) {
-    do_check_eq(ex, "Could not acquire lock");
+    // Should throw an Error, not a string.
+    do_check_begins(ex, "Could not acquire lock");
   }
   do_check_eq(ret, null);
   do_check_true(rightThis);
   do_check_true(didCall);
   _("Lock should be called twice so state 3 is skipped");
   do_check_eq(lockState, 4);
   do_check_eq(lockedState, 5);
   do_check_eq(unlockState, 6);
--- a/services/sync/tests/unit/test_utils_passphrase.js
+++ b/services/sync/tests/unit/test_utils_passphrase.js
@@ -1,32 +1,59 @@
 Cu.import("resource://services-sync/util.js");
 
 function run_test() {
-  _("Generated passphrase has length 20.");
+  _("Generated passphrase has length 26.");
   let pp = Utils.generatePassphrase();
-  do_check_eq(pp.length, 20);
+  do_check_eq(pp.length, 26);
+
+  const key = "abcdefghijkmnpqrstuvwxyz23456789";
+  _("Passphrase only contains [" + key + "].");
+  do_check_true(pp.split('').every(function(chr) key.indexOf(chr) != -1));
 
-  _("Passphrase only contains a-z.");
-  let bytes = [chr.charCodeAt() for each (chr in pp)];
-  do_check_true(Math.min.apply(null, bytes) >= 97);
-  do_check_true(Math.max.apply(null, bytes) <= 122);
+  _("Hyphenated passphrase has 5 hyphens.");
+  let hyphenated = Utils.hyphenatePassphrase(pp);
+  _("H: " + hyphenated);
+  do_check_eq(hyphenated.length, 31);
+  do_check_eq(hyphenated[1], '-');
+  do_check_eq(hyphenated[7], '-');
+  do_check_eq(hyphenated[13], '-');
+  do_check_eq(hyphenated[19], '-');
+  do_check_eq(hyphenated[25], '-');
+  do_check_eq(pp,
+      hyphenated.slice(0, 1) + hyphenated.slice(2, 7)
+      + hyphenated.slice(8, 13) + hyphenated.slice(14, 19)
+      + hyphenated.slice(20, 25) + hyphenated.slice(26, 31));
 
-  _("Hyphenated passphrase has 3 hyphens.");
-  let hyphenated = Utils.hyphenatePassphrase(pp);
-  do_check_eq(hyphenated.length, 23);
-  do_check_eq(hyphenated[5], '-');
-  do_check_eq(hyphenated[11], '-');
-  do_check_eq(hyphenated[17], '-');
-  do_check_eq(hyphenated.slice(0, 5) + hyphenated.slice(6, 11)
-            + hyphenated.slice(12, 17) + hyphenated.slice(18, 23), pp);
+  _("Arbitrary hyphenation.");
+  // We don't allow invalid characters for our base32 character set.
+  do_check_eq(Utils.hyphenatePassphrase("1234567"), "2-34567");  // Not partial, so no trailing dash.
+  do_check_eq(Utils.hyphenatePassphrase("1234567890"), "2-34567-89");
+  do_check_eq(Utils.hyphenatePassphrase("abcdeabcdeabcdeabcdeabcde"), "a-bcdea-bcdea-bcdea-bcdea-bcde");
+  do_check_eq(Utils.hyphenatePartialPassphrase("1234567"), "2-34567-");
+  do_check_eq(Utils.hyphenatePartialPassphrase("1234567890"), "2-34567-89");
+  do_check_eq(Utils.hyphenatePartialPassphrase("abcdeabcdeabcdeabcdeabcde"), "a-bcdea-bcdea-bcdea-bcdea-bcde");
+  
+  do_check_eq(Utils.hyphenatePartialPassphrase("a"), "a-");
+  do_check_eq(Utils.hyphenatePartialPassphrase("1234567"), "2-34567-");
+  do_check_eq(Utils.hyphenatePartialPassphrase("a-bcdef-g"),
+              "a-bcdef-g");
+  do_check_eq(Utils.hyphenatePartialPassphrase("abcdefghijklmnop"),
+              "a-bcdef-ghijk-mnp");
+  do_check_eq(Utils.hyphenatePartialPassphrase("abcdefghijklmnopabcde"),
+              "a-bcdef-ghijk-mnpab-cde");
+  do_check_eq(Utils.hyphenatePartialPassphrase("a-bcdef-ghijk-LMNOP-ABCDE-Fg"),
+              "a-bcdef-ghijk-mnpab-cdefg-");
+  // Cuts off.
+  do_check_eq(Utils.hyphenatePartialPassphrase("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").length, 31);
+        
+  
 
   _("Normalize passphrase recognizes hyphens.");
   do_check_eq(Utils.normalizePassphrase(hyphenated), pp);
-  do_check_eq(pp, pp);
 
   _("Passphrase strength calculated according to the NIST algorithm.");
   do_check_eq(Utils.passphraseStrength(""), 0);
   do_check_eq(Utils.passphraseStrength("a"), 4);
   do_check_eq(Utils.passphraseStrength("ab"), 6);
   do_check_eq(Utils.passphraseStrength("abc"), 8);
   do_check_eq(Utils.passphraseStrength("abcdefgh"), 18);
   do_check_eq(Utils.passphraseStrength("abcdefghi"), 19.5);