Bug 730989 - Refactor identity and authentication in Sync; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Thu, 22 Mar 2012 15:49:50 -0700
changeset 89920 82c4a54e0437dd4f03e887bad846e188dd665791
parent 89919 e8b6b6b810ee2914fcdb694798ad1c81955dad72
child 89921 d6b9981245d2f2e4d042f48fa0333fcfd56ef648
push id434
push usergszorc@mozilla.com
push dateThu, 22 Mar 2012 22:50:13 +0000
treeherderservices-central@82c4a54e0437 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs730989
milestone14.0a1
Bug 730989 - Refactor identity and authentication in Sync; r=rnewman
browser/base/content/sync/addDevice.js
browser/base/content/sync/genericChange.js
browser/base/content/sync/setup.js
browser/base/content/sync/utils.js
services/sync/modules/engines.js
services/sync/modules/identity.js
services/sync/modules/keys.js
services/sync/modules/main.js
services/sync/modules/policies.js
services/sync/modules/record.js
services/sync/modules/resource.js
services/sync/modules/rest.js
services/sync/modules/service.js
services/sync/modules/status.js
services/sync/modules/util.js
services/sync/tests/unit/head_helpers.js
services/sync/tests/unit/head_http_server.js
services/sync/tests/unit/test_addons_engine.js
services/sync/tests/unit/test_auth_manager.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_bookmark_record.js
services/sync/tests/unit/test_bookmark_smart_bookmarks.js
services/sync/tests/unit/test_clients_engine.js
services/sync/tests/unit/test_clients_escape.js
services/sync/tests/unit/test_collections_recovery.js
services/sync/tests/unit/test_corrupt_keys.js
services/sync/tests/unit/test_engine_abort.js
services/sync/tests/unit/test_errorhandler.js
services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
services/sync/tests/unit/test_history_engine.js
services/sync/tests/unit/test_hmac_error.js
services/sync/tests/unit/test_identity_manager.js
services/sync/tests/unit/test_interval_triggers.js
services/sync/tests/unit/test_jpakeclient.js
services/sync/tests/unit/test_keys.js
services/sync/tests/unit/test_load_modules.js
services/sync/tests/unit/test_node_reassignment.js
services/sync/tests/unit/test_records_crypto.js
services/sync/tests/unit/test_records_crypto_generateEntry.js
services/sync/tests/unit/test_resource.js
services/sync/tests/unit/test_resource_async.js
services/sync/tests/unit/test_resource_ua.js
services/sync/tests/unit/test_score_triggers.js
services/sync/tests/unit/test_sendcredentials_controller.js
services/sync/tests/unit/test_service_attributes.js
services/sync/tests/unit/test_service_changePassword.js
services/sync/tests/unit/test_service_cluster.js
services/sync/tests/unit/test_service_detect_upgrade.js
services/sync/tests/unit/test_service_getStorageInfo.js
services/sync/tests/unit/test_service_login.js
services/sync/tests/unit/test_service_passwordUTF8.js
services/sync/tests/unit/test_service_persistLogin.js
services/sync/tests/unit/test_service_startOver.js
services/sync/tests/unit/test_service_startup.js
services/sync/tests/unit/test_service_sync_401.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_syncscheduler.js
services/sync/tests/unit/test_syncstoragerequest.js
services/sync/tests/unit/test_upgrade_old_sync_key.js
services/sync/tests/unit/xpcshell.ini
services/sync/tps/extensions/tps/modules/sync.jsm
services/sync/tps/extensions/tps/modules/tps.jsm
--- a/browser/base/content/sync/addDevice.js
+++ b/browser/base/content/sync/addDevice.js
@@ -78,17 +78,17 @@ let gSyncAddDevice = {
         this.pin1.focus();
         break;
       case SYNC_KEY_PAGE:
         this.wizard.canAdvance = false;
         this.wizard.canRewind = true;
         this.wizard.getButton("back").hidden = false;
         this.wizard.getButton("next").hidden = true;
         document.getElementById("weavePassphrase").value =
-          Weave.Utils.hyphenatePassphrase(Weave.Service.passphrase);
+          Weave.Utils.hyphenatePassphrase(Weave.Identity.syncKey);
         break;
       case DEVICE_CONNECTED_PAGE:
         this.wizard.canAdvance = true;
         this.wizard.canRewind = false;
         this.wizard.getButton("cancel").hidden = true;
         break;
     }
   },
@@ -108,19 +108,19 @@ let gSyncAddDevice = {
   startTransfer: function startTransfer() {
     this.errorRow.hidden = true;
     // When onAbort is called, Weave may already be gone.
     const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
 
     let self = this;
     let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
       onPaired: function onPaired() {
-        let credentials = {account:   Weave.Service.account,
-                           password:  Weave.Service.password,
-                           synckey:   Weave.Service.passphrase,
+        let credentials = {account:   Weave.Identity.account,
+                           password:  Weave.Identity.basicPassword,
+                           synckey:   Weave.Identity.syncKey,
                            serverURL: Weave.Service.serverURL};
         jpakeclient.sendAndComplete(credentials);
       },
       onComplete: function onComplete() {
         delete self._jpakeclient;
         self.wizard.pageIndex = DEVICE_CONNECTED_PAGE;
 
         // Schedule a Sync for soonish to fetch the data uploaded by the
--- a/browser/base/content/sync/genericChange.js
+++ b/browser/base/content/sync/genericChange.js
@@ -101,17 +101,17 @@ let Change = {
           introText.textContent = this._str("new.recoverykey.introText");
           this._dialog.getButton("finish").label
             = this._str("new.recoverykey.acceptButton");
         }
         else {
           document.getElementById("generatePassphraseButton").hidden = false;
           document.getElementById("passphraseBackupButtons").hidden = false;
           this._passphraseBox.setAttribute("readonly", "true");
-          let pp = Weave.Service.passphrase;
+          let pp = Weave.Identity.syncKey;
           if (Weave.Utils.isPassphrase(pp))
              pp = Weave.Utils.hyphenatePassphrase(pp);
           this._passphraseBox.value = pp;
           this._passphraseBox.focus();
           document.title = this._str("change.recoverykey.title");
           introText.textContent = this._str("change.synckey.introText2");
           warningText.textContent = this._str("change.recoverykey.warningText");
           this._dialog.getButton("finish").label
@@ -188,17 +188,17 @@ let Change = {
     let passphrase = Weave.Utils.generatePassphrase();
     this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase);
     this._dialog.getButton("finish").disabled = false;
   },
 
   doChangePassphrase: function Change_doChangePassphrase() {
     let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
     if (this._updatingPassphrase) {
-      Weave.Service.passphrase = pp;
+      Weave.Identity.syncKey = pp;
       if (Weave.Service.login()) {
         this._updateStatus("change.recoverykey.success", "success");
         Weave.Service.persistLogin();
         Weave.SyncScheduler.delayedAutoConnect(0);
       }
       else {
         this._updateStatus("new.passphrase.status.incorrect", "error");
       }
@@ -212,17 +212,17 @@ let Change = {
         this._updateStatus("change.recoverykey.error", "error");
     }
 
     return false;
   },
 
   doChangePassword: function Change_doChangePassword() {
     if (this._currentPasswordInvalid) {
-      Weave.Service.password = this._firstBox.value;
+      Weave.Identity.basicPassword = this._firstBox.value;
       if (Weave.Service.login()) {
         this._updateStatus("change.password.status.success", "success");
         Weave.Service.persistLogin();
       }
       else {
         this._updateStatus("new.password.status.incorrect", "error");
       }
     }
--- a/browser/base/content/sync/setup.js
+++ b/browser/base/content/sync/setup.js
@@ -174,23 +174,23 @@ var gSyncSetup = {
     } else {
       this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE;
     }
   },
 
   resetPassphrase: function resetPassphrase() {
     // Apply the existing form fields so that
     // Weave.Service.changePassphrase() has the necessary credentials.
-    Weave.Service.account = document.getElementById("existingAccountName").value;
-    Weave.Service.password = document.getElementById("existingPassword").value;
+    Weave.Identity.account = document.getElementById("existingAccountName").value;
+    Weave.Identity.basicPassword = document.getElementById("existingPassword").value;
 
     // Generate a new passphrase so that Weave.Service.login() will
     // actually do something.
     let passphrase = Weave.Utils.generatePassphrase();
-    Weave.Service.passphrase = passphrase;
+    Weave.Identity.syncKey = passphrase;
 
     // Only open the dialog if username + password are actually correct.
     Weave.Service.login();
     if ([Weave.LOGIN_FAILED_INVALID_PASSPHRASE,
          Weave.LOGIN_FAILED_NO_PASSPHRASE,
          Weave.LOGIN_SUCCEEDED].indexOf(Weave.Status.login) == -1) {
       return;
     }
@@ -204,37 +204,37 @@ var gSyncSetup = {
     // changePassphrase() will sync, make sure we set the "firstSync" pref
     // according to the user's pref.
     Weave.Svc.Prefs.reset("firstSync");
     this.setupInitialSync();
     gSyncUtils.resetPassphrase(true);
   },
 
   onResetPassphrase: function () {
-    document.getElementById("existingPassphrase").value = 
-      Weave.Utils.hyphenatePassphrase(Weave.Service.passphrase);
+    document.getElementById("existingPassphrase").value =
+      Weave.Utils.hyphenatePassphrase(Weave.Identity.syncKey);
     this.checkFields();
     this.wizard.advance();
   },
 
   onLoginStart: function () {
     this.toggleLoginFeedback(false);
   },
 
   onLoginEnd: function () {
     this.toggleLoginFeedback(true);
   },
 
   sendCredentialsAfterSync: function () {
     let send = function() {
       Services.obs.removeObserver("weave:service:sync:finish", send);
       Services.obs.removeObserver("weave:service:sync:error", send);
-      let credentials = {account:   Weave.Service.account,
-                         password:  Weave.Service.password,
-                         synckey:   Weave.Service.passphrase,
+      let credentials = {account:   Weave.Identity.account,
+                         password:  Weave.Identity.basicPassword,
+                         synckey:   Weave.Identity.syncKey,
                          serverURL: Weave.Service.serverURL};
       this._jpakeclient.sendAndComplete(credentials);
     }.bind(this);
     Services.obs.addObserver("weave:service:sync:finish", send, false);
     Services.obs.addObserver("weave:service:sync:error", send, false);
   },
 
   toggleLoginFeedback: function (stop) {
@@ -363,17 +363,17 @@ var gSyncSetup = {
         else
           str = availCheck;
       }
     }
 
     this._setFeedbackMessage(feedback, valid, str);
     this.status.email = valid;
     if (valid)
-      Weave.Service.account = value;
+      Weave.Identity.account = value;
     this.checkFields();
   },
 
   onPasswordChange: function () {
     let password = document.getElementById("weavePassword");
     let pwconfirm = document.getElementById("weavePasswordConfirm");
     let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm);
 
@@ -499,34 +499,35 @@ var gSyncSetup = {
           document.getElementById("weaveEmail").value);
         let challenge = getField("challenge");
         let response = getField("response");
 
         let error = Weave.Service.createAccount(email, password,
                                                 challenge, response);
 
         if (error == null) {
-          Weave.Service.account = email;
-          Weave.Service.password = password;
-          Weave.Service.passphrase = Weave.Utils.generatePassphrase();
+          Weave.Identity.account = email;
+          Weave.Identity.basicPassword = password;
+          Weave.Identity.syncKey = Weave.Utils.generatePassphrase();
           this._handleNoScript(false);
           Weave.Svc.Prefs.set("firstSync", "newAccount");
           this.wizardFinish();
           return false;
         }
 
         image.setAttribute("status", "error");
         label.value = Weave.Utils.getErrorString(error);
         return false;
       case EXISTING_ACCOUNT_LOGIN_PAGE:
-        Weave.Service.account = Weave.Utils.normalizeAccount(
+        Weave.Identity.account = Weave.Utils.normalizeAccount(
           document.getElementById("existingAccountName").value);
-        Weave.Service.password = document.getElementById("existingPassword").value;
+        Weave.Identity.basicPssword =
+          document.getElementById("existingPassword").value;
         let pp = document.getElementById("existingPassphrase").value;
-        Weave.Service.passphrase = Weave.Utils.normalizePassphrase(pp);
+        Weave.Identity.syncKey = Weave.Utils.normalizePassphrase(pp);
         if (Weave.Service.login()) {
           this.wizardFinish();
         }
         return false;
       case OPTIONS_PAGE:
         let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
         // No confirmation needed on new account setup or merge option
         // with existing account.
@@ -695,19 +696,19 @@ var gSyncSetup = {
         document.getElementById("easySetupPIN1").value = pin.slice(0, 4);
         document.getElementById("easySetupPIN2").value = pin.slice(4, 8);
         document.getElementById("easySetupPIN3").value = pin.slice(8);
       },
 
       onPairingStart: function onPairingStart() {},
 
       onComplete: function onComplete(credentials) {
-        Weave.Service.account = credentials.account;
-        Weave.Service.password = credentials.password;
-        Weave.Service.passphrase = credentials.synckey;
+        Weave.Identity.account = credentials.account;
+        Weave.Identity.basicPassword = credentials.password;
+        Weave.Identity.syncKey = credentials.synckey;
         Weave.Service.serverURL = credentials.serverURL;
         gSyncSetup.wizardFinish();
       },
 
       onAbort: function onAbort(error) {
         delete self._jpakeclient;
 
         // Ignore if wizard is aborted.
--- a/browser/base/content/sync/utils.js
+++ b/browser/base/content/sync/utils.js
@@ -222,23 +222,23 @@ let gSyncUtils = {
   validatePassword: function (el1, el2) {
     let valid = false;
     let val1 = el1.value;
     let val2 = el2 ? el2.value : "";
     let error = "";
 
     if (!el2)
       valid = val1.length >= Weave.MIN_PASS_LENGTH;
-    else if (val1 && val1 == Weave.Service.username)
+    else if (val1 && val1 == Weave.Identity.username)
       error = "change.password.pwSameAsUsername";
-    else if (val1 && val1 == Weave.Service.account)
+    else if (val1 && val1 == Weave.Identity.account)
       error = "change.password.pwSameAsEmail";
-    else if (val1 && val1 == Weave.Service.password)
+    else if (val1 && val1 == Weave.Identity.basicPassword)
       error = "change.password.pwSameAsPassword";
-    else if (val1 && val1 == Weave.Service.passphrase)
+    else if (val1 && val1 == Weave.Identity.syncKey)
       error = "change.password.pwSameAsRecoveryKey";
     else if (val1 && val2) {
       if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH)
         valid = true;
       else if (val1.length < Weave.MIN_PASS_LENGTH)
         error = "change.password.tooShort";
       else if (val1 != val2)
         error = "change.password.mismatch";
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -568,17 +568,17 @@ SyncEngine.prototype = {
   // URI length limitations.
   guidFetchBatchSize: DEFAULT_GUID_FETCH_BATCH_SIZE,
   mobileGUIDFetchBatchSize: DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE,
   
   // How many records to process in a single batch.
   applyIncomingBatchSize: DEFAULT_STORE_BATCH_SIZE,
 
   get storageURL() Svc.Prefs.get("clusterURL") + SYNC_API_VERSION +
-    "/" + ID.get("WeaveID").username + "/storage/",
+    "/" + Identity.username + "/storage/",
 
   get engineURL() this.storageURL + this.name,
 
   get cryptoKeysURL() this.storageURL + "crypto/keys",
 
   get metaURL() this.storageURL + "meta/global",
 
   get syncID() {
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -1,143 +1,491 @@
-/* ***** 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 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>
- *
- * 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 ***** */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const EXPORTED_SYMBOLS = ['Identity', 'ID'];
+"use strict";
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const EXPORTED_SYMBOLS = ["Identity", "IdentityManager"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/keys.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;
-});
-
-XPCOMUtils.defineLazyGetter(this, "ID", function () {
-  return new IDManager();
+XPCOMUtils.defineLazyGetter(this, "Identity", function() {
+  return new IdentityManager();
 });
 
-// For storing identities we'll use throughout Weave
-function IDManager() {
-  this._ids = {};
-}
-IDManager.prototype = {
-  get: function IDMgr_get(name) {
-    if (name in this._ids)
-      return this._ids[name];
-    return null;
-  },
-  set: function IDMgr_set(name, id) {
-    this._ids[name] = id;
-    return id;
-  },
-  del: function IDMgr_del(name) {
-    delete this._ids[name];
-  }
-};
+/**
+ * Manages identity and authentication for Sync.
+ *
+ * The following entities are managed:
+ *
+ *   account - The main Sync/services account. This is typically an email
+ *     address.
+ *   username - A normalized version of your account. This is what's
+ *     transmitted to the server.
+ *   basic password - UTF-8 password used for authenticating when using HTTP
+ *     basic authentication.
+ *   sync key - The main encryption key used by Sync.
+ *   sync key bundle - A representation of your sync key.
+ *
+ * An instance of this type is lazily instantiated under Weave.Identity. It is
+ * and should be treated as a global variable. The reason is that saved changes
+ * are stored in preferences and the password manager. So, if you created
+ * multiple instances, they would just step on each other's state.
+ *
+ * When changes are made to entities that are stored in the password manager
+ * (basic password, sync key), those changes are merely staged. To commit them
+ * to the password manager, you'll need to call persistCredentials().
+ *
+ * This type also manages authenticating Sync's network requests. Sync's
+ * network code calls into getRESTRequestAuthenticator and
+ * getResourceAuthenticator (depending on the network layer being used). Each
+ * returns a function which can be used to add authentication information to an
+ * outgoing request.
+ *
+ * In theory, this type supports arbitrary identity and authentication
+ * mechanisms. You can add support for them by monkeypatching the global
+ * instance of this type. Specifically, you'll need to redefine the
+ * aforementioned network code functions to do whatever your authentication
+ * mechanism needs them to do. In addition, you may wish to install custom
+ * functions to support your API. Although, that is certainly not required.
+ * If you do monkeypatch, please be advised that Sync expects the core
+ * attributes to have values. You will need to carry at least account and
+ * username forward. If you do not wish to support one of the built-in
+ * authentication mechanisms, you'll probably want to redefine currentAuthState
+ * and any other function that involves the built-in functionality.
+ */
+function IdentityManager() {
+  this._log = Log4Moz.repository.getLogger("Sync.Identity");
+  this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.identity")];
 
-/*
- * Identity
- * These objects hold a realm, username, and password
- * They can hold a password in memory, but will try to fetch it from
- * the password manager if it's not set.
- * FIXME: need to rethink this stuff as part of a bigger identity mgmt framework
- */
+  this._basicPassword = null;
+  this._basicPasswordAllowLookup = true;
+  this._basicPasswordUpdated = false;
+  this._syncKey = null;
+  this._syncKeyAllowLookup = true;
+  this._syncKeySet = false;
+  this._syncKeyBundle = null;
+}
+IdentityManager.prototype = {
+  _log: null,
+
+  _basicPassword: null,
+  _basicPasswordAllowLookup: true,
+  _basicPasswordUpdated: false,
+
+  _syncKey: null,
+  _syncKeyAllowLookup: true,
+  _syncKeySet: false,
+
+  _syncKeyBundle: null,
+
+  get account() {
+    return Svc.Prefs.get("account", this.username);
+  },
 
-function Identity(realm, username, password) {
-  this.realm = realm;
-  this.username = username;
-  this._password = password;
-  if (password)
-    this._passwordUTF8 = Utils.encodeUTF8(password);
-}
-Identity.prototype = {
-  get password() {
-    // Look up the password then cache it
-    if (this._password == null)
-      for each (let login in this._logins)
-        if (login.username.toLowerCase() == this.username) {
-          this._password = login.password;
-          this._passwordUTF8 = Utils.encodeUTF8(login.password);
-        }
-    return this._password;
+  /**
+   * Sets the active account name.
+   *
+   * This should almost always be called in favor of setting username, as
+   * username is derived from account.
+   *
+   * Changing the account name has the side-effect of wiping out stored
+   * credentials. Keep in mind that persistCredentials() will need to be called
+   * to flush the changes to disk.
+   *
+   * Set this value to null to clear out identity information.
+   */
+  set account(value) {
+    if (value) {
+      value = value.toLowerCase();
+      Svc.Prefs.set("account", value);
+    } else {
+      Svc.Prefs.reset("account");
+    }
+
+    this.username = this.usernameFromAccount(value);
+  },
+
+  get username() {
+    return Svc.Prefs.get("username", null);
   },
 
-  set password(value) {
-    this._password = value;
-    this._passwordUTF8 = Utils.encodeUTF8(value);
+  /**
+   * Set the username value.
+   *
+   * Changing the username has the side-effect of wiping credentials.
+   */
+  set username(value) {
+    if (value) {
+      value = value.toLowerCase();
+
+      if (value == this.username) {
+        return;
+      }
+
+      Svc.Prefs.set("username", value);
+    } else {
+      Svc.Prefs.reset("username");
+    }
+
+    // If we change the username, we interpret this as a major change event
+    // and wipe out the credentials.
+    this._log.info("Username changed. Removing stored credentials.");
+    this.basicPassword = null;
+    this.syncKey = null;
+    // syncKeyBundle cleared as a result of setting syncKey.
   },
 
-  get passwordUTF8() {
-    if (!this._passwordUTF8)
-      this.password; // invoke password getter
-    return this._passwordUTF8;
+  /**
+   * Obtains the HTTP Basic auth password.
+   *
+   * Returns a string if set or null if it is not set.
+   */
+  get basicPassword() {
+    if (this._basicPasswordAllowLookup) {
+      // We need a username to find the credentials.
+      let username = this.username;
+      if (!username) {
+        return null;
+      }
+
+      for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
+        if (login.username.toLowerCase() == username) {
+          // It should already be UTF-8 encoded, but we don't take any chances.
+          this._basicPassword = Utils.encodeUTF8(login.password);
+        }
+      }
+
+      this._basicPasswordAllowLookup = false;
+    }
+
+    return this._basicPassword;
   },
 
-  persist: function persist() {
-    // Clean up any existing passwords unless it's what we're persisting
-    let exists = false;
-    for each (let login in this._logins) {
-      if (login.username == this.username && login.password == this._password)
-        exists = true;
-      else
-        Services.logins.removeLogin(login);
+  /**
+   * Set the HTTP basic password to use.
+   *
+   * Changes will not persist unless persistSyncCredentials() is called.
+   */
+  set basicPassword(value) {
+    // Wiping out value.
+    if (!value) {
+      this._log.info("Basic password has no value. Removing.");
+      this._basicPassword = null;
+      this._basicPasswordUpdated = true;
+      this._basicPasswordAllowLookup = false;
+      return;
+    }
+
+    let username = this.username;
+    if (!username) {
+      throw new Error("basicPassword cannot be set before username.");
     }
 
-    // No need to create the login after clearing out the other ones
-    let log = Log4Moz.repository.getLogger("Sync.Identity");
-    if (exists) {
-      log.trace("Skipping persist: " + this.realm + " for " + this.username);
+    this._log.info("Basic password being updated.");
+    this._basicPassword = Utils.encodeUTF8(value);
+    this._basicPasswordUpdated = true;
+  },
+
+  /**
+   * Obtain the Sync Key.
+   *
+   * This returns a 26 character "friendly" Base32 encoded string on success or
+   * null if no Sync Key could be found.
+   *
+   * If the Sync Key hasn't been set in this session, this will look in the
+   * password manager for the sync key.
+   */
+  get syncKey() {
+    if (this._syncKeyAllowLookup) {
+      let username = this.username;
+      if (!username) {
+        return null;
+      }
+
+      for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
+        if (login.username.toLowerCase() == username) {
+          this._syncKey = login.password;
+        }
+      }
+
+      this._syncKeyAllowLookup = false;
+    }
+
+    return this._syncKey;
+  },
+
+  /**
+   * Set the active Sync Key.
+   *
+   * If being set to null, the Sync Key and its derived SyncKeyBundle are
+   * removed. However, the Sync Key won't be deleted from the password manager
+   * until persistSyncCredentials() is called.
+   *
+   * If a value is provided, it should be a 26 or 32 character "friendly"
+   * Base32 string for which Utils.isPassphrase() returns true.
+   *
+   * A side-effect of setting the Sync Key is that a SyncKeyBundle is
+   * generated. For historical reasons, this will silently error out if the
+   * value is not a proper Sync Key (!Utils.isPassphrase()). This should be
+   * fixed in the future (once service.js is more sane) to throw if the passed
+   * value is not valid.
+   */
+  set syncKey(value) {
+    if (!value) {
+      this._log.info("Sync Key has no value. Deleting.");
+      this._syncKey = null;
+      this._syncKeyBundle = null;
+      this._syncKeyUpdated = true;
       return;
     }
 
-    // Add the new username/password
-    log.trace("Persisting " + this.realm + " for " + this.username);
-    let nsLoginInfo = new Components.Constructor(
-      "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
-    let newLogin = new nsLoginInfo(PWDMGR_HOST, null, this.realm,
-      this.username, this.password, "", "");
-    Services.logins.addLogin(newLogin);
+    if (!this.username) {
+      throw new Error("syncKey cannot be set before username.");
+    }
+
+    this._log.info("Sync Key being updated.");
+    this._syncKey = value;
+
+    // Calling the getter has the side-effect of populating the object, which
+    // we desire.
+    let bundle = this.syncKeyBundle;
+
+    this._syncKeyUpdated = true;
+  },
+
+  /**
+   * Obtain the active SyncKeyBundle.
+   *
+   * This returns a SyncKeyBundle representing a key pair derived from the
+   * Sync Key on success. If no Sync Key is present or if the Sync Key is not
+   * valid, this returns null.
+   *
+   * The SyncKeyBundle should be treated as immutable.
+   */
+  get syncKeyBundle() {
+    // We can't obtain a bundle without a username set.
+    if (!this.username) {
+      this._log.warn("Attempted to obtain Sync Key Bundle with no username set!");
+      return null;
+    }
+
+    if (!this.syncKey) {
+      this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " +
+                     "set!");
+      return null;
+    }
+
+    if (!this._syncKeyBundle) {
+      try {
+        this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey);
+      } catch (ex) {
+        this._log.warn(Utils.exceptionStr(ex));
+        return null;
+      }
+    }
+
+    return this._syncKeyBundle;
+  },
+
+  /**
+   * The current state of the auth credentials.
+   *
+   * This essentially validates that enough credentials are available to use
+   * Sync.
+   */
+  get currentAuthState() {
+    if (!this.username) {
+      return LOGIN_FAILED_NO_USERNAME;
+    }
+
+    if (Utils.mpLocked()) {
+      return STATUS_OK;
+    }
+
+    if (!this.basicPassword) {
+      return LOGIN_FAILED_NO_PASSWORD;
+    }
+
+    if (!this.syncKey) {
+      return LOGIN_FAILED_NO_PASSPHRASE;
+    }
+
+    return STATUS_OK;
+  },
+
+  /**
+   * Persist credentials to password store.
+   *
+   * When credentials are updated, they are changed in memory only. This will
+   * need to be called to save them to the underlying password store.
+   *
+   * If the password store is locked (e.g. if the master password hasn't been
+   * entered), this could throw an exception.
+   */
+  persistCredentials: function persistCredentials() {
+    if (this._basicPasswordUpdated) {
+      if (this._basicPassword) {
+        this._setLogin(PWDMGR_PASSWORD_REALM, this.username,
+                       this._basicPassword);
+      } else {
+        for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
+          Services.logins.removeLogin(login);
+        }
+      }
+
+      this._basicPasswordUpdated = false;
+    }
+
+    if (this._syncKeyUpdated) {
+      if (this._syncKey) {
+        this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey);
+      } else {
+        for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
+          Services.logins.removeLogin(login);
+        }
+      }
+
+      this._syncKeyUpdated = false;
+    }
+
+  },
+
+  /**
+   * Deletes the Sync Key from the system.
+   */
+  deleteSyncKey: function deleteSyncKey() {
+    this.syncKey = null;
+    this.persistCredentials();
   },
 
-  get _logins() Services.logins.findLogins({}, PWDMGR_HOST, null, this.realm)
+  hasBasicCredentials: function hasBasicCredentials() {
+    // Because JavaScript.
+    return this.username && this.basicPassword && true;
+  },
+
+  /**
+   * Obtains the array of basic logins from nsiPasswordManager.
+   */
+  _getLogins: function _getLogins(realm) {
+    return Services.logins.findLogins({}, PWDMGR_HOST, null, realm);
+  },
+
+  /**
+   * Set a login in the password manager.
+   *
+   * This has the side-effect of deleting any other logins for the specified
+   * realm.
+   */
+  _setLogin: function _setLogin(realm, username, password) {
+    let exists = false;
+    for each (let login in this._getLogins(realm)) {
+      if (login.username == username && login.password == password) {
+        exists = true;
+      } else {
+        this._log.debug("Pruning old login for " + username + " from " + realm);
+        Services.logins.removeLogin(login);
+      }
+    }
+
+    if (exists) {
+      return;
+    }
+
+    this._log.debug("Updating saved password for " + username + " in " +
+                    realm);
+
+    let loginInfo = new Components.Constructor(
+      "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
+    let login = new loginInfo(PWDMGR_HOST, null, realm, username,
+                                password, "", "");
+    Services.logins.addLogin(login);
+  },
+
+  /**
+   * Deletes Sync credentials from the password manager.
+   */
+  deleteSyncCredentials: function deleteSyncCredentials() {
+    let logins = Services.logins.findLogins({}, PWDMGR_HOST, "", "");
+    for each (let login in logins) {
+      Services.logins.removeLogin(login);
+    }
+
+    // Wait until after store is updated in case it fails.
+    this._basicPassword = null;
+    this._basicPasswordAllowLookup = true;
+    this._basicPasswordUpdated = false;
+
+    this._syncKey = null;
+    // this._syncKeyBundle is nullified as part of _syncKey setter.
+    this._syncKeyAllowLookup = true;
+    this._syncKeyUpdated = false;
+  },
+
+  usernameFromAccount: function usernameFromAccount(value) {
+    // If we encounter characters not allowed by the API (as found for
+    // instance in an email address), hash the value.
+    if (value && value.match(/[^A-Z0-9._-]/i)) {
+      return Utils.sha1Base32(value.toLowerCase()).toLowerCase();
+    }
+
+    return value ? value.toLowerCase() : value;
+  },
+
+  /**
+   * Obtain a function to be used for adding auth to Resource HTTP requests.
+   */
+  getResourceAuthenticator: function getResourceAuthenticator() {
+    if (this.hasBasicCredentials()) {
+      return this._onResourceRequestBasic.bind(this);
+    }
+
+    return null;
+  },
+
+  /**
+   * Helper method to return an authenticator for basic Resource requests.
+   */
+  getBasicResourceAuthenticator:
+    function getBasicResourceAuthenticator(username, password) {
+
+    return function basicAuthenticator(resource) {
+      let value = "Basic " + btoa(username + ":" + password);
+      return {headers: {authorization: value}};
+    };
+  },
+
+  _onResourceRequestBasic: function _onResourceRequestBasic(resource) {
+    let value = "Basic " + btoa(this.username + ":" + this.basicPassword);
+    return {headers: {authorization: value}};
+  },
+
+  _onResourceRequestMAC: function _onResourceRequestMAC(resource, method) {
+    // TODO Get identifier and key from somewhere.
+    let identifier;
+    let key;
+    let result = Utils.computeHTTPMACSHA1(identifier, key, method, resource.uri);
+
+    return {headers: {authorization: result.header}};
+  },
+
+  /**
+   * Obtain a function to be used for adding auth to RESTRequest instances.
+   */
+  getRESTRequestAuthenticator: function getRESTRequestAuthenticator() {
+    if (this.hasBasicCredentials()) {
+      return this.onRESTRequestBasic.bind(this);
+    }
+
+    return null;
+  },
+
+  onRESTRequestBasic: function onRESTRequestBasic(request) {
+    let up = this.username + ":" + this.basicPassword;
+    request.setHeader("authorization", "Basic " + btoa(up));
+  }
 };
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/keys.js
@@ -0,0 +1,214 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+  "BulkKeyBundle",
+  "SyncKeyBundle"
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-sync/util.js");
+
+/**
+ * Represents a pair of keys.
+ *
+ * Each key stored in a key bundle is 256 bits. One key is used for symmetric
+ * encryption. The other is used for HMAC.
+ *
+ * A KeyBundle by itself is just an anonymous pair of keys. Other types
+ * deriving from this one add semantics, such as associated collections or
+ * generating a key bundle via HKDF from another key.
+ */
+function KeyBundle() {
+  this._encrypt = null;
+  this._encryptB64 = null;
+  this._hmac = null;
+  this._hmacB64 = null;
+  this._hmacObj = null;
+  this._sha256HMACHasher = null;
+}
+KeyBundle.prototype = {
+  _encrypt: null,
+  _encryptB64: null,
+  _hmac: null,
+  _hmacB64: null,
+  _hmacObj: null,
+  _sha256HMACHasher: null,
+
+  equals: function equals(bundle) {
+    return bundle &&
+           (bundle.hmacKey == this.hmacKey) &&
+           (bundle.encryptionKey == this.encryptionKey);
+  },
+
+  /*
+   * Accessors for the two keys.
+   */
+  get encryptionKey() {
+    return this._encrypt;
+  },
+
+  set encryptionKey(value) {
+    if (!value || typeof value != "string") {
+      throw new Error("Encryption key can only be set to string values.");
+    }
+
+    if (value.length < 16) {
+      throw new Error("Encryption key must be at least 128 bits long.");
+    }
+
+    this._encrypt = value;
+    this._encryptB64 = btoa(value);
+  },
+
+  get encryptionKeyB64() {
+    return this._encryptB64;
+  },
+
+  get hmacKey() {
+    return this._hmac;
+  },
+
+  set hmacKey(value) {
+    if (!value || typeof value != "string") {
+      throw new Error("HMAC key can only be set to string values.");
+    }
+
+    if (value.length < 16) {
+      throw new Error("HMAC key must be at least 128 bits long.");
+    }
+
+    this._hmac = value;
+    this._hmacB64 = btoa(value);
+    this._hmacObj = value ? Utils.makeHMACKey(value) : null;
+    this._sha256HMACHasher = value ? Utils.makeHMACHasher(
+      Ci.nsICryptoHMAC.SHA256, this._hmacObj) : null;
+  },
+
+  get hmacKeyB64() {
+    return this._hmacB64;
+  },
+
+  get hmacKeyObject() {
+    return this._hmacObj;
+  },
+
+  get sha256HMACHasher() {
+    return this._sha256HMACHasher;
+  },
+
+  /**
+   * Populate this key pair with 2 new, randomly generated keys.
+   */
+  generateRandom: function generateRandom() {
+    let generatedHMAC = Svc.Crypto.generateRandomKey();
+    let generatedEncr = Svc.Crypto.generateRandomKey();
+    this.keyPairB64 = [generatedEncr, generatedHMAC];
+  },
+
+};
+
+/**
+ * Represents a KeyBundle associated with a collection.
+ *
+ * This is just a KeyBundle with a collection attached.
+ */
+function BulkKeyBundle(collection) {
+  let log = Log4Moz.repository.getLogger("Sync.BulkKeyBundle");
+  log.info("BulkKeyBundle being created for " + collection);
+  KeyBundle.call(this);
+
+  this._collection = collection;
+}
+
+BulkKeyBundle.prototype = {
+  __proto__: KeyBundle.prototype,
+
+  get collection() {
+    return this._collection;
+  },
+
+  /**
+   * Obtain the key pair in this key bundle.
+   *
+   * The returned keys are represented as raw byte strings.
+   */
+  get keyPair() {
+    return [this.encryptionKey, this.hmacKey];
+  },
+
+  set keyPair(value) {
+    if (!Array.isArray(value) || value.length != 2) {
+      throw new Error("BulkKeyBundle.keyPair value must be array of 2 keys.");
+    }
+
+    this.encryptionKey = value[0];
+    this.hmacKey       = value[1];
+  },
+
+  get keyPairB64() {
+    return [this.encryptionKeyB64, this.hmacKeyB64];
+  },
+
+  set keyPairB64(value) {
+    if (!Array.isArray(value) || value.length != 2) {
+      throw new Error("BulkKeyBundle.keyPairB64 value must be an array of 2 " +
+                      "keys.");
+    }
+
+    this.encryptionKey  = Utils.safeAtoB(value[0]);
+    this.hmacKey        = Utils.safeAtoB(value[1]);
+  },
+};
+
+/**
+ * Represents a key pair derived from a Sync Key via HKDF.
+ *
+ * Instances of this type should be considered immutable. You create an
+ * instance by specifying the username and 26 character "friendly" Base32
+ * encoded Sync Key. The Sync Key is derived at instance creation time.
+ *
+ * If the username or Sync Key is invalid, an Error will be thrown.
+ */
+function SyncKeyBundle(username, syncKey) {
+  let log = Log4Moz.repository.getLogger("Sync.SyncKeyBundle");
+  log.info("SyncKeyBundle being created.");
+  KeyBundle.call(this);
+
+  this.generateFromKey(username, syncKey);
+}
+SyncKeyBundle.prototype = {
+  __proto__: KeyBundle.prototype,
+
+  /*
+   * If we've got a string, hash it into keys and store them.
+   */
+  generateFromKey: function generateFromKey(username, syncKey) {
+    if (!username || (typeof username != "string")) {
+      throw new Error("Sync Key cannot be generated from non-string username.");
+    }
+
+    if (!syncKey || (typeof syncKey != "string")) {
+      throw new Error("Sync Key cannot be generated from non-string key.");
+    }
+
+    if (!Utils.isPassphrase(syncKey)) {
+      throw new Error("Provided key is not a passphrase, cannot derive Sync " +
+                      "Key Bundle.");
+    }
+
+    // Expand the base32 Sync Key to an AES 256 and 256 bit HMAC key.
+    let prk = Utils.decodeKeyBase32(syncKey);
+    let info = HMAC_INPUT + username;
+    let okm = Utils.hkdfExpand(prk, info, 32 * 2);
+    this.encryptionKey = okm.slice(0, 32);
+    this.hmacKey = okm.slice(32, 64);
+  },
+};
+
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -35,34 +35,34 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ['Weave'];
 
 let Weave = {};
 Components.utils.import("resource://services-sync/constants.js", Weave);
 let lazies = {
-  "record.js":            ["CollectionKeys", "BulkKeyBundle", "SyncKeyBundle"],
+  "record.js":            ["CollectionKeys"],
   "engines.js":           ['Engines', 'Engine', 'SyncEngine', 'Store'],
   "engines/addons.js":    ["AddonsEngine"],
   "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"],
   "engines/apps.js":      ["AppsEngine"],
-  "identity.js":          ["Identity", "ID"],
+  "identity.js":          ["Identity"],
   "jpakeclient.js":       ["JPAKEClient"],
+  "keys.js":              ["BulkKeyBundle", "SyncKeyBundle"],
   "notifications.js":     ["Notifications", "Notification", "NotificationButton"],
   "policies.js":          ["SyncScheduler", "ErrorHandler",
                            "SendCredentialsController"],
-  "resource.js":          ["Resource", "AsyncResource", "Auth",
-                           "BasicAuthenticator", "NoOpAuthenticator"],
+  "resource.js":          ["Resource", "AsyncResource"],
   "service.js":           ["Service"],
   "status.js":            ["Status"],
   "util.js":              ['Utils', 'Svc', 'Str']
 };
 
 function lazyImport(module, dest, props) {
   function getter(prop) function() {
     let ns = {};
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -415,17 +415,19 @@ let SyncScheduler = {
   },
 
 
   /**
    * Incorporates the backoff/retry logic used in error handling and elective
    * non-syncing.
    */
   scheduleAtInterval: function scheduleAtInterval(minimumInterval) {
-    let interval = Utils.calculateBackoff(this._syncErrors, MINIMUM_BACKOFF_INTERVAL);
+    let interval = Utils.calculateBackoff(this._syncErrors,
+                                          MINIMUM_BACKOFF_INTERVAL,
+                                          Status.backoffInterval);
     if (minimumInterval) {
       interval = Math.max(minimumInterval, interval);
     }
 
     this._log.debug("Starting client-initiated backoff. Next sync in " +
                     interval + " ms.");
     this.scheduleNextSync(interval);
   },
@@ -902,19 +904,19 @@ SendCredentialsController.prototype = {
         // This will call onAbort which will call unload().
         this.jpakeclient.abort();
         break;
     }
   },
 
   sendCredentials: function sendCredentials() {
     this._log.trace("Sending credentials.");
-    let credentials = {account:   Weave.Service.account,
-                       password:  Weave.Service.password,
-                       synckey:   Weave.Service.passphrase,
+    let credentials = {account:   Weave.Identity.account,
+                       password:  Weave.Identity.basicPassword,
+                       synckey:   Weave.Identity.syncKey,
                        serverURL: Weave.Service.serverURL};
     this.jpakeclient.sendAndComplete(credentials);
   },
 
   // JPAKEClient controller API
 
   onComplete: function onComplete() {
     this._log.debug("Exchange was completed successfully!");
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -33,29 +33,29 @@
  * 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 = ["WBORecord", "RecordManager", "Records",
-                          "CryptoWrapper", "CollectionKeys", "BulkKeyBundle",
-                          "SyncKeyBundle", "Collection"];
+                          "CryptoWrapper", "CollectionKeys", "Collection"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 const CRYPTO_COLLECTION = "crypto";
 const KEYS_WBO = "keys";
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 function WBORecord(collection, id) {
   this.data = {};
   this.payload = {};
   this.collection = collection;      // Optional.
@@ -193,63 +193,66 @@ function CryptoWrapper(collection, id) {
   this.id = id;
 }
 CryptoWrapper.prototype = {
   __proto__: WBORecord.prototype,
   _logName: "Sync.Record.CryptoWrapper",
 
   ciphertextHMAC: function ciphertextHMAC(keyBundle) {
     let hasher = keyBundle.sha256HMACHasher;
-    if (!hasher)
+    if (!hasher) {
       throw "Cannot compute HMAC without an HMAC key.";
+    }
 
     return Utils.bytesAsHex(Utils.digestUTF8(this.ciphertext, hasher));
   },
 
   /*
    * 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) {
     keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
-    if (!keyBundle)
+    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),
-                                         keyBundle.encryptionKey, this.IV);
+                                         keyBundle.encryptionKeyB64, this.IV);
     this.hmac = this.ciphertextHMAC(keyBundle);
     this.cleartext = null;
   },
 
   // Optional key bundle.
   decrypt: function decrypt(keyBundle) {
     if (!this.ciphertext) {
       throw "No ciphertext: nothing to decrypt?";
     }
 
     keyBundle = keyBundle || CollectionKeys.keyForCollection(this.collection);
-    if (!keyBundle)
+    if (!keyBundle) {
       throw new Error("Key bundle is null for " + this.collection + "/" + this.id);
+    }
 
     // Authenticate the encrypted blob with the expected HMAC
     let computedHMAC = this.ciphertextHMAC(keyBundle);
 
     if (computedHMAC != this.hmac) {
       Utils.throwHMACMismatch(this.hmac, computedHMAC);
     }
 
     // Handle invalid data here. Elsewhere we assume that cleartext is an object.
     let cleartext = Svc.Crypto.decrypt(this.ciphertext,
-                                       keyBundle.encryptionKey, this.IV);
+                                       keyBundle.encryptionKeyB64, this.IV);
     let json_result = JSON.parse(cleartext);
 
     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.";
     }
@@ -288,18 +291,17 @@ Utils.deferGetSet(CryptoWrapper, "payloa
 Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted");
 
 XPCOMUtils.defineLazyGetter(this, "CollectionKeys", function () {
   return new CollectionKeyManager();
 });
 
 
 /**
- * Keeps track of mappings between collection names ('tabs') and
- * keyStrs, which you can feed into KeyBundle to get encryption tokens.
+ * Keeps track of mappings between collection names ('tabs') and KeyBundles.
  *
  * You can update this thing simply by giving it /info/collections. It'll
  * use the last modified time to bring itself up to date.
  */
 function CollectionKeyManager() {
   this.lastModified = 0;
   this._collections = {};
   this._default = null;
@@ -360,20 +362,20 @@ CollectionKeyManager.prototype = {
    * If `collections` (an array of strings) is provided, iterate
    * over it and generate random keys for each collection.
    * Create a WBO for the given data.
    */
   _makeWBO: function(collections, defaultBundle) {
     let wbo = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
     let c = {};
     for (let k in collections) {
-      c[k] = collections[k].keyPair;
+      c[k] = collections[k].keyPairB64;
     }
     wbo.cleartext = {
-      "default":     defaultBundle ? defaultBundle.keyPair : null,
+      "default":     defaultBundle ? defaultBundle.keyPairB64 : null,
       "collections": c,
       "collection":  CRYPTO_COLLECTION,
       "id":          KEYS_WBO
     };
     return wbo;
   },
 
   /**
@@ -381,23 +383,23 @@ CollectionKeyManager.prototype = {
    */
   asWBO: function(collection, id)
     this._makeWBO(this._collections, this._default),
 
   /**
    * Compute a new default key, and new keys for any specified collections.
    */
   newKeys: function(collections) {
-    let newDefaultKey = new BulkKeyBundle(null, DEFAULT_KEYBUNDLE_NAME);
+    let newDefaultKey = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
     newDefaultKey.generateRandom();
 
     let newColls = {};
     if (collections) {
       collections.forEach(function (c) {
-        let b = new BulkKeyBundle(null, c);
+        let b = new BulkKeyBundle(c);
         b.generateRandom();
         newColls[c] = b;
       });
     }
     return [newDefaultKey, newColls];
   },
 
   /**
@@ -454,30 +456,30 @@ CollectionKeyManager.prototype = {
 
     if (!payload.default) {
       this._log.warn("No downloaded default key: this should not occur.");
       this._log.warn("Not clearing local keys.");
       throw "No default key in CollectionKeys.setContents(). Cannot proceed.";
     }
 
     // Process the incoming default key.
-    let b = new BulkKeyBundle(null, DEFAULT_KEYBUNDLE_NAME);
-    b.keyPair = payload.default;
+    let b = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
+    b.keyPairB64 = payload.default;
     let newDefault = b;
 
     // Process the incoming collections.
     let newCollections = {};
     if ("collections" in payload) {
       this._log.info("Processing downloaded per-collection keys.");
       let colls = payload.collections;
       for (let k in colls) {
         let v = colls[k];
         if (v) {
-          let keyObj = new BulkKeyBundle(null, k);
-          keyObj.keyPair = v;
+          let keyObj = new BulkKeyBundle(k);
+          keyObj.keyPairB64 = v;
           if (keyObj) {
             newCollections[k] = keyObj;
           }
         }
       }
     }
 
     // Check to see if these are already our keys.
@@ -523,212 +525,16 @@ CollectionKeyManager.prototype = {
     }
 
     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);
-}
-KeyBundle.prototype = {
-  __proto__: Identity.prototype,
-
-  _encrypt: null,
-  _hmac: null,
-  _hmacObj: null,
-  _sha256HMACHasher: null,
-
-  equals: function equals(bundle) {
-    return bundle &&
-           (bundle.hmacKey == this.hmacKey) &&
-           (bundle.encryptionKey == this.encryptionKey);
-  },
-
-  /*
-   * Accessors for the two keys.
-   */
-  get encryptionKey() {
-    return this._encrypt;
-  },
-
-  set encryptionKey(value) {
-    this._encrypt = value;
-  },
-
-  get hmacKey() {
-    return this._hmac;
-  },
-
-  set hmacKey(value) {
-    this._hmac = value;
-    this._hmacObj = value ? Utils.makeHMACKey(value) : null;
-    this._sha256HMACHasher = value ? Utils.makeHMACHasher(
-      Ci.nsICryptoHMAC.SHA256, this._hmacObj) : null;
-  },
-
-  get hmacKeyObject() {
-    return this._hmacObj;
-  },
-
-  get sha256HMACHasher() {
-    return this._sha256HMACHasher;
-  }
-};
-
-function BulkKeyBundle(realm, collectionName) {
-  let log = Log4Moz.repository.getLogger("Sync.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.hmacKey  = Utils.safeAtoB(hm);
-      this._encrypt = en;          // Store in base64.
-    }
-    else {
-      throw "Invalid keypair";
-    }
-  }
-};
-
-function SyncKeyBundle(realm, collectionName, syncKey) {
-  let log = Log4Moz.repository.getLogger("Sync.SyncKeyBundle");
-  log.info("SyncKeyBundle being created for " + collectionName);
-  KeyBundle.call(this, realm, collectionName, syncKey);
-  if (syncKey)
-    this.keyStr = syncKey;      // Accessor sets up keys.
-}
-
-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;
-  },
-
-  set keyStr(value) {
-    this.password = value;
-    this._hmac    = null;
-    this._hmacObj = null;
-    this._encrypt = null;
-    this._sha256HMACHasher = null;
-  },
-
-  /*
-   * 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;
-  },
-
-  get hmacKeyObject() {
-    if (!this._hmacObj)
-      this.generateEntry();
-    return this._hmacObj;
-  },
-
-  get sha256HMACHasher() {
-    if (!this._sha256HMACHasher)
-      this.generateEntry();
-    return this._sha256HMACHasher;
-  },
-
-  /*
-   * If we've got a string, hash it into keys and store them.
-   */
-  generateEntry: function generateEntry() {
-    let syncKey = this.keyStr;
-    if (!syncKey)
-      return;
-
-    // Expand the base32 Sync Key to an AES 256 and 256 bit HMAC key.
-    let prk = Utils.decodeKeyBase32(syncKey);
-    let info = HMAC_INPUT + this.username;
-    let okm = Utils.hkdfExpand(prk, info, 32 * 2);
-    let enc = okm.slice(0, 32);
-    let hmac = okm.slice(32, 64);
-
-    // Save them.
-    this._encrypt = btoa(enc);
-    // Individual sets: cheaper than calling parent setter.
-    this._hmac = hmac;
-    this._hmacObj = Utils.makeHMACKey(hmac);
-    this._sha256HMACHasher = Utils.makeHMACHasher(
-      Ci.nsICryptoHMAC.SHA256, this._hmacObj);
-  }
-};
-
-
 function Collection(uri, recordObj) {
   Resource.call(this, uri);
   this._recordObj = recordObj;
 
   this._full = false;
   this._ids = null;
   this._limit = 0;
   this._older = 0;
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -32,91 +32,34 @@
  * 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 = ["Resource", "AsyncResource",
-                          "Auth", "BrokenBasicAuthenticator",
-                          "BasicAuthenticator", "NoOpAuthenticator"];
+const EXPORTED_SYMBOLS = [
+  "AsyncResource",
+  "Resource"
+];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/async.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/ext/Observers.js");
 Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
-XPCOMUtils.defineLazyGetter(this, "Auth", function () {
-  return new AuthMgr();
-});
-
-// XXX: the authenticator api will probably need to be changed to support
-// other methods (digest, oauth, etc)
-
-function NoOpAuthenticator() {}
-NoOpAuthenticator.prototype = {
-  onRequest: function NoOpAuth_onRequest(headers) {
-    return headers;
-  }
-};
-
-// Warning: This will drop the high unicode bytes from passwords.
-// Use BasicAuthenticator to send non-ASCII passwords UTF8-encoded.
-function BrokenBasicAuthenticator(identity) {
-  this._id = identity;
-}
-BrokenBasicAuthenticator.prototype = {
-  onRequest: function BasicAuth_onRequest(headers) {
-    headers['authorization'] = 'Basic ' +
-      btoa(this._id.username + ':' + this._id.password);
-    return headers;
-  }
-};
-
-function BasicAuthenticator(identity) {
-  this._id = identity;
-}
-BasicAuthenticator.prototype = {
-  onRequest: function onRequest(headers) {
-    headers['authorization'] = 'Basic ' +
-      btoa(this._id.username + ':' + this._id.passwordUTF8);
-    return headers;
-  }
-};
-
-function AuthMgr() {
-  this._authenticators = {};
-  this.defaultAuthenticator = new NoOpAuthenticator();
-}
-AuthMgr.prototype = {
-  defaultAuthenticator: null,
-
-  registerAuthenticator: function AuthMgr_register(match, authenticator) {
-    this._authenticators[match] = authenticator;
-  },
-
-  lookupAuthenticator: function AuthMgr_lookup(uri) {
-    for (let match in this._authenticators) {
-      if (uri.match(match))
-        return this._authenticators[match];
-    }
-    return this.defaultAuthenticator;
-  }
-};
-
-
 /*
  * AsyncResource represents a remote network resource, identified by a URI.
  * Create an instance like so:
  * 
  *   let resource = new AsyncResource("http://foobar.com/path/to/resource");
  * 
  * The 'resource' object has the following methods to issue HTTP requests
  * of the corresponding HTTP methods:
@@ -145,16 +88,24 @@ function AsyncResource(uri) {
 AsyncResource.prototype = {
   _logName: "Sync.AsyncResource",
 
   // ** {{{ AsyncResource.serverTime }}} **
   //
   // Caches the latest server timestamp (X-Weave-Timestamp header).
   serverTime: null,
 
+  /**
+   * Callback to be invoked at request time to add authentication details.
+   *
+   * By default, a global authenticator is provided. If this is set, it will
+   * be used instead of the global one.
+   */
+  authenticator: null,
+
   // The string to use as the base User-Agent in Sync requests.
   // These strings will look something like
   // 
   //   Firefox/4.0 FxSync/1.8.0.20100101.mobile
   //   
   // or
   // 
   //   Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop
@@ -162,39 +113,23 @@ AsyncResource.prototype = {
   _userAgent:
     Services.appinfo.name + "/" + Services.appinfo.version +  // Product.
     " FxSync/" + WEAVE_VERSION + "." +                        // Sync.
     Services.appinfo.appBuildID + ".",                        // Build.
 
   // Wait 5 minutes before killing a request.
   ABORT_TIMEOUT: 300000,
 
-  // ** {{{ AsyncResource.authenticator }}} **
-  //
-  // Getter and setter for the authenticator module
-  // responsible for this particular resource. The authenticator
-  // module may modify the headers to perform authentication
-  // while performing a request for the resource, for example.
-  get authenticator() {
-    if (this._authenticator)
-      return this._authenticator;
-    else
-      return Auth.lookupAuthenticator(this.spec);
-  },
-  set authenticator(value) {
-    this._authenticator = value;
-  },
-
   // ** {{{ AsyncResource.headers }}} **
   //
   // Headers to be included when making a request for the resource.
   // Note: Header names should be all lower case, there's no explicit
   // check for duplicates due to case!
   get headers() {
-    return this.authenticator.onRequest(this._headers);
+    return this._headers;
   },
   set headers(value) {
     this._headers = value;
   },
   setHeader: function Res_setHeader(header, value) {
     this._headers[header.toLowerCase()] = value;
   },
 
@@ -230,17 +165,17 @@ AsyncResource.prototype = {
   },
 
   // ** {{{ AsyncResource._createRequest }}} **
   //
   // This method returns a new IO Channel for requests to be made
   // through. It is never called directly, only {{{_doRequest}}} uses it
   // to obtain a request channel.
   //
-  _createRequest: function Res__createRequest() {
+  _createRequest: function Res__createRequest(method) {
     let channel = Services.io.newChannel(this.spec, null, null)
                           .QueryInterface(Ci.nsIRequest)
                           .QueryInterface(Ci.nsIHttpChannel);
 
     // Always validate the cache:
     channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
     channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
 
@@ -248,34 +183,49 @@ AsyncResource.prototype = {
     channel.notificationCallbacks = new ChannelNotificationListener();
 
     // Compose a UA string fragment from the various available identifiers.
     if (Svc.Prefs.get("sendVersionInfo", true)) {
       let ua = this._userAgent + Svc.Prefs.get("client.type", "desktop");
       channel.setRequestHeader("user-agent", ua, false);
     }
 
-    // Avoid calling the authorizer more than once.
     let headers = this.headers;
-    for (let key in headers) {
+
+    let authenticator = this.authenticator;
+    if (!authenticator) {
+      authenticator = Identity.getResourceAuthenticator();
+    }
+    if (authenticator) {
+      let result = authenticator(this, method);
+      if (result && result.headers) {
+        for (let [k, v] in Iterator(result.headers)) {
+          headers[k.toLowerCase()] = v;
+        }
+      }
+    } else {
+      this._log.debug("No authenticator found.");
+    }
+
+    for (let [key, value] in Iterator(headers)) {
       if (key == 'authorization')
         this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
       else
         this._log.trace("HTTP Header " + key + ": " + headers[key]);
       channel.setRequestHeader(key, headers[key], false);
     }
     return channel;
   },
 
   _onProgress: function Res__onProgress(channel) {},
 
   _doRequest: function _doRequest(action, data, callback) {
     this._log.trace("In _doRequest.");
     this._callback = callback;
-    let channel = this._createRequest();
+    let channel = this._createRequest(action);
 
     if ("undefined" != typeof(data))
       this._data = data;
 
     // PUT and POST are treated differently because they have payload data.
     if ("PUT" == action || "POST" == action) {
       // Convert non-string bodies into JSON
       if (this._data.constructor.toString() != String)
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -649,23 +649,21 @@ SyncStorageRequest.prototype = {
 
   dispatch: function dispatch(method, data, onComplete, onProgress) {
     // Compose a UA string fragment from the various available identifiers.
     if (Svc.Prefs.get("sendVersionInfo", true)) {
       let ua = this.userAgent + Svc.Prefs.get("client.type", "desktop");
       this.setHeader("user-agent", ua);
     }
 
-    // Set the BasicAuth header.
-    let id = ID.get("WeaveID");
-    if (id) {
-      let auth_header = "Basic " + btoa(id.username + ':' + id.passwordUTF8);
-      this.setHeader("authorization", auth_header);
+    let authenticator = Identity.getRESTRequestAuthenticator();
+    if (authenticator) {
+      authenticator(this);
     } else {
-      this._log.debug("Couldn't set Authentication header: WeaveID not found.");
+      this._log.debug("No authenticator found.");
     }
 
     return RESTRequest.prototype.dispatch.apply(this, arguments);
   },
 
   onStartRequest: function onStartRequest(channel) {
     RESTRequest.prototype.onStartRequest.call(this, channel);
     if (this.status == this.ABORTED) {
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -86,63 +86,23 @@ const STORAGE_INFO_TYPES = [INFO_COLLECT
 function WeaveSvc() {
   this._notify = Utils.notify("weave:service:");
 }
 WeaveSvc.prototype = {
 
   _lock: Utils.lock,
   _locked: false,
   _loggedIn: false,
-
-  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");
-    }
-    this.username = this._usernameFromAccount(value);
-  },
-
-  _usernameFromAccount: function _usernameFromAccount(value) {
-    // If we encounter characters not allowed by the API (as found for
-    // instance in an email address), hash the value.
-    if (value && value.match(/[^A-Z0-9._-]/i))
-      return Utils.sha1Base32(value.toLowerCase()).toLowerCase();
-    return value;
-  },
+  _identity: Weave.Identity,
 
-  get username() {
-    return Svc.Prefs.get("username", "").toLowerCase();
-  },
-  set username(value) {
-    if (value) {
-      // Make sure all uses of this new username is lowercase
-      value = value.toLowerCase();
-      Svc.Prefs.set("username", value);
-    }
-    else
-      Svc.Prefs.reset("username");
-
-    // fixme - need to loop over all Identity objects - needs some rethinking...
-    ID.get('WeaveID').username = value;
-    ID.get('WeaveCryptoID').username = value;
-
-    // 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").keyStr,
-  set passphrase(value) ID.get("WeaveCryptoID").keyStr = value,
-
-  get syncKeyBundle() ID.get("WeaveCryptoID"),
+  userBaseURL: null,
+  infoURL: null,
+  storageURL: null,
+  metaURL: null,
+  cryptoKeyURL: null,
 
   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
@@ -213,21 +173,21 @@ WeaveSvc.prototype = {
       }
     }
 
     return Utils.catch.call(this, func, lockExceptions);
   },
 
   _updateCachedURLs: function _updateCachedURLs() {
     // Nothing to cache yet if we don't have the building blocks
-    if (this.clusterURL == "" || this.username == "")
+    if (this.clusterURL == "" || this._identity.username == "")
       return;
 
     let storageAPI = this.clusterURL + SYNC_API_VERSION + "/";
-    this.userBaseURL = storageAPI + this.username + "/";
+    this.userBaseURL = storageAPI + this._identity.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";
     this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO;
   },
@@ -296,17 +256,17 @@ WeaveSvc.prototype = {
       // CollectionKeys, this will prevent us from uploading junk.
       let cipherText = cryptoKeys.ciphertext;
 
       if (!cryptoResp.success) {
         this._log.warn("Failed to download keys.");
         return false;
       }
 
-      let keysChanged = this.handleFetchedKeys(this.syncKeyBundle,
+      let keysChanged = this.handleFetchedKeys(this._identity.syncKeyBundle,
                                                cryptoKeys, true);
       if (keysChanged) {
         // Did they change? If so, carry on.
         this._log.info("Suggesting retry.");
         return true;              // Try again.
       }
 
       // If not, reupload them and continue the current sync.
@@ -385,47 +345,40 @@ WeaveSvc.prototype = {
                       "Weave, since it will not work correctly.");
     }
 
     Svc.Obs.add("weave:service:setup-complete", this);
     Svc.Prefs.observe("engine.", this);
 
     SyncScheduler.init();
 
-    if (!this.enabled)
-      this._log.info("Weave Sync disabled");
-
-    // 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 SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, this.username));
+    if (!this.enabled) {
+      this._log.info("Firefox Sync disabled.");
+    }
 
     this._updateCachedURLs();
 
     let status = this._checkSetup();
-    if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED)
+    if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
       Svc.Obs.notify("weave:engine:start-tracking");
+    }
 
     // Send an event now that Weave service is ready.  We don't do this
     // synchronously so that observers can import this module before
     // registering an observer.
     Utils.nextTick(function() {
       Status.ready = true;
       Svc.Obs.notify("weave:service:ready");
     });
   },
 
-  _checkSetup: function WeaveSvc__checkSetup() {
-    if (!this.enabled)
+  _checkSetup: function _checkSetup() {
+    if (!this.enabled) {
       return Status.service = STATUS_DISABLED;
+    }
     return Status.checkSetup();
   },
 
   _migratePrefs: function _migratePrefs() {
     // Migrate old debugLog prefs.
     let logLevel = Svc.Prefs.get("log.appender.debugLog");
     if (logLevel) {
       Svc.Prefs.set("log.appender.file.level", logLevel);
@@ -502,20 +455,20 @@ WeaveSvc.prototype = {
     } else {
       // Remember that the engine status changed locally until the next sync.
       Svc.Prefs.set("engineStatusChanged." + engine, true);
     }
   },
 
   // gets cluster from central LDAP server and returns it, or null on error
   _findCluster: function _findCluster() {
-    this._log.debug("Finding cluster for user " + this.username);
+    this._log.debug("Finding cluster for user " + this._identity.username);
 
     let fail;
-    let res = new Resource(this.userAPI + this.username + "/node/weave");
+    let res = new Resource(this.userAPI + this._identity.username + "/node/weave");
     try {
       let node = res.get();
       switch (node.status) {
         case 400:
           Status.login = LOGIN_FAILED_LOGIN_REJECTED;
           fail = "Find cluster denied: " + ErrorHandler.errorStr(node);
           break;
         case 404:
@@ -596,26 +549,26 @@ WeaveSvc.prototype = {
   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) {
+    let syncKeyBundle = this._identity.syncKeyBundle;
+    if (!syncKeyBundle) {
       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)) {
+    if (!Utils.isPassphrase(this._identity.syncKey)) {
       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)
@@ -642,17 +595,17 @@ WeaveSvc.prototype = {
         let cryptoKeys;
 
         if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) {
           try {
             cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
             let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
 
             if (cryptoResp.success) {
-              let keysChanged = this.handleFetchedKeys(syncKey, cryptoKeys);
+              let keysChanged = this.handleFetchedKeys(syncKeyBundle, 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;
             }
@@ -710,29 +663,29 @@ WeaveSvc.prototype = {
                       + Utils.exceptionStr(ex));
       ErrorHandler.checkServerError(ex);
       return false;
     }
   },
 
   verifyLogin: function verifyLogin()
     this._notify("verify-login", "", function() {
-      if (!this.username) {
+      if (!this._identity.username) {
         this._log.warn("No username in verifyLogin.");
         Status.login = LOGIN_FAILED_NO_USERNAME;
         return false;
       }
 
       // Unlock master password, or return.
       // Attaching auth credentials to a request requires access to
       // passwords, which means that Resource.get can throw MP-related
       // exceptions!
       // Try to fetch the passphrase first, while we still have control.
       try {
-        this.passphrase;
+        this._identity.syncKey;
       } catch (ex) {
         this._log.debug("Fetching passphrase threw " + ex +
                         "; assuming master password locked.");
         Status.login = MASTER_PASSWORD_LOCKED;
         return false;
       }
 
       try {
@@ -750,17 +703,17 @@ WeaveSvc.prototype = {
 
         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) {
+            if (!this._identity.syncKey) {
               this._log.warn("No passphrase in verifyLogin.");
               Status.login = LOGIN_FAILED_NO_PASSPHRASE;
               return false;
             }
 
             // Go ahead and do remote setup, so that we can determine
             // conclusively that our passphrase is correct.
             if (this._remoteSetup()) {
@@ -770,36 +723,18 @@ WeaveSvc.prototype = {
             }
 
             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();
-              if (test.status == 200) {
-                this._log.debug("Non-ASCII password detected. "
-                                + "Changing to UTF-8 version.");
-                // Let's change the password on the server to the UTF8 version.
-                let url = this.userAPI + this.username + "/password";
-                res = new Resource(url);
-                res.authenticator = auth;
-                res.post(id.passwordUTF8);
-                return this.verifyLogin();
-              }
-            }
-            // Yes, we want to fall through to the 404 case.
+            // Fall through to the 404 case.
+
           case 404:
             // Check that we're verifying with the correct cluster
             if (this._setCluster())
               return this.verifyLogin();
 
             // We must have the right cluster, but the server doesn't expect us
             Status.login = LOGIN_FAILED_LOGIN_REJECTED;
             return false;
@@ -820,17 +755,17 @@ WeaveSvc.prototype = {
       }
     })(),
 
   generateNewSymmetricKeys:
   function WeaveSvc_generateNewSymmetricKeys() {
     this._log.info("Generating new keys WBO...");
     let wbo = CollectionKeys.generateNewKeysWBO();
     this._log.info("Encrypting new key bundle.");
-    wbo.encrypt(this.syncKeyBundle);
+    wbo.encrypt(this._identity.syncKeyBundle);
 
     this._log.info("Uploading...");
     let uploadRes = wbo.upload(this.cryptoKeysURL);
     if (uploadRes.status != 200) {
       this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!");
       ErrorHandler.checkServerError(uploadRes);
       throw new Error("Unable to upload symmetric keys.");
     }
@@ -866,74 +801,74 @@ WeaveSvc.prototype = {
     
     // Download and install them.
     let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
     let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
     if (cryptoResp.status != 200) {
       this._log.warn("Failed to download keys.");
       throw new Error("Symmetric key download failed.");
     }
-    let keysChanged = this.handleFetchedKeys(this.syncKeyBundle,
+    let keysChanged = this.handleFetchedKeys(this._identity.syncKeyBundle,
                                              cryptoKeys, true);
     if (keysChanged) {
       this._log.info("Downloaded keys differed, as expected.");
     }
   },
 
   changePassword: function WeaveSvc_changePassword(newpass)
     this._notify("changepwd", "", function() {
-      let url = this.userAPI + this.username + "/password";
+      let url = this.userAPI + this._identity.username + "/password";
       try {
         let resp = new Resource(url).post(Utils.encodeUTF8(newpass));
         if (resp.status != 200) {
           this._log.debug("Password change failed: " + resp);
           return false;
         }
       }
       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.
-      this.password = newpass;
+      this._identity.basicPassword = newpass;
       this.persistLogin();
       return true;
     })(),
 
   changePassphrase: function WeaveSvc_changePassphrase(newphrase)
     this._catch(this._notify("changepph", "", function() {
       /* Wipe. */
       this.wipeServer();
 
       this.logout();
 
       /* Set this so UI is updated on next run. */
-      this.passphrase = newphrase;
+      this._identity.syncKey = newphrase;
       this.persistLogin();
 
       /* We need to re-encrypt everything, so reset. */
       this.resetClient();
       CollectionKeys.clear();
 
       /* Login and sync. This also generates new keys. */
       this.sync();
       return true;
     }))(),
 
-  startOver: function() {
+  startOver: function startOver() {
     this._log.trace("Invoking Service.startOver.");
     Svc.Obs.notify("weave:engine:stop-tracking");
     Status.resetSync();
 
     // We want let UI consumers of the following notification know as soon as
     // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
     // by emptying the passphrase (we still need the password).
-    Service.passphrase = "";
+    this._identity.syncKey = null;
     Status.login = LOGIN_FAILED_NO_PASSPHRASE;
     this.logout();
     Svc.Obs.notify("weave:service:start-over");
 
     // Deletion doesn't make sense if we aren't set up yet!
     if (this.clusterURL != "") {
       // Clear client-specific data from the server, including disabled engines.
       for each (let engine in [Clients].concat(Engines.getAll())) {
@@ -954,86 +889,87 @@ WeaveSvc.prototype = {
     Status.resetBackoff();
 
     // Reset Weave prefs.
     this._ignorePrefObserver = true;
     Svc.Prefs.resetBranch("");
     this._ignorePrefObserver = false;
 
     Svc.Prefs.set("lastversion", WEAVE_VERSION);
-    // Find weave logins and remove them.
-    this.password = "";
-    Services.logins.findLogins({}, PWDMGR_HOST, "", "").map(function(login) {
-      Services.logins.removeLogin(login);
-    });
+
+    this._identity.deleteSyncCredentials();
   },
 
   persistLogin: function persistLogin() {
-    // Canceled master password prompt can prevent these from succeeding.
     try {
-      ID.get("WeaveID").persist();
-      ID.get("WeaveCryptoID").persist();
+      this._identity.persistCredentials();
+    } catch (ex) {
+      this._log.info("Unable to persist credentials: " + ex);
     }
-    catch(ex) {}
   },
 
-  login: function WeaveSvc_login(username, password, passphrase)
+  login: function login(username, password, passphrase)
     this._catch(this._lock("service.js: login",
           this._notify("login", "", function() {
       this._loggedIn = false;
       if (Services.io.offline) {
         Status.login = LOGIN_FAILED_NETWORK_ERROR;
         throw "Application is offline, login should not be called";
       }
 
       let initialStatus = this._checkSetup();
-      if (username)
-        this.username = username;
-      if (password)
-        this.password = password;
-      if (passphrase)
-        this.passphrase = passphrase;
+      if (username) {
+        this._identity.username = username;
+      }
+      if (password) {
+        this._identity.basicPassword = password;
+      }
+      if (passphrase) {
+        this._identity.syncKey = passphrase;
+      }
 
-      if (this._checkSetup() == CLIENT_NOT_CONFIGURED)
-        throw "aborting login, client not configured";
+      if (this._checkSetup() == CLIENT_NOT_CONFIGURED) {
+        throw "Aborting login, client not configured.";
+      }
 
       // Calling login() with parameters when the client was
       // previously not configured means setup was completed.
       if (initialStatus == CLIENT_NOT_CONFIGURED
-          && (username || password || passphrase))
+          && (username || password || passphrase)) {
         Svc.Obs.notify("weave:service:setup-complete");
+      }
 
-      this._log.info("Logging in user " + this.username);
+      this._log.info("Logging in user " + this._identity.username);
+      this._updateCachedURLs();
 
       if (!this.verifyLogin()) {
         // verifyLogin sets the failure states here.
         throw "Login failed: " + Status.login;
       }
 
       this._loggedIn = true;
 
       return true;
     })))(),
 
-  logout: function WeaveSvc_logout() {
+  logout: function logout() {
     // No need to do anything if we're already logged out.
     if (!this._loggedIn)
       return;
 
     this._log.info("Logging out");
     this._loggedIn = false;
 
     Svc.Obs.notify("weave:service:logout:finish");
   },
 
   checkAccount: function checkAccount(account) {
-    let username = this._usernameFromAccount(account);
+    let username = this._identity.usernameFromAccount(account);
     let url = this.userAPI + username;
     let res = new Resource(url);
-    res.authenticator = new NoOpAuthenticator();
 
     let data = "";
     try {
       data = res.get();
       if (data.status == 200) {
         if (data == "0")
           return "available";
         else if (data == "1")
@@ -1044,27 +980,26 @@ WeaveSvc.prototype = {
     catch(ex) {}
 
     // Convert to the error string, or default to generic on exception.
     return ErrorHandler.errorStr(data);
   },
 
   createAccount: function createAccount(email, password,
                                         captchaChallenge, captchaResponse) {
-    let username = this._usernameFromAccount(email);
+    let username = this._identity.usernameFromAccount(email);
     let payload = JSON.stringify({
       "password": Utils.encodeUTF8(password),
       "email": email,
       "captcha-challenge": captchaChallenge,
       "captcha-response": captchaResponse
     });
 
     let url = this.userAPI + username;
     let res = new Resource(url);
-    res.authenticator = new NoOpAuthenticator();
 
     // Hint to server to allow scripted user creation or otherwise
     // ignore captcha.
     if (Svc.Prefs.isSet("admin-secret"))
       res.setHeader("X-Weave-Secret", Svc.Prefs.get("admin-secret", ""));
 
     let error = "generic-server-error";
     try {
@@ -1101,17 +1036,17 @@ WeaveSvc.prototype = {
       // Delete the cached meta record...
       this._log.debug("Clearing cached meta record. metaModified is " +
           JSON.stringify(this.metaModified) + ", setting to " +
           JSON.stringify(infoResponse.obj.meta));
 
       Records.del(this.metaURL);
 
       // ... fetch the current record from the server, and COPY THE FLAGS.
-      let newMeta       = Records.get(this.metaURL);
+      let newMeta = Records.get(this.metaURL);
 
       if (!Records.response.success || !newMeta) {
         this._log.debug("No meta/global record on the server. Creating one.");
         newMeta = new WBORecord("meta", "global");
         newMeta.payload.syncID = this.syncID;
         newMeta.payload.storageVersion = STORAGE_VERSION;
 
         newMeta.isNew = true;
@@ -1504,60 +1439,51 @@ WeaveSvc.prototype = {
         // appropriate value.
         return false;
       }
     }
     return 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;
+    let p = this._identity.syncKey;
+
+    if (!p) {
+      return false;
+    }
 
     // Check whether it's already a key that we generated.
-    if (!this.syncKeyNeedsUpgrade(p))
+    if (Utils.isPassphrase(p)) {
+      this._log.info("Sync key is up-to-date: no need to upgrade.");
       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._identity.syncKey = 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.");
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -37,20 +37,22 @@ const EXPORTED_SYMBOLS = ["Status"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://gre/modules/Services.jsm");
 
 let Status = {
   _log: Log4Moz.repository.getLogger("Sync.Status"),
+  _authManager: Identity,
   ready: false,
 
   get service() {
     return this._service;
   },
 
   set service(code) {
     this._log.debug("Status.service: " + this._service + " => " + code);
@@ -60,20 +62,20 @@ let Status = {
   get login() {
     return this._login;
   },
 
   set login(code) {
     this._log.debug("Status.login: " + this._login + " => " + code);
     this._login = code;
 
-    if (code == LOGIN_FAILED_NO_USERNAME || 
-        code == LOGIN_FAILED_NO_PASSWORD || 
+    if (code == LOGIN_FAILED_NO_USERNAME ||
+        code == LOGIN_FAILED_NO_PASSWORD ||
         code == LOGIN_FAILED_NO_PASSPHRASE) {
-      this.service = CLIENT_NOT_CONFIGURED;      
+      this.service = CLIENT_NOT_CONFIGURED;
     } else if (code != LOGIN_SUCCEEDED) {
       this.service = LOGIN_FAILED;
     } else {
       this.service = STATUS_OK;
     }
   },
 
   get sync() {
@@ -104,56 +106,24 @@ let Status = {
   toString: function toString() {
     return "<Status" +
            ": login: "   + Status.login +
            ", service: " + Status.service +
            ", sync: "    + Status.sync + ">";
   },
 
   checkSetup: function checkSetup() {
-    // 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;
+    let result = this._authManager.currentAuthState;
+    if (result == STATUS_OK) {
+      Status.service = result;
+      return result;
     }
 
-    Cu.import("resource://services-sync/util.js");
-    Cu.import("resource://services-sync/identity.js");
-    Cu.import("resource://services-sync/record.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 SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, username));
-      }
-
-      if (!id.keyStr) {
-        Status.login = LOGIN_FAILED_NO_PASSPHRASE;
-        return Status.service;
-      }
-    }
-    return Status.service = STATUS_OK;
+    Status.login = result;
+    return Status.service;
   },
 
   resetBackoff: function resetBackoff() {
     this.enforceBackoff = false;
     this.backoffInterval = 0;
     this.minimumNextSync = 0;
   },
 
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -44,17 +44,16 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/async.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/ext/Observers.js");
 Cu.import("resource://services-sync/ext/Preferences.js");
 Cu.import("resource://services-sync/ext/StringBundle.js");
 Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/status.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 /*
  * Utility functions
@@ -1228,22 +1227,24 @@ let Utils = {
     return false;
   },
   
   /**
    * Return a value for a backoff interval.  Maximum is eight hours, unless
    * Status.backoffInterval is higher.
    *
    */
-  calculateBackoff: function calculateBackoff(attempts, base_interval) {
+  calculateBackoff: function calculateBackoff(attempts, baseInterval,
+                                              statusInterval) {
     let backoffInterval = attempts *
-                          (Math.floor(Math.random() * base_interval) +
-                           base_interval);
-    return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), Status.backoffInterval);
-  }
+                          (Math.floor(Math.random() * baseInterval) +
+                           baseInterval);
+    return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
+                    statusInterval);
+  },
 };
 
 XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() {
   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                     .createInstance(Ci.nsIScriptableUnicodeConverter);
   converter.charset = "UTF-8";
   return converter;
 });
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 let btoa;
 let atob;
 
 let provider = {
   getFile: function(prop, persistent) {
@@ -255,24 +256,32 @@ FakeCryptoService.prototype = {
     return "some derived key string composed of bytes";
   },
 
   generateRandomBytes: function(aByteCount) {
     return "not-so-random-now-are-we-HA-HA-HA! >:)".slice(aByteCount);
   }
 };
 
-
-function SyncTestingInfrastructure() {
-  Cu.import("resource://services-sync/identity.js");
+function setBasicCredentials(username, password, syncKey) {
+  let auth = Identity;
+  auth.username = username;
+  auth.basicPassword = password;
+  auth.syncKey = syncKey;
+}
 
-  ID.set('WeaveID',
-         new Identity('Mozilla Services Encryption Passphrase', 'foo'));
-  ID.set('WeaveCryptoID',
-         new Identity('Mozilla Services Encryption Passphrase', 'foo'));
+function SyncTestingInfrastructure(username, password, syncKey) {
+  Cu.import("resource://services-sync/service.js");
+
+  Identity.account = username || "foo";
+  Identity.basicPassword = password || "password";
+  Identity.syncKey = syncKey || "foo";
+
+  Service.serverURL = TEST_SERVER_URL;
+  Service.clusterURL = TEST_CLUSTER_URL;
 
   this.logStats = initTestLogging();
   this.fakeFilesystem = new FakeFilesystemService({});
   this.fakeGUIDService = new FakeGUIDService();
   this.fakeCryptoService = new FakeCryptoService();
 }
 
 /*
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -59,18 +59,22 @@ function httpd_handler(statusCode, statu
   };
 }
 
 function basic_auth_header(user, password) {
   return "Basic " + btoa(user + ":" + Utils.encodeUTF8(password));
 }
 
 function basic_auth_matches(req, user, password) {
-  return req.hasHeader("Authorization") &&
-         (req.getHeader("Authorization") == basic_auth_header(user, password));
+  if (!req.hasHeader("Authorization")) {
+    return false;
+  }
+
+  let expected = basic_auth_header(user, Utils.encodeUTF8(password));
+  return req.getHeader("Authorization") == expected;
 }
 
 function httpd_basic_auth_handler(body, metadata, response) {
   if (basic_auth_matches(metadata, "guest", "guest")) {
     response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
     response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
   } else {
     body = "This path exists and is protected - failed";
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -149,21 +149,17 @@ add_test(function test_disabled_install_
 
   Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 
   const USER       = "foo";
   const PASSWORD   = "password";
   const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
   const ADDON_ID   = "addon1@tests.mozilla.org";
 
-  Service.username   = USER;
-  Service.password   = PASSWORD;
-  Service.passphrase = PASSPHRASE;
-  Service.serverURL  = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
+  new SyncTestingInfrastructure(USER, PASSWORD, PASSPHRASE);
 
   generateNewKeys();
 
   let contents = {
     meta: {global: {engines: {addons: {version: engine.version,
                                       syncID:  engine.syncID}}}},
     crypto: {},
     addons: {}
deleted file mode 100644
--- a/services/sync/tests/unit/test_auth_manager.js
+++ /dev/null
@@ -1,65 +0,0 @@
-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/util.js");
-
-let logger;
-
-function server_handler(metadata, response) {
-  let body, statusCode, status;
-  let guestHeader = basic_auth_header("guest", "guest");
-  let johnHeader  = basic_auth_header("johndoe", "moneyislike$£¥");
-
-  _("Guest header: " + guestHeader);
-  _("John header:  " + johnHeader);
-  
-  switch (metadata.getHeader("Authorization")) {
-    case guestHeader:
-      body = "This path exists and is protected";
-      statusCode = 200;
-      status = "OK";
-      break;
-    case johnHeader:
-      body = "This path exists and is protected by a UTF8 password";
-      statusCode = 200;
-      status = "OK";
-      break;
-    default:
-      body = "This path exists and is protected - failed";
-      statusCode = 401;
-      status = "Unauthorized";
-  }
-
-  response.setStatusLine(metadata.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 = new nsHttpServer();
-  server.registerPathHandler("/foo", server_handler);
-  server.registerPathHandler("/bar", server_handler);
-  server.start(8080);
-
-  let guestIdentity = new Identity("secret", "guest", "guest");
-  let johnIdentity  = new Identity("secret2", "johndoe", "moneyislike$£¥")
-  let guestAuth     = new BasicAuthenticator(guestIdentity);
-  let johnAuth      = new BasicAuthenticator(johnIdentity);
-  Auth.defaultAuthenticator = guestAuth;
-  Auth.registerAuthenticator("bar$", johnAuth);
-
-  try {
-    let content = new Resource("http://localhost:8080/foo").get();
-    do_check_eq(content, "This path exists and is protected");
-    do_check_eq(content.status, 200);
-
-    content = new Resource("http://localhost:8080/bar").get();
-    do_check_eq(content, "This path exists and is protected by a UTF8 password");
-    do_check_eq(content.status, 200);
-  } finally {
-    server.stop(do_test_finished);
-  }
-}
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -87,20 +87,17 @@ function serverForFoo(engine) {
     meta: {global: {engines: {bookmarks: {version: engine.version,
                                           syncID: engine.syncID}}}},
     bookmarks: {}
   });
 }
 
 add_test(function test_processIncoming_error_orderChildren() {
   _("Ensure that _orderChildren() is called even when _processIncoming() throws an error.");
-  let syncTesting = new SyncTestingInfrastructure();
-  Svc.Prefs.set("serverURL", TEST_SERVER_URL);
-  Svc.Prefs.set("clusterURL", TEST_CLUSTER_URL);
-  Svc.Prefs.set("username", "foo");
+  new SyncTestingInfrastructure();
 
   let engine = new BookmarksEngine();
   let store  = engine._store;
   let server = serverForFoo(engine);
 
   let collection = server.user("foo").collection("bookmarks");
 
   try {
@@ -160,20 +157,17 @@ add_test(function test_processIncoming_e
     Svc.Prefs.resetBranch("");
     Records.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_restorePromptsReupload() {
   _("Ensure that restoring from a backup will reupload all records.");
-  let syncTesting = new SyncTestingInfrastructure();
-  Svc.Prefs.set("username", "foo");
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
+  new SyncTestingInfrastructure();
 
   let engine = new BookmarksEngine();
   let store  = engine._store;
   let server = serverForFoo(engine);
 
   let collection = server.user("foo").collection("bookmarks");
 
   Svc.Obs.notify("weave:engine:start-tracking");   // We skip usual startup...
@@ -328,20 +322,17 @@ add_test(function test_mismatched_types(
     "description":null,
     "children":
       ["HCRq40Rnxhrd", "YeyWCV1RVsYw", "GCceVZMhvMbP", "sYi2hevdArlF",
        "vjbZlPlSyGY8", "UtjUhVyrpeG6", "rVq8WMG2wfZI", "Lx0tcy43ZKhZ",
        "oT74WwV8_j4P", "IztsItWVSo3-"],
     "parentid": "toolbar"
   };
 
-  let syncTesting = new SyncTestingInfrastructure();
-  Svc.Prefs.set("username", "foo");
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
+  new SyncTestingInfrastructure();
 
   let engine = new BookmarksEngine();
   let store  = engine._store;
   let server = serverForFoo(engine);
 
   _("GUID: " + store.GUIDForId(6, true));
 
   try {
@@ -374,20 +365,18 @@ add_test(function test_mismatched_types(
     Records.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_bookmark_guidMap_fail() {
   _("Ensure that failures building the GUID map cause early death.");
 
-  let syncTesting = new SyncTestingInfrastructure();
-  Svc.Prefs.set("serverURL", TEST_SERVER_URL);
-  Svc.Prefs.set("clusterURL", TEST_CLUSTER_URL);
-  Svc.Prefs.set("username", "foo");
+  new SyncTestingInfrastructure();
+
   let engine = new BookmarksEngine();
   let store = engine._store;
 
   let store  = engine._store;
   let server = serverForFoo(engine);
   let coll   = server.user("foo").collection("bookmarks");
 
   // Add one item to the server.
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -1,26 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/bookmarks.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";
-  
+  Identity.username = "john@example.com";
+  Identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
   generateNewKeys();
-  
+  let keyBundle = Identity.syncKeyBundle;
+
   let log = Log4Moz.repository.getLogger("Test");
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   log.info("Creating a record");
 
   let u = "http://localhost:8080/storage/bookmarks/foo";
   let placesItem = new PlacesItem("bookmarks", "foo", "bookmark");
   let bookmarkItem = prepareBookmarkItem("bookmarks", "foo");
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -53,16 +53,18 @@ function serverForFoo(engine) {
                                           syncID: engine.syncID}}}},
     bookmarks: {}
   });
 }
 
 // Verify that Places smart bookmarks have their annotation uploaded and
 // handled locally.
 add_test(function test_annotation_uploaded() {
+  new SyncTestingInfrastructure();
+
   let startCount = smartBookmarkCount();
   
   _("Start count is " + startCount);
   
   if (startCount > 0) {
     // This can happen in XULRunner.
     clearBookmarks();
     _("Start count is now " + startCount);
@@ -101,20 +103,16 @@ add_test(function test_annotation_upload
 
   _("Make sure the new record carries with it the annotation.");
   do_check_eq("MostVisited", record.queryId);
 
   _("Our count has increased since we started.");
   do_check_eq(smartBookmarkCount(), startCount + 1);
 
   _("Sync record to the server.");
-  Svc.Prefs.set("username", "foo");
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
-
   let server = serverForFoo(engine);
   let collection = server.user("foo").collection("bookmarks");
 
   try {
     engine.sync();
     let wbos = collection.keys(function (id) {
                  return ["menu", "toolbar", "mobile"].indexOf(id) == -1;
                });
@@ -173,34 +171,32 @@ add_test(function test_annotation_upload
     store.wipe();
     Svc.Prefs.resetBranch("");
     Records.clearCache();
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_smart_bookmarks_duped() {
+  new SyncTestingInfrastructure();
+
   let parent = PlacesUtils.toolbarFolderId;
   let uri =
     Utils.makeURI("place:redirectsMode=" +
                   Ci.nsINavHistoryQueryOptions.REDIRECTS_MODE_TARGET +
                   "&sort=" +
                   Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING +
                   "&maxResults=10");
   let title = "Most Visited";
   let mostVisitedID = newSmartBookmark(parent, uri, -1, title, "MostVisited");
   let mostVisitedGUID = store.GUIDForId(mostVisitedID);
   
   let record = store.createRecord(mostVisitedGUID);
   
   _("Prepare sync.");
-  Svc.Prefs.set("username", "foo");
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
-
   let server = serverForFoo(engine);
   let collection = server.user("foo").collection("bookmarks");
 
   try {
     engine._syncStartup();
     
     _("Verify that mapDupe uses the anno, discovering a dupe regardless of URI.");
     do_check_eq(mostVisitedGUID, engine._mapDupe(record));
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/service.js");
 
@@ -43,17 +46,17 @@ add_test(function test_bad_hmac() {
     let coll = user.collection("clients");
     let wbo  = coll.wbo(id);
     return !wbo || !wbo.payload;
   }
 
   function uploadNewKeys() {
     generateNewKeys();
     let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-    serverKeys.encrypt(Weave.Service.syncKeyBundle);
+    serverKeys.encrypt(Weave.Identity.syncKeyBundle);
     do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
   }
 
   try {
     let passphrase     = "abcdeabcdeabcdeabcdeabcdea";
     Service.serverURL  = TEST_SERVER_URL;
     Service.clusterURL = TEST_CLUSTER_URL;
     Service.login("foo", "ilovejane", passphrase);
@@ -72,17 +75,17 @@ add_test(function test_bad_hmac() {
     deletedItems       = [];
 
     _("Change our keys and our client ID, reupload keys.");
     let oldLocalID  = Clients.localID;     // Preserve to test for deletion!
     Clients.localID = Utils.makeGUID();
     Clients.resetClient();
     generateNewKeys();
     let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-    serverKeys.encrypt(Weave.Service.syncKeyBundle);
+    serverKeys.encrypt(Weave.Identity.syncKeyBundle);
     do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
 
     _("Sync.");
     Clients._sync();
 
     _("Old record " + oldLocalID + " was deleted, new one uploaded.");
     check_clients_count(1);
     check_client_deleted(oldLocalID);
@@ -159,20 +162,18 @@ add_test(function test_properties() {
   } finally {
     Svc.Prefs.resetBranch("");
     run_next_test();
   }
 });
 
 add_test(function test_sync() {
   _("Ensure that Clients engine uploads a new client record once a week.");
-  
-  Svc.Prefs.set("serverURL", TEST_SERVER_URL);
-  Svc.Prefs.set("clusterURL", TEST_CLUSTER_URL);
-  Svc.Prefs.set("username", "foo");
+
+  new SyncTestingInfrastructure();
   generateNewKeys();
 
   let contents = {
     meta: {global: {engines: {clients: {version: Clients.version,
                                         syncID: Clients.syncID}}}},
     clients: {},
     crypto: {}
   };
@@ -400,19 +401,17 @@ add_test(function test_process_incoming_
 
   // logout command causes processIncomingCommands to return explicit false.
   do_check_false(Clients.processIncomingCommands());
 });
 
 add_test(function test_command_sync() {
   _("Ensure that commands are synced across clients.");
 
-  Svc.Prefs.set("serverURL", TEST_SERVER_URL);
-  Svc.Prefs.set("clusterURL", TEST_CLUSTER_URL);
-  Svc.Prefs.set("username", "foo");
+  new SyncTestingInfrastructure();
 
   Clients._store.wipe();
   generateNewKeys();
 
   let contents = {
     meta: {global: {engines: {clients: {version: Clients.version,
                                         syncID: Clients.syncID}}}},
     clients: {},
--- a/services/sync/tests/unit/test_clients_escape.js
+++ b/services/sync/tests/unit/test_clients_escape.js
@@ -1,24 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.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'));
+
+  Identity.username = "john@example.com";
   Svc.Prefs.set("clusterURL", "http://fakebase/");
   let baseUri = "http://fakebase/1.1/foo/storage/";
   let pubUri = baseUri + "keys/pubkey";
   let privUri = baseUri + "keys/privkey";
 
-  let keyBundle = ID.set("WeaveCryptoID",
-                         new SyncKeyBundle(null, "john@example.com", "abcdeabcdeabcdeabcdeabcdea"));
+  Identity.syncKey = "abcdeabcdeabcdeabcdeabcdea";
+  let keyBundle = Identity.syncKeyBundle;
 
   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");
--- a/services/sync/tests/unit/test_collections_recovery.js
+++ b/services/sync/tests/unit/test_collections_recovery.js
@@ -14,21 +14,19 @@ add_test(function test_missing_crypto_co
         response.setStatusLine(request.httpVersion, 200, "OK");
         response.bodyOutputStream.write(body, body.length);
       } else {
         handler(request, response);
       }
     };
   }
 
+  setBasicCredentials("johndoe", "ilovejane", "a-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa");
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "a-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa";
 
   let handlers = {
     "/1.1/johndoe/info/collections": maybe_empty(johnHelper.handler),
     "/1.1/johndoe/storage/crypto/keys": johnU("crypto", new ServerWBO("keys").handler()),
     "/1.1/johndoe/storage/meta/global": johnU("meta",   new ServerWBO("global").handler())
   };
   let collections = ["clients", "bookmarks", "forms", "history",
                      "passwords", "prefs", "tabs"];
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -45,24 +45,21 @@ add_test(function test_locally_changed_k
                           },
                           extData: {
                             weaveLastUsed: 1
                           }}]}]};
     delete Svc.Session;
     Svc.Session = {
       getBrowserState: function () JSON.stringify(myTabs)
     };
-    
-    Weave.Service.username = "johndoe";
-    Weave.Service.password = "ilovejane";
-    Weave.Service.passphrase = passphrase;
-    
-    Weave.Service.serverURL = TEST_SERVER_URL;
-    Weave.Service.clusterURL = TEST_CLUSTER_URL;
-    
+
+    setBasicCredentials("johndoe", "password", passphrase);
+    Service.serverURL = TEST_SERVER_URL;
+    Service.clusterURL = TEST_CLUSTER_URL;
+
     Engines.register(HistoryEngine);
     Weave.Service._registerEngines();
     
     function corrupt_local_keys() {
       CollectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(),
                                          Svc.Crypto.generateRandomKey()];
     }
     
@@ -74,17 +71,17 @@ add_test(function test_locally_changed_k
                  "storageVersion": STORAGE_VERSION};
     m.upload(Weave.Service.metaURL);
     
     _("New meta/global: " + JSON.stringify(johndoe.collection("meta").wbo("global")));
     
     // Upload keys.
     generateNewKeys();
     let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-    serverKeys.encrypt(Weave.Service.syncKeyBundle);
+    serverKeys.encrypt(Weave.Identity.syncKeyBundle);
     do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
     
     // Check that login works.
     do_check_true(Weave.Service.login("johndoe", "ilovejane", passphrase));
     do_check_true(Weave.Service.isLoggedIn);
     
     // Sync should upload records.
     Weave.Service.sync();
--- a/services/sync/tests/unit/test_engine_abort.js
+++ b/services/sync/tests/unit/test_engine_abort.js
@@ -1,17 +1,14 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 
 add_test(function test_processIncoming_abort() {
   _("An abort exception, raised in applyIncoming, will abort _processIncoming.");
-  let syncTesting = new SyncTestingInfrastructure();
-  Svc.Prefs.set("serverURL", TEST_SERVER_URL);
-  Svc.Prefs.set("clusterURL", TEST_CLUSTER_URL);
-  Svc.Prefs.set("username", "foo");
+  new SyncTestingInfrastructure();
   generateNewKeys();
 
   let engine = new RotaryEngine();
 
   _("Create some server data.");
   let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
   meta_global.payload.engines = {rotary: {version: engine.version,
                                           syncID: engine.syncID}};
--- a/services/sync/tests/unit/test_errorhandler.js
+++ b/services/sync/tests/unit/test_errorhandler.js
@@ -1,13 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/status.js");
 
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
 
 const TEST_MAINTENANCE_URL = "http://localhost:8080/maintenance/";
 const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
@@ -47,18 +48,17 @@ function run_test() {
   Log4Moz.repository.getLogger("Sync.ErrorHandler").level = Log4Moz.Level.Trace;
 
   run_next_test();
 }
 
 function generateCredentialsChangedFailure() {
   // Make sync fail due to changed credentials. We simply re-encrypt
   // the keys with a different Sync Key, without changing the local one.
-  let newSyncKeyBundle = new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, Service.username);
-  newSyncKeyBundle.keyStr = "23456234562345623456234562";
+  let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
   let keys = CollectionKeys.asWBO();
   keys.encrypt(newSyncKeyBundle);
   keys.upload(Service.cryptoKeysURL);
 }
 
 function service_unavailable(request, response) {
   let body = "Service Unavailable";
   response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
@@ -113,29 +113,27 @@ function sync_httpd_setup() {
       upd("crypto", (new ServerWBO("keys")).handler()),
     "/maintenance/1.1/broken.wipe/storage": service_unavailable,
     "/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
     "/maintenance/1.1/broken.wipe/storage/catapult": service_unavailable
   });
 }
 
 function setUp() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   return generateAndUploadKeys();
 }
 
 function generateAndUploadKeys() {
   generateNewKeys();
   let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Service.syncKeyBundle);
+  serverKeys.encrypt(Identity.syncKeyBundle);
   return serverKeys.upload(Service.cryptoKeysURL).success;
 }
 
 function clean() {
   Service.startOver();
   Status.resetSync();
   Status.resetBackoff();
 }
@@ -167,17 +165,18 @@ add_test(function test_401_logout() {
         Service.startOver();
         server.stop(run_next_test);
       });
     }
     Svc.Obs.add("weave:service:login:error", onLoginError);
   }
 
   // Make sync fail due to login rejected.
-  Service.username = "janedoe";
+  setBasicCredentials("janedoe", "irrelevant", "irrelevant");
+  Service._updateCachedURLs();
 
   _("Starting first sync.");
   Service.sync();
   _("First sync done.");
 });
 
 add_test(function test_credentials_changed_logout() {
   let server = sync_httpd_setup();
@@ -420,17 +419,17 @@ add_test(function test_shouldReportError
   server.stop(run_next_test);
 });
 
 add_test(function test_login_syncAndReportErrors_non_network_error() {
   // Test non-network errors are reported
   // when calling syncAndReportErrors
   let server = sync_httpd_setup();
   setUp();
-  Service.password = "";
+  Identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
     clean();
     server.stop(run_next_test);
   });
@@ -464,17 +463,17 @@ add_test(function test_sync_syncAndRepor
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_prolonged_non_network_error() {
   // Test prolonged, non-network errors are
   // reported when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
-  Service.password = "";
+  Identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
     clean();
     server.stop(run_next_test);
   });
@@ -505,19 +504,17 @@ add_test(function test_sync_syncAndRepor
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_network_error() {
   // Test network errors are reported when calling syncAndReportErrors.
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
     clean();
@@ -544,19 +541,17 @@ add_test(function test_sync_syncAndRepor
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_syncAndReportErrors_prolonged_network_error() {
   // Test prolonged, network errors are reported
   // when calling syncAndReportErrors.
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
 
     clean();
@@ -584,17 +579,17 @@ add_test(function test_sync_syncAndRepor
   setLastSync(PROLONGED_ERROR_DURATION);
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_login_prolonged_non_network_error() {
   // Test prolonged, non-network errors are reported
   let server = sync_httpd_setup();
   setUp();
-  Service.password = "";
+  Identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
     clean();
     server.stop(run_next_test);
   });
@@ -624,19 +619,17 @@ add_test(function test_sync_prolonged_no
   });
 
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_prolonged_network_error() {
   // Test prolonged, network errors are reported
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
 
     clean();
@@ -663,17 +656,17 @@ add_test(function test_sync_prolonged_ne
   setLastSync(PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_non_network_error() {
   // Test non-network errors are reported
   let server = sync_httpd_setup();
   setUp();
-  Service.password = "";
+  Identity.basicPassword = null;
 
   Svc.Obs.add("weave:ui:login:error", function onSyncError() {
     Svc.Obs.remove("weave:ui:login:error", onSyncError);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
 
     clean();
     server.stop(run_next_test);
   });
@@ -702,19 +695,17 @@ add_test(function test_sync_non_network_
     server.stop(run_next_test);
   });
 
   setLastSync(NON_PROLONGED_ERROR_DURATION);
   Service.sync();
 });
 
 add_test(function test_login_network_error() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   // Test network errors are not reported.
   Svc.Obs.add("weave:ui:clear-error", function onClearError() {
     Svc.Obs.remove("weave:ui:clear-error", onClearError);
 
     do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
@@ -779,27 +770,28 @@ add_test(function test_sync_server_maint
 });
 
 add_test(function test_info_collections_login_server_maintenance_error() {
   // Test info/collections server maintenance errors are not reported.
   let server = sync_httpd_setup();
   setUp();
 
   Service.username = "broken.info";
+  setBasicCredentials("broken.info", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
 
   function onUIUpdate() {
-    do_throw("Shouldn't get here!");
+    do_throw("Shouldn't experience UI update!");
   }
   Svc.Obs.add("weave:ui:login:error", onUIUpdate);
 
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.service, STATUS_OK);
 
   Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
     Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
@@ -818,17 +810,17 @@ add_test(function test_info_collections_
   Service.sync();
 });
 
 add_test(function test_meta_global_login_server_maintenance_error() {
   // Test meta/global server maintenance errors are not reported.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.meta";
+  setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -858,19 +850,20 @@ add_test(function test_meta_global_login
   Service.sync();
 });
 
 add_test(function test_crypto_keys_login_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are not reported.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.keys";
+  setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
+
   // Force re-download of keys
   CollectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -926,17 +919,17 @@ add_test(function test_sync_prolonged_se
   Service.sync();
 });
 
 add_test(function test_info_collections_login_prolonged_server_maintenance_error(){
   // Test info/collections prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.info";
+  setBasicCredentials("broken.info", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -959,17 +952,17 @@ add_test(function test_info_collections_
   Service.sync();
 });
 
 add_test(function test_meta_global_login_prolonged_server_maintenance_error(){
   // Test meta/global prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.meta";
+  setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -992,17 +985,17 @@ add_test(function test_meta_global_login
   Service.sync();
 });
 
 add_test(function test_download_crypto_keys_login_prolonged_server_maintenance_error(){
   // Test crypto/keys prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.keys";
+  setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
   CollectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
@@ -1027,19 +1020,17 @@ add_test(function test_download_crypto_k
   Service.sync();
 });
 
 add_test(function test_upload_crypto_keys_login_prolonged_server_maintenance_error(){
   // Test crypto/keys prolonged server maintenance errors are reported.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
-  Service.username = "broken.keys";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.keys", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1063,19 +1054,17 @@ add_test(function test_upload_crypto_key
 });
 
 add_test(function test_wipeServer_login_prolonged_server_maintenance_error(){
   // Test that we report prolonged server maintenance errors that occur whilst
   // wiping the server.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
-  Service.username = "broken.wipe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1098,19 +1087,18 @@ add_test(function test_wipeServer_login_
   Service.sync();
 });
 
 add_test(function test_wipeRemote_prolonged_server_maintenance_error(){
   // Test that we report prolonged server maintenance errors that occur whilst
   // wiping all remote devices.
   let server = sync_httpd_setup();
 
-  Service.username = "broken.wipe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  server.registerPathHandler("/1.1/broken.wipe/storage/catapult", service_unavailable);
+  setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   generateAndUploadKeys();
 
   let engine = Engines.get("catapult");
   engine.exception = null;
   engine.enabled = true;
 
@@ -1168,17 +1156,17 @@ add_test(function test_sync_syncAndRepor
 });
 
 add_test(function test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
   // Test info/collections server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.info";
+  setBasicCredentials("broken.info", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1202,17 +1190,17 @@ add_test(function test_info_collections_
 });
 
 add_test(function test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
   // Test meta/global server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.meta";
+  setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1236,17 +1224,17 @@ add_test(function test_meta_global_login
 });
 
 add_test(function test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.keys";
+  setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
   CollectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
@@ -1272,19 +1260,17 @@ add_test(function test_download_crypto_k
 });
 
 add_test(function test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
-  Service.username = "broken.keys";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.keys", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1308,19 +1294,17 @@ add_test(function test_upload_crypto_key
 });
 
 add_test(function test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
-  Service.username = "broken.wipe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1343,19 +1327,17 @@ add_test(function test_wipeServer_login_
   ErrorHandler.syncAndReportErrors();
 });
 
 add_test(function test_wipeRemote_syncAndReportErrors_server_maintenance_error(){
   // Test that we report prolonged server maintenance errors that occur whilst
   // wiping all remote devices.
   let server = sync_httpd_setup();
 
-  Service.username = "broken.wipe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   generateAndUploadKeys();
 
   let engine = Engines.get("catapult");
   engine.exception = null;
   engine.enabled = true;
 
@@ -1413,17 +1395,17 @@ add_test(function test_sync_syncAndRepor
 });
 
 add_test(function test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test info/collections server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.info";
+  setBasicCredentials("broken.info", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1447,17 +1429,17 @@ add_test(function test_info_collections_
 });
 
 add_test(function test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test meta/global server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.meta";
+  setBasicCredentials("broken.meta", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1481,17 +1463,17 @@ add_test(function test_meta_global_login
 });
 
 add_test(function test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
   setUp();
 
-  Service.username = "broken.keys";
+  setBasicCredentials("broken.keys", "irrelevant", "irrelevant");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
   // Force re-download of keys
   CollectionKeys.clear();
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
@@ -1517,19 +1499,17 @@ add_test(function test_download_crypto_k
 });
 
 add_test(function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
-  Service.username = "broken.keys";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.keys", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
@@ -1553,19 +1533,17 @@ add_test(function test_upload_crypto_key
 });
 
 add_test(function test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
   // Test crypto/keys server maintenance errors are reported
   // when calling syncAndReportErrors.
   let server = sync_httpd_setup();
 
   // Start off with an empty account, do not upload a key.
-  Service.username = "broken.wipe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("broken.wipe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_MAINTENANCE_URL;
   Service.clusterURL = TEST_MAINTENANCE_URL;
 
   let backoffInterval;
   Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
     Svc.Obs.remove("weave:service:backoff:interval", observe);
     backoffInterval = subject;
   });
--- a/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
+++ b/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
@@ -43,28 +43,26 @@ function sync_httpd_setup() {
     "/1.1/johndoe/storage/meta/global": upd("meta",    globalWBO.handler()),
     "/1.1/johndoe/storage/clients":     upd("clients", clientsColl.handler()),
     "/1.1/johndoe/storage/crypto/keys": upd("crypto",  keysWBO.handler())
   };
   return httpd_setup(handlers);
 }
 
 function setUp() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "aabcdeabcdeabcdeabcdeabcde";
+  setBasicCredentials("johndoe", "ilovejane", "aabcdeabcdeabcdeabcdeabcde");
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   new FakeCryptoService();
 }
 
 function generateAndUploadKeys() {
   generateNewKeys();
   let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Weave.Service.syncKeyBundle);
+  serverKeys.encrypt(Weave.Identity.syncKeyBundle);
   return serverKeys.upload("http://localhost:8080/1.1/johndoe/storage/crypto/keys").success;
 }
 
 
 add_test(function test_backoff500() {
   _("Test: HTTP 500 sets backoff status.");
   setUp();
   let server = sync_httpd_setup();
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -1,25 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/history.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/util.js");
 
-var syncTesting = new SyncTestingInfrastructure();
-
 add_test(function test_processIncoming_mobile_history_batched() {
   _("SyncEngine._processIncoming works on history engine.");
 
   let FAKE_DOWNLOAD_LIMIT = 100;
-  
-  Svc.Prefs.set("serverURL", TEST_SERVER_URL);
-  Svc.Prefs.set("clusterURL", TEST_CLUSTER_URL);
-  Svc.Prefs.set("username", "foo");
+
+  new SyncTestingInfrastructure();
+
   Svc.Prefs.set("client.type", "mobile");
   PlacesUtils.history.removeAllPages();
   Engines.register(HistoryEngine);
 
   // A collection that logs each GET
   let collection = new ServerCollection();
   collection.get_log = [];
   collection._get = collection.get;
--- a/services/sync/tests/unit/test_hmac_error.js
+++ b/services/sync/tests/unit/test_hmac_error.js
@@ -13,21 +13,19 @@ let hmacErrorCount = 0;
     return hHE.call(Service);
   };
 })();
 
 function shared_setup() {
   hmacErrorCount = 0;
 
   // Do not instantiate SyncTestingInfrastructure; we need real crypto.
+  setBasicCredentials("foo", "foo", "aabcdeabcdeabcdeabcdeabcde");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
-  Service.username   = "foo";
-  Service.password   = "foo";
-  Service.passphrase = "aabcdeabcdeabcdeabcdeabcde";
 
   // Make sure RotaryEngine is the only one we sync.
   Engines._engines = {};
   Engines.register(RotaryEngine);
   let engine = Engines.get("rotary");
   engine.enabled = true;
   engine.lastSync = 123; // Needs to be non-zero so that tracker is queried.
   engine._store.items = {flying: "LNER Class A3 4472",
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_identity_manager.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/identity.js");
+
+function run_test() {
+  initTestLogging("Trace");
+  Log4Moz.repository.getLogger("Sync.Identity").level =
+    Log4Moz.Level.Trace;
+
+  run_next_test();
+}
+
+add_test(function test_username_from_account() {
+  _("Ensure usernameFromAccount works properly.");
+
+  do_check_eq(Identity.usernameFromAccount(null), null);
+  do_check_eq(Identity.usernameFromAccount("user"), "user");
+  do_check_eq(Identity.usernameFromAccount("User"), "user");
+  do_check_eq(Identity.usernameFromAccount("john@doe.com"),
+                                           "7wohs32cngzuqt466q3ge7indszva4of");
+
+  run_next_test();
+});
+
+add_test(function test_account_username() {
+  _("Ensure the account and username attributes work properly.");
+
+  _("Verify initial state");
+  do_check_eq(Svc.Prefs.get("account"), undefined);
+  do_check_eq(Svc.Prefs.get("username"), undefined);
+  do_check_eq(Identity.account, null);
+  do_check_eq(Identity.username, null);
+
+  _("The 'username' attribute is normalized to lower case, updates preferences and identities.");
+  Identity.username = "TarZan";
+  do_check_eq(Identity.username, "tarzan");
+  do_check_eq(Svc.Prefs.get("username"), "tarzan");
+  do_check_eq(Identity.username, "tarzan");
+
+  _("If not set, the 'account attribute' falls back to the username for backwards compatibility.");
+  do_check_eq(Identity.account, "tarzan");
+
+  _("Setting 'username' to a non-truthy value resets the pref.");
+  Identity.username = null;
+  do_check_eq(Identity.username, null);
+  do_check_eq(Identity.account, null);
+  const default_marker = {};
+  do_check_eq(Svc.Prefs.get("username", default_marker), default_marker);
+  do_check_eq(Identity.username, null);
+
+  _("The 'account' attribute will set the 'username' if it doesn't contain characters that aren't allowed in the username.");
+  Identity.account = "johndoe";
+  do_check_eq(Identity.account, "johndoe");
+  do_check_eq(Identity.username, "johndoe");
+  do_check_eq(Svc.Prefs.get("username"), "johndoe");
+  do_check_eq(Identity.username, "johndoe");
+
+  _("If 'account' contains disallowed characters such as @, 'username' will the base32 encoded SHA1 hash of 'account'");
+  Identity.account = "John@Doe.com";
+  do_check_eq(Identity.account, "john@doe.com");
+  do_check_eq(Identity.username, "7wohs32cngzuqt466q3ge7indszva4of");
+
+  _("Setting 'account' to a non-truthy value resets the pref.");
+  Identity.account = null;
+  do_check_eq(Identity.account, null);
+  do_check_eq(Svc.Prefs.get("account", default_marker), default_marker);
+  do_check_eq(Identity.username, null);
+  do_check_eq(Svc.Prefs.get("username", default_marker), default_marker);
+
+  Svc.Prefs.resetBranch("");
+  run_next_test();
+});
+
+add_test(function test_basic_password() {
+  _("Ensure basic password setting works as expected.");
+
+  Identity.account = null;
+  do_check_eq(Identity.currentAuthState, LOGIN_FAILED_NO_USERNAME);
+  let thrown = false;
+  try {
+    Identity.basicPassword = "foobar";
+  } catch (ex) {
+    thrown = true;
+  }
+
+  do_check_true(thrown);
+  thrown = false;
+
+  Identity.account = "johndoe";
+  do_check_eq(Identity.currentAuthState, LOGIN_FAILED_NO_PASSWORD);
+  Identity.basicPassword = "password";
+  do_check_eq(Identity.basicPassword, "password");
+  do_check_eq(Identity.currentAuthState, LOGIN_FAILED_NO_PASSPHRASE);
+  do_check_true(Identity.hasBasicCredentials());
+
+  Identity.account = null;
+
+  run_next_test();
+});
+
+add_test(function test_basic_password_persistence() {
+  _("Ensure credentials are saved and restored to the login manager properly.");
+
+  // Just in case.
+  Identity.account = null;
+  Identity.deleteSyncCredentials();
+
+  Identity.account = "janesmith";
+  Identity.basicPassword = "ilovejohn";
+  Identity.persistCredentials();
+
+  let im1 = new IdentityManager();
+  do_check_eq(im1._basicPassword, null);
+  do_check_eq(im1.username, "janesmith");
+  do_check_eq(im1.basicPassword, "ilovejohn");
+
+  let im2 = new IdentityManager();
+  do_check_eq(im2._basicPassword, null);
+
+  _("Now remove the password and ensure it is deleted from storage.");
+  Identity.basicPassword = null;
+  Identity.persistCredentials(); // This should nuke from storage.
+  do_check_eq(im2.basicPassword, null);
+
+  _("Ensure that retrieving an unset but unpersisted removal returns null.");
+  Identity.account = "janesmith";
+  Identity.basicPassword = "myotherpassword";
+  Identity.persistCredentials();
+
+  Identity.basicPassword = null;
+  do_check_eq(Identity.basicPassword, null);
+
+  // Reset for next test.
+  Identity.account = null;
+  Identity.persistCredentials();
+
+  run_next_test();
+});
+
+add_test(function test_sync_key() {
+  _("Ensure Sync Key works as advertised.");
+
+  _("Ensure setting a Sync Key before an account throws.");
+  let thrown = false;
+  try {
+    Identity.syncKey = "blahblah";
+  } catch (ex) {
+    thrown = true;
+  }
+  do_check_true(thrown);
+  thrown = false;
+
+  Identity.account = "johnsmith";
+  Identity.basicPassword = "johnsmithpw";
+
+  do_check_eq(Identity.syncKey, null);
+  do_check_eq(Identity.syncKeyBundle, null);
+
+  _("An invalid Sync Key is silently accepted for historical reasons.");
+  Identity.syncKey = "synckey";
+  do_check_eq(Identity.syncKey, "synckey");
+
+  _("But the SyncKeyBundle should not be created from bad keys.");
+  do_check_eq(Identity.syncKeyBundle, null);
+
+  let syncKey = Utils.generatePassphrase();
+  Identity.syncKey = syncKey;
+  do_check_eq(Identity.syncKey, syncKey);
+  do_check_neq(Identity.syncKeyBundle, null);
+
+  let im = new IdentityManager();
+  im.account = "pseudojohn";
+  do_check_eq(im.syncKey, null);
+  do_check_eq(im.syncKeyBundle, null);
+
+  Identity.account = null;
+
+  run_next_test();
+});
+
+add_test(function test_sync_key_persistence() {
+  _("Ensure Sync Key persistence works as expected.");
+
+  Identity.account = "pseudojohn";
+  Identity.password = "supersecret";
+
+  let syncKey = Utils.generatePassphrase();
+  Identity.syncKey = syncKey;
+
+  Identity.persistCredentials();
+
+  let im = new IdentityManager();
+  im.account = "pseudojohn";
+  do_check_eq(im.syncKey, syncKey);
+  do_check_neq(im.syncKeyBundle, null);
+
+  let kb1 = Identity.syncKeyBundle;
+  let kb2 = im.syncKeyBundle;
+
+  do_check_eq(kb1.encryptionKeyB64, kb2.encryptionKeyB64);
+  do_check_eq(kb1.hmacKeyB64, kb2.hmacKeyB64);
+
+  Identity.account = null;
+  Identity.persistCredentials();
+
+  let im2 = new IdentityManager();
+  im2.account = "pseudojohn";
+  do_check_eq(im2.syncKey, null);
+
+  im2.account = null;
+
+  _("Ensure deleted but not persisted value is retrieved.");
+  Identity.account = "someoneelse";
+  Identity.syncKey = Utils.generatePassphrase();
+  Identity.persistCredentials();
+  Identity.syncKey = null;
+  do_check_eq(Identity.syncKey, null);
+
+  // Clean up.
+  Identity.account = null;
+  Identity.persistCredentials();
+
+  run_next_test();
+});
--- a/services/sync/tests/unit/test_interval_triggers.js
+++ b/services/sync/tests/unit/test_interval_triggers.js
@@ -27,25 +27,23 @@ function sync_httpd_setup() {
     "/1.1/johndoe/info/collections": collectionsHelper.handler,
     "/1.1/johndoe/storage/crypto/keys":
       upd("crypto", (new ServerWBO("keys")).handler()),
     "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler())
   });
 }
 
 function setUp() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   generateNewKeys();
   let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Service.syncKeyBundle);
+  serverKeys.encrypt(Identity.syncKeyBundle);
   return serverKeys.upload(Service.cryptoKeysURL);
 }
 
 function run_test() {
   initTestLogging("Trace");
 
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
--- a/services/sync/tests/unit/test_jpakeclient.js
+++ b/services/sync/tests/unit/test_jpakeclient.js
@@ -178,19 +178,17 @@ function run_test() {
     Svc.Prefs.resetBranch("");
   });
 
   // Ensure PSM is initialized.
   Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
 
   // Simulate Sync setup with credentials in place. We want to make
   // sure the J-PAKE requests don't include those data.
-  let id = new Identity(PWDMGR_PASSWORD_REALM, "johndoe");
-  id.password = "ilovejane";
-  ID.set("WeaveID", id);
+  setBasicCredentials("johndoe", "ilovejane");
 
   server = httpd_setup({"/new_channel": server_new_channel,
                         "/report":      server_report});
 
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
   run_next_test();
--- a/services/sync/tests/unit/test_keys.js
+++ b/services/sync/tests/unit/test_keys.js
@@ -1,260 +1,327 @@
-var btoa;
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
-btoa = Cu.import("resource://services-sync/util.js").btoa;
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/keys.js");
 
 function sha256HMAC(message, key) {
   let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key);
   return Utils.digestBytes(message, h);
 }
 
-function test_time_keyFromString(iterations) {
-  let k;
-  let o;
-  let b = new BulkKeyBundle();
-  let d = Utils.decodeKeyBase32("ababcdefabcdefabcdefabcdef");
-  b.generateRandom();
-  
-  _("Running " + iterations + " iterations of hmacKeyObject + sha256HMAC.");
-  for (let i = 0; i < iterations; ++i) {
-    let k = b.hmacKeyObject;
-    o = sha256HMAC(d, k);
-  }
-  do_check_true(!!o);
-  _("Done.");
-}
-
-function test_repeated_hmac() {
-  let testKey = "ababcdefabcdefabcdefabcdef";
-  let k = Utils.makeHMACKey("foo");
-  let one = sha256HMAC(Utils.decodeKeyBase32(testKey), k);
-  let two = sha256HMAC(Utils.decodeKeyBase32(testKey), k);
-  do_check_eq(one, two);
-}
-
 function do_check_array_eq(a1, a2) {
   do_check_eq(a1.length, a2.length);
   for (let i = 0; i < a1.length; ++i) {
     do_check_eq(a1[i], a2[i]);
   }
 }
 
-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 = "" + HMAC_INPUT + username + "\x01";
-  let key = Utils.makeHMACKey(Utils.decodeKeyBase32(testKey));
-  let encryptKey = sha256HMAC(sha256inputE, key);
-  
-  let sha256inputH = encryptKey + HMAC_INPUT + username + "\x02";
-  let hmacKey = sha256HMAC(sha256inputH, key);
-  
-  // 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() {
+function test_time_keyFromString(iterations) {
+  let k;
+  let o;
+  let b = new BulkKeyBundle("dummy");
+  let d = Utils.decodeKeyBase32("ababcdefabcdefabcdefabcdef");
+  b.generateRandom();
+
+  _("Running " + iterations + " iterations of hmacKeyObject + sha256HMAC.");
+  for (let i = 0; i < iterations; ++i) {
+    let k = b.hmacKeyObject;
+    o = sha256HMAC(d, k);
+  }
+  do_check_true(!!o);
+  _("Done.");
+}
+
+add_test(function test_set_invalid_values() {
+  _("Ensure that setting invalid encryption and HMAC key values is caught.");
+
+  let bundle = new BulkKeyBundle("foo");
+
+  let thrown = false;
+  try {
+    bundle.encryptionKey = null;
+  } catch (ex) {
+    thrown = true;
+    do_check_eq(ex.message.indexOf("Encryption key can only be set to"), 0);
+  } finally {
+    do_check_true(thrown);
+    thrown = false;
+  }
+
+  try {
+    bundle.encryptionKey = ["trollololol"];
+  } catch (ex) {
+    thrown = true;
+    do_check_eq(ex.message.indexOf("Encryption key can only be set to"), 0);
+  } finally {
+    do_check_true(thrown);
+    thrown = false;
+  }
+
+  try {
+    bundle.hmacKey = Utils.generateRandomBytes(15);
+  } catch (ex) {
+    thrown = true;
+    do_check_eq(ex.message.indexOf("HMAC key must be at least 128"), 0);
+  } finally {
+    do_check_true(thrown);
+    thrown = false;
+  }
+
+  try {
+    bundle.hmacKey = null;
+  } catch (ex) {
+    thrown = true;
+    do_check_eq(ex.message.indexOf("HMAC key can only be set to string"), 0);
+  } finally {
+    do_check_true(thrown);
+    thrown = false;
+  }
+
+  try {
+    bundle.hmacKey = ["trollolol"];
+  } catch (ex) {
+    thrown = true;
+    do_check_eq(ex.message.indexOf("HMAC key can only be set to"), 0);
+  } finally {
+    do_check_true(thrown);
+    thrown = false;
+  }
+
+  try {
+    bundle.hmacKey = Utils.generateRandomBytes(15);
+  } catch (ex) {
+    thrown = true;
+    do_check_eq(ex.message.indexOf("HMAC key must be at least 128"), 0);
+  } finally {
+    do_check_true(thrown);
+    thrown = false;
+  }
+
+  run_next_test();
+});
+
+add_test(function test_repeated_hmac() {
+  let testKey = "ababcdefabcdefabcdefabcdef";
+  let k = Utils.makeHMACKey("foo");
+  let one = sha256HMAC(Utils.decodeKeyBase32(testKey), k);
+  let two = sha256HMAC(Utils.decodeKeyBase32(testKey), k);
+  do_check_eq(one, two);
+
+  run_next_test();
+});
+
+add_test(function test_sync_key_bundle_derivation() {
+  _("Ensure derivation from known values works.");
+
+  // The known values in this test were originally verified against Firefox
+  // Home.
+  let bundle = new SyncKeyBundle("st3fan", "q7ynpwq7vsc9m34hankbyi3s3i");
+
+  // These should be compared to the results from Home, as they once were.
+  let e = "14b8c09fa84e92729ee695160af6e0385f8f6215a25d14906e1747bdaa2de426";
+  let h = "370e3566245d79fe602a3adb5137e42439cd2a571235197e0469d7d541b07875";
+
+  let realE = Utils.bytesAsHex(bundle.encryptionKey);
+  let realH = Utils.bytesAsHex(bundle.hmacKey);
+
+  _("Real E: " + realE);
+  _("Real H: " + realH);
+  do_check_eq(realH, h);
+  do_check_eq(realE, e);
+
+  run_next_test();
+});
+
+add_test(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 = "" + HMAC_INPUT + username + "\x01";
+  let key = Utils.makeHMACKey(Utils.decodeKeyBase32(testKey));
+  let encryptKey = sha256HMAC(sha256inputE, key);
+
+  let sha256inputH = encryptKey + HMAC_INPUT + username + "\x02";
+  let hmacKey = sha256HMAC(sha256inputH, key);
+
+  // Encryption key is stored in base64 for WeaveCrypto convenience.
+  do_check_eq(encryptKey, new SyncKeyBundle(username, testKey).encryptionKey);
+  do_check_eq(hmacKey,    new SyncKeyBundle(username, testKey).hmacKey);
+
+  // Test with the same KeyBundle for both.
+  let obj = new SyncKeyBundle(username, testKey);
+  do_check_eq(hmacKey, obj.hmacKey);
+  do_check_eq(encryptKey, obj.encryptionKey);
+
+  run_next_test();
+});
+
+add_test(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"));
-  
+
+  Identity.account = "john@example.com";
+  Identity.syncKey = "a-bbbbb-ccccc-ddddd-eeeee-fffff";
+
+  let keyBundle = Identity.syncKeyBundle;
+
   /*
    * 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, releasing the payload for us to use.
   // Returns true, because the default key has changed.
   do_check_true(CollectionKeys.updateContents(keyBundle, storage_keys));
   let payload = storage_keys.cleartext;
-  
+
   _("CK: " + JSON.stringify(CollectionKeys._collections));
-  
+
   // Test that the CollectionKeyManager returns a similar WBO.
   let wbo = CollectionKeys.asWBO("crypto", "keys");
-  
+
   _("WBO: " + JSON.stringify(wbo));
   _("WBO cleartext: " + JSON.stringify(wbo.cleartext));
-  
+
   // Check the individual contents.
   do_check_eq(wbo.collection, "crypto");
   do_check_eq(wbo.id, "keys");
   do_check_eq(undefined, wbo.modified);
   do_check_eq(CollectionKeys.lastModified, storage_keys.modified);
   do_check_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);
-  
+
   _("Updating contents twice with the same data doesn't proceed.");
   storage_keys.encrypt(keyBundle);
   do_check_false(CollectionKeys.updateContents(keyBundle, storage_keys));
-  
+
   /*
    * Test that we get the right keys out when we ask for
    * a collection's tokens.
    */
-  let b1 = new BulkKeyBundle(null, "bookmarks");
-  b1.keyPair = [bookmarks_key64, bookmarks_hmac64];
+  let b1 = new BulkKeyBundle("bookmarks");
+  b1.keyPairB64 = [bookmarks_key64, bookmarks_hmac64];
   let b2 = CollectionKeys.keyForCollection("bookmarks");
   do_check_keypair_eq(b1.keyPair, b2.keyPair);
-  
+
   // Check key equality.
   do_check_true(b1.equals(b2));
   do_check_true(b2.equals(b1));
-  
-  b1 = new BulkKeyBundle(null, "[default]");
-  b1.keyPair = [default_key64, default_hmac64];
-  
+
+  b1 = new BulkKeyBundle("[default]");
+  b1.keyPairB64 = [default_key64, default_hmac64];
+
   do_check_false(b1.equals(b2));
   do_check_false(b2.equals(b1));
-  
+
   b2 = CollectionKeys.keyForCollection(null);
   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({}));
-  
+
   /*
    * Check _compareKeyBundleCollections.
    */
   function newBundle(name) {
-    let r = new BulkKeyBundle(null, name);
+    let r = new BulkKeyBundle(name);
     r.generateRandom();
     return r;
   }
   let k1 = newBundle("k1");
   let k2 = newBundle("k2");
   let k3 = newBundle("k3");
   let k4 = newBundle("k4");
   let k5 = newBundle("k5");
   let coll1 = {"foo": k1, "bar": k2};
   let coll2 = {"foo": k1, "bar": k2};
   let coll3 = {"foo": k1, "bar": k3};
   let coll4 = {"foo": k4};
   let coll5 = {"baz": k5, "bar": k2};
   let coll6 = {};
-  
+
   let d1 = CollectionKeys._compareKeyBundleCollections(coll1, coll2); // []
   let d2 = CollectionKeys._compareKeyBundleCollections(coll1, coll3); // ["bar"]
   let d3 = CollectionKeys._compareKeyBundleCollections(coll3, coll2); // ["bar"]
   let d4 = CollectionKeys._compareKeyBundleCollections(coll1, coll4); // ["bar", "foo"]
   let d5 = CollectionKeys._compareKeyBundleCollections(coll5, coll2); // ["baz", "foo"]
   let d6 = CollectionKeys._compareKeyBundleCollections(coll6, coll1); // ["bar", "foo"]
   let d7 = CollectionKeys._compareKeyBundleCollections(coll5, coll5); // []
   let d8 = CollectionKeys._compareKeyBundleCollections(coll6, coll6); // []
-  
+
   do_check_true(d1.same);
   do_check_false(d2.same);
   do_check_false(d3.same);
   do_check_false(d4.same);
   do_check_false(d5.same);
   do_check_false(d6.same);
   do_check_true(d7.same);
   do_check_true(d8.same);
-  
+
   do_check_array_eq(d1.changed, []);
   do_check_array_eq(d2.changed, ["bar"]);
   do_check_array_eq(d3.changed, ["bar"]);
   do_check_array_eq(d4.changed, ["bar", "foo"]);
   do_check_array_eq(d5.changed, ["baz", "foo"]);
   do_check_array_eq(d6.changed, ["bar", "foo"]);
-}
 
-// Make sure that KeyBundles work when persisted through Identity.
-function test_key_persistence() {
-  _("Testing key persistence.");
-  
-  // Create our sync key bundle and persist it.
-  let k = new SyncKeyBundle(null, null, "abcdeabcdeabcdeabcdeabcdea");
-  k.username = "john@example.com";
-  ID.set("WeaveCryptoID", k);
-  let id = ID.get("WeaveCryptoID");
-  do_check_eq(k, id);
-  id.persist();
-  
-  // Now erase any memory of it.
-  ID.del("WeaveCryptoID");
-  k = id = null;
-  
-  // Now recreate via the persisted value.
-  id = new SyncKeyBundle();
-  id.username = "john@example.com";
-  
-  // The password should have been fetched from storage...
-  do_check_eq(id.password, "abcdeabcdeabcdeabcdeabcdea");
-  
-  // ... and we should be able to grab these by derivation.
-  do_check_true(!!id.hmacKeyObject);
-  do_check_true(!!id.hmacKey);
-  do_check_true(!!id.encryptionKey);
-}
+  run_next_test();
+});
 
 function run_test() {
-  test_keymanager();
-  test_collections_manager();
-  test_key_persistence();
-  test_repeated_hmac();
-  
   // Only do 1,000 to avoid a 5-second pause in test runs.
   test_time_keyFromString(1000);
+
+  run_next_test();
 }
--- a/services/sync/tests/unit/test_load_modules.js
+++ b/services/sync/tests/unit/test_load_modules.js
@@ -10,16 +10,17 @@ const modules = [
                  "engines/passwords.js",
                  "engines/prefs.js",
                  "engines/tabs.js",
                  "engines.js",
                  "ext/Observers.js",
                  "ext/Preferences.js",
                  "identity.js",
                  "jpakeclient.js",
+                 "keys.js",
                  "log4moz.js",
                  "main.js",
                  "notifications.js",
                  "policies.js",
                  "record.js",
                  "resource.js",
                  "rest.js",
                  "service.js",
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -68,19 +68,17 @@ function installNodeHandler(server, next
     Utils.nextTick(next);
   }
   let nodePath = "/user/1.0/johndoe/node/weave";
   server.server.registerPathHandler(nodePath, handleNodeRequest);
   _("Registered node handler at " + nodePath);
 }
 
 function prepareServer() {
-  Service.username   = "johndoe";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
-  Service.password   = "ilovejane";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.serverURL  = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   do_check_eq(Service.userAPI, "http://localhost:8080/user/1.0/");
   let server = new SyncServer();
   server.registerUser("johndoe");
   server.start();
   return server;
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -1,13 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/identity.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 
 let cryptoWrap;
 
 function crypted_resource_handler(metadata, response) {
   let obj = {id: "resource",
              modified: cryptoWrap.modified,
              payload: JSON.stringify(cryptoWrap.payload)};
@@ -21,35 +25,33 @@ function prepareCryptoWrap(collection, i
   w.id = id;
   return w;
 }
 
 function run_test() {
   let server;
   do_test_pending();
 
-  let keyBundle = ID.set("WeaveCryptoID", new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, "john@example.com"));
-  keyBundle.keyStr = "a-abcde-abcde-abcde-abcde-abcde";
+  Identity.username = "john@example.com";
+  Identity.syncKey = "a-abcde-abcde-abcde-abcde-abcde";
+  let keyBundle = Identity.syncKeyBundle;
 
   try {
     let log = Log4Moz.repository.getLogger("Test");
     Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
     log.info("Setting up server and authenticator");
 
     server = httpd_setup({"/steam/resource": crypted_resource_handler});
 
-    let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
-    Auth.defaultAuthenticator = auth;
-
     log.info("Creating a record");
 
     let cryptoUri = "http://localhost:8080/crypto/steam";
     cryptoWrap = prepareCryptoWrap("steam", "resource");
-    
+
     log.info("cryptoWrap: " + cryptoWrap.toString());
 
     log.info("Encrypting a record");
 
     cryptoWrap.encrypt(keyBundle);
     log.info("Ciphertext is " + cryptoWrap.ciphertext);
     do_check_true(cryptoWrap.ciphertext != null);
     
@@ -102,52 +104,48 @@ function run_test() {
       cryptoWrap.decrypt(keyBundle);
     }
     catch(ex) {
       error = ex;
     }
     do_check_eq(error.substr(0, 42), "Record SHA256 HMAC mismatch: should be foo");
 
     // Checking per-collection keys and default key handling.
-    
+
     generateNewKeys();
     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".
     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 {
deleted file mode 100644
--- a/services/sync/tests/unit/test_records_crypto_generateEntry.js
+++ /dev/null
@@ -1,26 +0,0 @@
-let atob = Cu.import("resource://services-sync/util.js").atob;
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/record.js");
-
-/**
- * Testing the SHA256-HMAC key derivation process against test vectors
- * verified with the Firefox Home implementation.
- */
-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 = "14b8c09fa84e92729ee695160af6e0385f8f6215a25d14906e1747bdaa2de426";
-  let h = "370e3566245d79fe602a3adb5137e42439cd2a571235197e0469d7d541b07875";
-  
-  // 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);
-}
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/ext/Observers.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/util.js");
 
 let logger;
 
@@ -141,16 +144,18 @@ function server_headers(metadata, respon
     headers[header] = metadata.getHeader(header);
   }
   let body = JSON.stringify(headers);
   response.setStatusLine(metadata.httpVersion, 200, "OK");
   response.bodyOutputStream.write(body, body.length);
 }
 
 function run_test() {
+  initTestLogging("Trace");
+
   do_test_pending();
 
   logger = Log4Moz.repository.getLogger('Test');
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   let server = httpd_setup({
     "/open": server_open,
     "/protected": server_protected,
@@ -222,40 +227,28 @@ function run_test() {
   do_check_eq(debugMessages.length, 1);
   do_check_eq(debugMessages[0],
               "Parse fail: Response body starts: \"\"This path exists\"\".");
   logger.debug = dbg;
 
   _("Test that the BasicAuthenticator doesn't screw up header case.");
   let res1 = new Resource("http://localhost:8080/foo");
   res1.setHeader("Authorization", "Basic foobar");
-  res1.authenticator = new NoOpAuthenticator();
-  do_check_eq(res1._headers["authorization"], "Basic foobar");
   do_check_eq(res1.headers["authorization"], "Basic foobar");
-  let id = new Identity("secret", "guest", "guest");
-  res1.authenticator = new BasicAuthenticator(id);
-
-  // In other words... it correctly overwrites our downcased version
-  // when accessed through .headers.
-  do_check_eq(res1._headers["authorization"], "Basic foobar");
-  do_check_eq(res1.headers["authorization"], "Basic Z3Vlc3Q6Z3Vlc3Q=");
-  do_check_eq(res1._headers["authorization"], "Basic Z3Vlc3Q6Z3Vlc3Q=");
-  do_check_true(!res1._headers["Authorization"]);
-  do_check_true(!res1.headers["Authorization"]);
 
   _("GET a password protected resource (test that it'll fail w/o pass, no throw)");
   let res2 = new Resource("http://localhost:8080/protected");
   content = res2.get();
   do_check_eq(content, "This path exists and is protected - failed");
   do_check_eq(content.status, 401);
   do_check_false(content.success);
 
   _("GET a password protected resource");
-  let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
   let res3 = new Resource("http://localhost:8080/protected");
+  let auth = Identity.getBasicResourceAuthenticator("guest", "guest");
   res3.authenticator = auth;
   do_check_eq(res3.authenticator, auth);
   content = res3.get();
   do_check_eq(content, "This path exists and is protected");
   do_check_eq(content.status, 200);
   do_check_true(content.success);
 
   _("GET a non-existent resource (test that it'll fail, but not throw)");
--- a/services/sync/tests/unit/test_resource_async.js
+++ b/services/sync/tests/unit/test_resource_async.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/ext/Observers.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/util.js");
 
 const RES_UPLOAD_URL = "http://localhost:8080/upload";
 const RES_HEADERS_URL = "http://localhost:8080/headers";
@@ -256,29 +259,18 @@ add_test(function test_get() {
     run_next_test();
   });
 });
 
 add_test(function test_basicauth() {
   _("Test that the BasicAuthenticator doesn't screw up header case.");
   let res1 = new AsyncResource("http://localhost:8080/foo");
   res1.setHeader("Authorization", "Basic foobar");
-  res1.authenticator = new NoOpAuthenticator();
   do_check_eq(res1._headers["authorization"], "Basic foobar");
   do_check_eq(res1.headers["authorization"], "Basic foobar");
-  let id = new Identity("secret", "guest", "guest");
-  res1.authenticator = new BasicAuthenticator(id);
-
-  // In other words... it correctly overwrites our downcased version
-  // when accessed through .headers.
-  do_check_eq(res1._headers["authorization"], "Basic foobar");
-  do_check_eq(res1.headers["authorization"], "Basic Z3Vlc3Q6Z3Vlc3Q=");
-  do_check_eq(res1._headers["authorization"], "Basic Z3Vlc3Q6Z3Vlc3Q=");
-  do_check_true(!res1._headers["Authorization"]);
-  do_check_true(!res1.headers["Authorization"]);
 
   run_next_test();
 });
 
 add_test(function test_get_protected_fail() {
   _("GET a password protected resource (test that it'll fail w/o pass, no throw)");
   let res2 = new AsyncResource("http://localhost:8080/protected");
   res2.get(function (error, content) {
@@ -287,17 +279,17 @@ add_test(function test_get_protected_fai
     do_check_eq(content.status, 401);
     do_check_false(content.success);
     run_next_test();
   });
 });
 
 add_test(function test_get_protected_success() {
   _("GET a password protected resource");
-  let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
+  let auth = Identity.getBasicResourceAuthenticator("guest", "guest");
   let res3 = new AsyncResource("http://localhost:8080/protected");
   res3.authenticator = auth;
   do_check_eq(res3.authenticator, auth);
   res3.get(function (error, content) {
     do_check_eq(error, null);
     do_check_eq(content, "This path exists and is protected");
     do_check_eq(content.status, 200);
     do_check_true(content.success);
--- a/services/sync/tests/unit/test_resource_ua.js
+++ b/services/sync/tests/unit/test_resource_ua.js
@@ -20,20 +20,19 @@ function test_resource_user_agent() {
   }
 
   do_test_pending();
   let server = httpd_setup({
     "/1.1/johndoe/info/collections": uaHandler(collectionsHelper.handler),
     "/1.1/johndoe/storage/meta/global": uaHandler(meta_global.handler()),
   });
 
+  setBasicCredentials("johndoe", "ilovejane");
   Weave.Service.serverURL  = TEST_SERVER_URL;
   Weave.Service.clusterURL = TEST_CLUSTER_URL;
-  Weave.Service.username   = "johndoe";
-  Weave.Service.password   = "ilovejane";
 
   let expectedUA = Services.appinfo.name + "/" + Services.appinfo.version +
                    " FxSync/" + WEAVE_VERSION + "." +
                    Services.appinfo.appBuildID;
 
   function test_fetchInfo(next) {
     _("Testing _fetchInfo.");
     Weave.Service._fetchInfo();
--- a/services/sync/tests/unit/test_score_triggers.js
+++ b/services/sync/tests/unit/test_score_triggers.js
@@ -38,22 +38,17 @@ function sync_httpd_setup() {
   let cl = new ServerCollection();
   handlers["/1.1/johndoe/storage/clients"] =
     upd("clients", cl.handler());
 
   return httpd_setup(handlers);
 }
 
 function setUp() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "sekrit";
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
-  new FakeCryptoService();
+  new SyncTestingInfrastructure("johndoe", "ilovejane", "sekrit");
 }
 
 function run_test() {
   initTestLogging("Trace");
 
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
 
   run_next_test();
--- a/services/sync/tests/unit/test_sendcredentials_controller.js
+++ b/services/sync/tests/unit/test_sendcredentials_controller.js
@@ -2,19 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */ 
 
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 function run_test() {
-  Service.account    = "johndoe";
-  Service.password   = "ilovejane";
-  Service.passphrase = Utils.generatePassphrase();
+  setBasicCredentials("johndoe", "ilovejane", Utils.generatePassphrase());
   Service.serverURL  = "http://weave.server/";
 
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.SendCredentialsController").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
   run_next_test();
 }
 
@@ -26,19 +24,19 @@ function make_sendCredentials_test(topic
     let jpakeclient = {
       sendAndComplete: function sendAndComplete(data) {
         // Verify that the controller unregisters itself as an observer
         // when the exchange is complete by faking another notification.
         do_check_false(sendAndCompleteCalled);
         sendAndCompleteCalled = true;
 
         // Verify it sends the correct data.
-        do_check_eq(data.account,   Service.account);
-        do_check_eq(data.password,  Service.password);
-        do_check_eq(data.synckey,   Service.passphrase);
+        do_check_eq(data.account,   Identity.account);
+        do_check_eq(data.password,  Identity.basicPassword);
+        do_check_eq(data.synckey,   Identity.syncKey);
         do_check_eq(data.serverURL, Service.serverURL);
 
         this.controller.onComplete();
         // Verify it schedules a sync for the expected interval.
         let expectedInterval = SyncScheduler.activeInterval;
         do_check_true(SyncScheduler.nextSync - Date.now() <= expectedInterval);
 
         // Signal the end of another sync. We shouldn't be registered anymore,
--- a/services/sync/tests/unit/test_service_attributes.js
+++ b/services/sync/tests/unit/test_service_attributes.js
@@ -1,80 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/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.");
-
-  try {
-    _("Verify initial state");
-    do_check_eq(Svc.Prefs.get("account"), undefined);
-    do_check_eq(Svc.Prefs.get("username"), undefined);
-    do_check_eq(ID.get("WeaveID").username, "");
-    do_check_eq(ID.get("WeaveCryptoID").username, "");
-
-    _("The 'username' attribute is normalized to lower case, updates preferences and identities.");
-    Service.username = "TarZan";
-    do_check_eq(Service.username, "tarzan");
-    do_check_eq(Svc.Prefs.get("username"), "tarzan");
-    do_check_eq(ID.get("WeaveID").username, "tarzan");
-    do_check_eq(ID.get("WeaveCryptoID").username, "tarzan");
-
-    _("If not set, the 'account attribute' falls back to the username for backwards compatibility.");
-    do_check_eq(Service.account, "tarzan");
-
-    _("Setting 'username' to a non-truthy value resets the pref.");
-    Service.username = null;
-    do_check_eq(Service.username, "");
-    do_check_eq(Service.account, "");
-    const default_marker = {};
-    do_check_eq(Svc.Prefs.get("username", default_marker), default_marker);
-    do_check_eq(ID.get("WeaveID").username, null);
-    do_check_eq(ID.get("WeaveCryptoID").username, null);
-
-    _("The 'account' attribute will set the 'username' if it doesn't contain characters that aren't allowed in the username.");
-    Service.account = "johndoe";
-    do_check_eq(Service.account, "johndoe");
-    do_check_eq(Service.username, "johndoe");
-    do_check_eq(Svc.Prefs.get("username"), "johndoe");
-    do_check_eq(ID.get("WeaveID").username, "johndoe");
-    do_check_eq(ID.get("WeaveCryptoID").username, "johndoe");
-
-    _("If 'account' contains disallowed characters such as @, 'username' will the base32 encoded SHA1 hash of 'account'");
-    Service.account = "John@Doe.com";
-    do_check_eq(Service.account, "john@doe.com");
-    do_check_eq(Service.username, "7wohs32cngzuqt466q3ge7indszva4of");
-
-    _("Setting 'account' to a non-truthy value resets the pref.");
-    Service.account = null;
-    do_check_eq(Service.account, "");
-    do_check_eq(Svc.Prefs.get("account", default_marker), default_marker);
-    do_check_eq(Service.username, "");
-    do_check_eq(Svc.Prefs.get("username", default_marker), default_marker);
-
-  } finally {
-    Svc.Prefs.resetBranch("");
-  }
-}
-
 function test_urls() {
   _("URL related Service properties corresopnd to preference settings.");
   try {
     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";
+    Identity.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);
 
     Service.serverURL = "http://weave.server/";
@@ -101,20 +50,19 @@ function test_urls() {
     Svc.Prefs.set("userURL", "http://weave.user.services/");
     do_check_eq(Service.miscAPI, "http://weave.misc.services/1.0/");
     do_check_eq(Service.userAPI, "http://weave.user.services/1.0/");
 
     do_check_eq(Service.pwResetURL,
                 "http://weave.server/weave-password-reset");
 
     _("Empty/false value for 'username' resets preference.");
-    Service.username = "";
+    Identity.username = "";
     do_check_eq(Svc.Prefs.get("username"), undefined);
-    do_check_eq(ID.get("WeaveID").username, "");
-    do_check_eq(ID.get("WeaveCryptoID").username, "");
+    do_check_eq(Identity.username, null);
 
     _("The 'serverURL' attributes updates/resets preferences.");
     // Identical value doesn't do anything
     Service.serverURL = Service.serverURL;
     do_check_eq(Svc.Prefs.get("clusterURL"), "http://weave.cluster/");
 
     Service.serverURL = "http://different.auth.node/";
     do_check_eq(Svc.Prefs.get("serverURL"), "http://different.auth.node/");
@@ -159,13 +107,12 @@ function test_locked() {
   // Locking again will return false
   do_check_eq(Service.lock(), false);
 
   Service.unlock();
   do_check_eq(Service.locked, false);
 }
 
 function run_test() {
-  test_identities();
   test_urls();
   test_syncID();
   test_locked();
 }
--- a/services/sync/tests/unit/test_service_changePassword.js
+++ b/services/sync/tests/unit/test_service_changePassword.js
@@ -1,8 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
 Cu.import("resource://services-sync/log4moz.js");
 
 function run_test() {
   initTestLogging("Trace");
@@ -23,53 +27,52 @@ add_test(function test_change_password()
       response.setStatusLine(request.httpVersion, statusCode, status);
       response.bodyOutputStream.write(body, body.length);
     };
   }
 
   try {
     Weave.Service.serverURL = TEST_SERVER_URL;
     Weave.Service.clusterURL = TEST_CLUSTER_URL;
-    Weave.Service.username = "johndoe";
-    Weave.Service.password = "ilovejane";
+    setBasicCredentials("johndoe", "ilovejane");
 
     _("changePassword() returns false for a network error, the password won't change.");
     let res = Weave.Service.changePassword("ILoveJane83");
     do_check_false(res);
-    do_check_eq(Weave.Service.password, "ilovejane");
+    do_check_eq(Identity.basicPassword, "ilovejane");
 
     _("Let's fire up the server and actually change the password.");
     server = httpd_setup({
       "/user/1.0/johndoe/password": send(200, "OK", ""),
       "/user/1.0/janedoe/password": send(401, "Unauthorized", "Forbidden!")
     });
 
     res = Weave.Service.changePassword("ILoveJane83");
     do_check_true(res);
-    do_check_eq(Weave.Service.password, "ILoveJane83");
+    do_check_eq(Identity.basicPassword, "ILoveJane83");
     do_check_eq(requestBody, "ILoveJane83");
 
     _("Make sure the password has been persisted in the login manager.");
     let logins = Services.logins.findLogins({}, PWDMGR_HOST, null,
                                             PWDMGR_PASSWORD_REALM);
+    do_check_eq(logins.length, 1);
     do_check_eq(logins[0].password, "ILoveJane83");
 
     _("A non-ASCII password is UTF-8 encoded.");
     const moneyPassword = "moneyislike$£¥";
     res = Weave.Service.changePassword(moneyPassword);
     do_check_true(res);
-    do_check_eq(Weave.Service.password, moneyPassword);
+    do_check_eq(Identity.basicPassword, Utils.encodeUTF8(moneyPassword));
     do_check_eq(requestBody, Utils.encodeUTF8(moneyPassword));
 
     _("changePassword() returns false for a server error, the password won't change.");
     Services.logins.removeAllLogins();
-    Weave.Service.username = "janedoe";
-    Weave.Service.password = "ilovejohn";
+    setBasicCredentials("janedoe", "ilovejohn");
     res = Weave.Service.changePassword("ILoveJohn86");
     do_check_false(res);
-    do_check_eq(Weave.Service.password, "ilovejohn");
+    do_check_eq(Identity.basicPassword, "ilovejohn");
 
   } finally {
     Weave.Svc.Prefs.resetBranch("");
     Services.logins.removeAllLogins();
     server.stop(run_next_test);
   }
 });
--- a/services/sync/tests/unit/test_service_cluster.js
+++ b/services/sync/tests/unit/test_service_cluster.js
@@ -11,17 +11,17 @@ function do_check_throws(func) {
   do_check_true(raised);
 }
 
 function test_findCluster() {
   _("Test Service._findCluster()");
   let server;
   try {
     Service.serverURL = TEST_SERVER_URL;
-    Service.username = "johndoe";
+    Identity.account = "johndoe";
 
     _("_findCluster() throws on network errors (e.g. connection refused).");
     do_check_throws(function() {
       Service._findCluster();
     });
 
     server = httpd_setup({
       "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "http://weave.user.node/"),
@@ -31,33 +31,33 @@ function test_findCluster() {
       "/user/1.0/joedoe/node/weave": httpd_handler(500, "Server Error", "Server Error")
     });
 
     _("_findCluster() returns the user's cluster node");
     let cluster = Service._findCluster();
     do_check_eq(cluster, "http://weave.user.node/");
 
     _("A 'null' response is converted to null.");
-    Service.username = "jimdoe";
+    Identity.account = "jimdoe";
     cluster = Service._findCluster();
     do_check_eq(cluster, null);
 
     _("If a 404 is encountered, the server URL is taken as the cluster URL");
-    Service.username = "janedoe";
+    Identity.account = "janedoe";
     cluster = Service._findCluster();
     do_check_eq(cluster, Service.serverURL);
 
     _("A 400 response will throw an error.");
-    Service.username = "juliadoe";
+    Identity.account = "juliadoe";
     do_check_throws(function() {
       Service._findCluster();
     });
 
     _("Any other server response (e.g. 500) will throw an error.");
-    Service.username = "joedoe";
+    Identity.account = "joedoe";
     do_check_throws(function() {
       Service._findCluster();
     });
 
   } finally {
     Svc.Prefs.resetBranch("");
     if (server) {
       server.stop(runNextTest);
@@ -69,49 +69,49 @@ function test_findCluster() {
 function test_setCluster() {
   _("Test Service._setCluster()");
   let server = httpd_setup({
     "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "http://weave.user.node/"),
     "/user/1.0/jimdoe/node/weave": httpd_handler(200, "OK", "null")
   });
   try {
     Service.serverURL = TEST_SERVER_URL;
-    Service.username = "johndoe";
+    Identity.account = "johndoe";
 
     _("Check initial state.");
     do_check_eq(Service.clusterURL, "");
 
     _("Set the cluster URL.");
     do_check_true(Service._setCluster());
     do_check_eq(Service.clusterURL, "http://weave.user.node/");
 
     _("Setting it again won't make a difference if it's the same one.");
     do_check_false(Service._setCluster());
     do_check_eq(Service.clusterURL, "http://weave.user.node/");
 
     _("A 'null' response won't make a difference either.");
-    Service.username = "jimdoe";
+    Identity.account = "jimdoe";
     do_check_false(Service._setCluster());
-    do_check_eq(Service.clusterURL, "http://weave.user.node/");      
+    do_check_eq(Service.clusterURL, "http://weave.user.node/");
 
   } finally {
     Svc.Prefs.resetBranch("");
     server.stop(runNextTest);
   }
 }
 
 function test_updateCluster() {
   _("Test Service._updateCluster()");
   let server = httpd_setup({
     "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "http://weave.user.node/"),
     "/user/1.0/janedoe/node/weave": httpd_handler(200, "OK", "http://weave.cluster.url/")
   });
   try {
     Service.serverURL = TEST_SERVER_URL;
-    Service.username = "johndoe";
+    Identity.account = "johndoe";
 
     _("Check initial state.");
     do_check_eq(Service.clusterURL, "");
     do_check_eq(Svc.Prefs.get("lastClusterUpdate"), null);
 
     _("Set the cluster URL.");
     let before = Date.now();
     do_check_true(Service._updateCluster());
@@ -120,17 +120,17 @@ function test_updateCluster() {
     do_check_true(lastUpdate >= before);
 
     _("Trying to update the cluster URL within the backoff timeout won't do anything.");
     do_check_false(Service._updateCluster());
     do_check_eq(Service.clusterURL, "http://weave.user.node/");
     do_check_eq(parseFloat(Svc.Prefs.get("lastClusterUpdate")), lastUpdate);
 
     _("Time travel 30 mins into the past and the update will work.");
-    Service.username = "janedoe";
+    Identity.account = "janedoe";
     Svc.Prefs.set("lastClusterUpdate", (lastUpdate - 30*60*1000).toString());
 
     before = Date.now();
     do_check_true(Service._updateCluster());
     do_check_eq(Service.clusterURL, "http://weave.cluster.url/");
     lastUpdate = parseFloat(Svc.Prefs.get("lastClusterUpdate"));
     do_check_true(lastUpdate >= before);
   
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -1,15 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.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/record.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/engines/tabs.js");
 Cu.import("resource://services-sync/log4moz.js");
   
 Engines.register(TabEngine);
 
 add_test(function v4_upgrade() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
@@ -109,52 +113,52 @@ add_test(function v4_upgrade() {
     do_check_true(Weave.Service.isLoggedIn);
     Weave.Service.sync();
     do_check_true(Weave.Service.isLoggedIn);
     
     let serverDecrypted;
     let serverKeys;
     let serverResp;
 
-      
+
     function retrieve_server_default() {
       serverKeys = serverResp = serverDecrypted = null;
-      
+
       serverKeys = new CryptoWrapper("crypto", "keys");
       serverResp = serverKeys.fetch(Weave.Service.cryptoKeysURL).response;
       do_check_true(serverResp.success);
-      
-      serverDecrypted = serverKeys.decrypt(Weave.Service.syncKeyBundle);
+
+      serverDecrypted = serverKeys.decrypt(Weave.Identity.syncKeyBundle);
       _("Retrieved WBO:       " + JSON.stringify(serverDecrypted));
       _("serverKeys:          " + JSON.stringify(serverKeys));
-      
+
       return serverDecrypted.default;
     }
-      
+
     function retrieve_and_compare_default(should_succeed) {
       let serverDefault = retrieve_server_default();
-      let localDefault = CollectionKeys.keyForCollection().keyPair;
+      let localDefault = CollectionKeys.keyForCollection().keyPairB64;
       
       _("Retrieved keyBundle: " + JSON.stringify(serverDefault));
       _("Local keyBundle:     " + JSON.stringify(localDefault));
       
       if (should_succeed)
         do_check_eq(JSON.stringify(serverDefault), JSON.stringify(localDefault));
       else
         do_check_neq(JSON.stringify(serverDefault), JSON.stringify(localDefault));
     }
-    
+
     // Uses the objects set above.
     function set_server_keys(pair) {
       serverDecrypted.default = pair;
       serverKeys.cleartext = serverDecrypted;
-      serverKeys.encrypt(Weave.Service.syncKeyBundle);
+      serverKeys.encrypt(Weave.Identity.syncKeyBundle);
       serverKeys.upload(Weave.Service.cryptoKeysURL);
     }
-    
+
     _("Checking we have the latest keys.");
     retrieve_and_compare_default(true);
     
     _("Update keys on server.");
     set_server_keys(["KaaaaaaaaaaaHAtfmuRY0XEJ7LXfFuqvF7opFdBD/MY=",
                      "aaaaaaaaaaaapxMO6TEWtLIOv9dj6kBAJdzhWDkkkis="]);
     
     _("Checking that we no longer have the latest keys.");
@@ -227,74 +231,67 @@ add_test(function v5_upgrade() {
                           },
                           extData: {
                             weaveLastUsed: 1
                           }}]}]};
     delete Svc.Session;
     Svc.Session = {
       getBrowserState: function () JSON.stringify(myTabs)
     };
-    
+
     Status.resetSync();
-    
-    Weave.Service.username = "johndoe";
-    Weave.Service.password = "ilovejane";
-    Weave.Service.passphrase = passphrase;
-    
+
+    setBasicCredentials("johndoe", "ilovejane", passphrase);
     Weave.Service.serverURL = TEST_SERVER_URL;
     Weave.Service.clusterURL = TEST_CLUSTER_URL;
-    
-    //
+
     // Test an upgrade where the contents of the server would cause us to error
     // -- keys decrypted with a different sync key, for example.
-    //     
-
     _("Testing v4 -> v5 (or similar) upgrade.");
     function update_server_keys(syncKeyBundle, wboName, collWBO) {
       generateNewKeys();
       serverKeys = CollectionKeys.asWBO("crypto", wboName);
       serverKeys.encrypt(syncKeyBundle);
       do_check_true(serverKeys.upload(Weave.Service.storageURL + collWBO).success);
     }
-    
+
     _("Bumping version.");
     // Bump version on the server.
     let m = new WBORecord("meta", "global");
     m.payload = {"syncID": "foooooooooooooooooooooooooo",
                  "storageVersion": STORAGE_VERSION + 1};
     m.upload(Weave.Service.metaURL);
-    
+
     _("New meta/global: " + JSON.stringify(meta_global));
-    
+
     // Fill the keys with bad data.
-    let badKeys = new SyncKeyBundle(null, null, "aaaaaaaaaaaaaaaaaaaaaaaaaa");
-    badKeys.generateEntry();
+    let badKeys = new SyncKeyBundle("foobar", "aaaaaaaaaaaaaaaaaaaaaaaaaa");
     update_server_keys(badKeys, "keys", "crypto/keys");  // v4
     update_server_keys(badKeys, "bulk", "crypto/bulk");  // v5
-    
-    // ... and get new ones.
+
+    _("Generating new keys.");
     generateNewKeys();
-    
+
     // Now sync and see what happens. It should be a version fail, not a crypto
     // fail.
-    
+
     _("Logging in.");
     try {
       Weave.Service.login("johndoe", "ilovejane", passphrase);
     }
     catch (e) {
       _("Exception: " + e);
     }
     _("Status: " + Status);
     do_check_false(Weave.Service.isLoggedIn);
     do_check_eq(VERSION_OUT_OF_DATE, Status.sync);
 
     // Clean up.
     Weave.Service.startOver();
-    
+
   } finally {
     Weave.Svc.Prefs.resetBranch("");
     server.stop(run_next_test);
   }
 });
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
--- a/services/sync/tests/unit/test_service_getStorageInfo.js
+++ b/services/sync/tests/unit/test_service_getStorageInfo.js
@@ -6,18 +6,17 @@ Cu.import("resource://services-sync/rest
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 let collections = {steam:  65.11328,
                    petrol: 82.488281,
                    diesel: 2.25488281};
 
 function run_test() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
+  setBasicCredentials("johndoe", "ilovejane");
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
 
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.StorageRequest").level = Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
@@ -27,18 +26,19 @@ add_test(function test_success() {
   let handler = httpd_handler(200, "OK", JSON.stringify(collections));
   let server = httpd_setup({"/1.1/johndoe/info/collections": handler});
 
   let request = Service.getStorageInfo("collections", function (error, info) {
     do_check_eq(error, null);
     do_check_true(Utils.deepEquals(info, collections));
 
     // Ensure that the request is sent off with the right bits.
-    do_check_true(basic_auth_matches(handler.request, Service.username,
-                                     Service.password));
+    do_check_true(basic_auth_matches(handler.request,
+                                     Identity.username,
+                                     Identity.basicPassword));
     let expectedUA = Services.appinfo.name + "/" + Services.appinfo.version +
                      " FxSync/" + WEAVE_VERSION + "." +
                      Services.appinfo.appBuildID + ".desktop";
     do_check_eq(handler.request.getHeader("User-Agent"), expectedUA);
 
     server.stop(run_next_test);
   });
   do_check_true(request instanceof RESTRequest);
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -74,53 +74,49 @@ add_test(function test_login_logout() {
 
     _("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);
 
     _("Try again with username and password set.");
-    Service.username = "johndoe";
-    Service.password = "ilovejane";
+    Identity.account = "johndoe";
+    Identity.basicPassword = "ilovejane";
     Service.login();
     do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
     do_check_false(Service.isLoggedIn);
 
     _("Success if passphrase is set.");
-    Service.passphrase = "foo";
+    Identity.syncKey = "foo";
     Service.login();
     do_check_eq(Status.service, STATUS_OK);
     do_check_eq(Status.login, LOGIN_SUCCEEDED);
     do_check_true(Service.isLoggedIn);
 
     _("We can also pass username, password and passphrase to login().");
     Service.login("janedoe", "incorrectpassword", "bar");
-    do_check_eq(Service.username, "janedoe");
-    do_check_eq(Service.password, "incorrectpassword");
-    do_check_eq(Service.passphrase, "bar");
+    setBasicCredentials("janedoe", "incorrectpassword", "bar");
     do_check_eq(Status.service, LOGIN_FAILED);
     do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
     do_check_false(Service.isLoggedIn);
 
     _("Try again with correct password.");
     Service.login("janedoe", "ilovejohn");
     do_check_eq(Status.service, STATUS_OK);
     do_check_eq(Status.login, LOGIN_SUCCEEDED);
     do_check_true(Service.isLoggedIn);
 
     _("Calling login() with parameters when the client is unconfigured sends notification.");
     let notified = false;
     Svc.Obs.add("weave:service:setup-complete", function() {
       notified = true;
     });
-    Service.username = "";
-    Service.password = "";
-    Service.passphrase = "";
+    setBasicCredentials(null, null, null);
     Service.login("janedoe", "ilovejohn", "bar");
     do_check_true(notified);
     do_check_eq(Status.service, STATUS_OK);
     do_check_eq(Status.login, LOGIN_SUCCEEDED);
     do_check_true(Service.isLoggedIn);
 
     _("Logout.");
     Service.logout();
@@ -133,19 +129,17 @@ add_test(function test_login_logout() {
   } finally {
     Svc.Prefs.resetBranch("");
     server.stop(run_next_test);
   }
 });
 
 add_test(function test_login_on_sync() {
   let server = setup();
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "bar";
+  setBasicCredentials("johndoe", "ilovejane", "bar");
 
   try {
     _("Sync calls login.");
     let oldLogin = Service.login;
     let loginCalled = false;
     Service.login = function() {
       loginCalled = true;
       Status.login = LOGIN_SUCCEEDED;
@@ -197,20 +191,21 @@ add_test(function test_login_on_sync() {
     do_check_true(scheduleCalled);
     SyncScheduler.scheduleNextSync = scheduleNextSyncF;
 
     // TODO: need better tests around master password prompting. See Bug 620583.
 
     mpLocked = true;
 
     // Testing exception handling if master password dialog is canceled.
-    // Do this by stubbing out Service.passphrase.
-    let oldPP = Service.__lookupGetter__("passphrase");
-    _("Old passphrase function is " + oldPP);
-    Service.__defineGetter__("passphrase",
+    // Do this by monkeypatching.
+    let oldGetter = Identity.__lookupGetter__("syncKey");
+    let oldSetter = Identity.__lookupSetter__("syncKey");
+    _("Old passphrase function is " + oldGetter);
+    Identity.__defineGetter__("syncKey",
                            function() {
                              throw "User canceled Master Password entry";
                            });
 
     let oldClearSyncTriggers = SyncScheduler.clearSyncTriggers;
     let oldLockedSync = Service._lockedSync;
 
     let cSTCalled = false;
@@ -228,16 +223,19 @@ add_test(function test_login_on_sync() {
     do_check_eq(Service._checkSync(), kSyncMasterPasswordLocked);
 
     _("Sync doesn't proceed and clears triggers if MP is still locked.");
     Service.sync();
 
     do_check_true(cSTCalled);
     do_check_false(lockedSyncCalled);
 
+    Identity.__defineGetter__("syncKey", oldGetter);
+    Identity.__defineSetter__("syncKey", oldSetter);
+
     // N.B., a bunch of methods are stubbed at this point. Be careful putting
     // new tests after this point!
 
   } finally {
     Svc.Prefs.resetBranch("");
     server.stop(run_next_test);
   }
 });
--- a/services/sync/tests/unit/test_service_passwordUTF8.js
+++ b/services/sync/tests/unit/test_service_passwordUTF8.js
@@ -57,34 +57,33 @@ function run_test() {
   do_test_pending();
   let server = httpd_setup({
     "/1.1/johndoe/info/collections":    login_handling(collectionsHelper.handler),
     "/1.1/johndoe/storage/meta/global": upd("meta",   new ServerWBO("global").handler()),
     "/1.1/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
     "/user/1.0/johndoe/password":       change_password
   });
 
-  Service.username = "johndoe";
-  Service.password = JAPANESE;
-  Service.passphrase = "cantentsveryrelevantabbbb";
+  setBasicCredentials("johndoe", JAPANESE, "irrelevant");
   Service.serverURL = TEST_SERVER_URL;
 
   try {
     _("Try to log in with the password.");
     server_password = "foobar";
     do_check_false(Service.verifyLogin());
     do_check_eq(server_password, "foobar");
 
-    _("Make the server password the low byte version of our password.  Login should work and have transparently changed the password to the UTF8 version.");
+    _("Make the server password the low byte version of our password.");
     server_password = LOWBYTES;
-    do_check_true(Service.verifyLogin());
-    do_check_eq(server_password, Utils.encodeUTF8(JAPANESE));
+    do_check_false(Service.verifyLogin());
+    do_check_eq(server_password, LOWBYTES);
 
     _("Can't use a password that has the same low bytes as ours.");
-    Service.password = APPLES;
+    server_password = Utils.encodeUTF8(JAPANESE);
+    Identity.basicPassword = APPLES;
     do_check_false(Service.verifyLogin());
     do_check_eq(server_password, Utils.encodeUTF8(JAPANESE));
 
   } finally {
     server.stop(do_test_finished);
     Svc.Prefs.resetBranch("");
   }
 }
--- a/services/sync/tests/unit/test_service_persistLogin.js
+++ b/services/sync/tests/unit/test_service_persistLogin.js
@@ -1,20 +1,18 @@
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
 function run_test() {
   try {
     // Ensure we have a blank slate to start.
     Services.logins.removeAllLogins();
-    
-    Weave.Service.username = "johndoe";
-    Weave.Service.password = "ilovejane";
-    Weave.Service.passphrase = "abbbbbcccccdddddeeeeefffff";
+
+    setBasicCredentials("johndoe", "ilovejane", "abbbbbcccccdddddeeeeefffff");
 
     _("Confirm initial environment is empty.");
     let logins = Services.logins.findLogins({}, PWDMGR_HOST, null,
                                         PWDMGR_PASSWORD_REALM);
     do_check_eq(logins.length, 0);
     logins = Services.logins.findLogins({}, PWDMGR_HOST, null,
                                         PWDMGR_PASSPHRASE_REALM);
     do_check_eq(logins.length, 0);
--- a/services/sync/tests/unit/test_service_startOver.js
+++ b/services/sync/tests/unit/test_service_startOver.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 function BlaEngine() {
@@ -22,19 +25,18 @@ Engines.register(BlaEngine);
 
 function run_test() {
   initTestLogging("Trace");
   run_next_test();
 }
 
 add_test(function test_resetLocalData() {
   // Set up.
-  Service.username = "foobar";
-  Service.password = "blablabla";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("foobar", "blablabla", // Law Blog
+                      "abcdeabcdeabcdeabcdeabcdea");
   Status.enforceBackoff = true;
   Status.backoffInterval = 42;
   Status.minimumNextSync = 23;
   Service.persistLogin();
 
   // Verify set up.
   do_check_eq(Status.checkSetup(), STATUS_OK);
 
@@ -47,18 +49,18 @@ add_test(function test_resetLocalData() 
     do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
   });
 
   Service.startOver();
   do_check_true(observerCalled);
 
   // Verify the site was nuked from orbit.
   do_check_eq(Svc.Prefs.get("username"), undefined);
-  do_check_eq(Service.password, "");
-  do_check_eq(Service.passphrase, "");
+  do_check_eq(Identity.basicPassword, null);
+  do_check_eq(Identity.syncKey, null);
 
   do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
   do_check_false(Status.enforceBackoff);
   do_check_eq(Status.backoffInterval, 0);
   do_check_eq(Status.minimumNextSync, 0);
 
   run_next_test();
 });
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -1,35 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/ext/Observers.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 
 function run_test() {
   _("When imported, Service.onStartup is called");
 
+  Svc.Prefs.set("registerEngines", "Tab,Bookmarks,Form,History");
+  new SyncTestingInfrastructure();
+
   // Test fixtures
-  Svc.Prefs.set("registerEngines", "Tab,Bookmarks,Form,History");
-  Svc.Prefs.set("username", "johndoe");
+  Identity.username = "johndoe";
 
   Cu.import("resource://services-sync/service.js");
 
   _("Service is enabled.");
   do_check_eq(Service.enabled, true);
 
   _("Engines are registered.");
   let engines = Engines.getAll();
   do_check_true(Utils.deepEquals([engine.name for each (engine in engines)],
                                  ['tabs', 'bookmarks', 'forms', 'history']));
 
-  _("Identities are registered.");
-  do_check_eq(ID.get('WeaveID').username, "johndoe");
-  do_check_eq(ID.get('WeaveCryptoID').username, "johndoe");
-
   _("Observers are notified of startup");
   do_test_pending();
   do_check_false(Status.ready);
   Observers.add("weave:service:ready", function (subject, data) {
     do_check_true(Status.ready);
 
     // Clean up.
     Svc.Prefs.resetBranch("");
--- a/services/sync/tests/unit/test_service_sync_401.js
+++ b/services/sync/tests/unit/test_service_sync_401.js
@@ -27,37 +27,33 @@ function run_test() {
     "/1.1/johndoe/storage/meta/global": upd("meta",   new ServerWBO("global").handler()),
     "/1.1/johndoe/info/collections":    login_handling(collectionsHelper.handler)
   });
 
   const GLOBAL_SCORE = 42;
 
   try {
     _("Set up test fixtures.");
-    Weave.Service.serverURL = TEST_SERVER_URL;
-    Weave.Service.clusterURL = TEST_CLUSTER_URL;
-    Weave.Service.username = "johndoe";
-    Weave.Service.password = "ilovejane";
-    Weave.Service.passphrase = "foo";
+    new SyncTestingInfrastructure("johndoe", "ilovejane", "foo");
     SyncScheduler.globalScore = GLOBAL_SCORE;
     // Avoid daily ping
     Weave.Svc.Prefs.set("lastPing", Math.floor(Date.now() / 1000));
 
     let threw = false;
     Weave.Svc.Obs.add("weave:service:sync:error", function (subject, data) {
       threw = true;
     });
 
     _("Initial state: We're successfully logged in.");
     Weave.Service.login();
     do_check_true(Weave.Service.isLoggedIn);
     do_check_eq(Weave.Status.login, Weave.LOGIN_SUCCEEDED);
 
     _("Simulate having changed the password somewhere else.");
-    Weave.Service.password = "ilovejosephine";
+    Identity.basicPassword = "ilovejosephine";
 
     _("Let's try to sync.");
     Weave.Service.sync();
 
     _("Verify that sync() threw an exception.");
     do_check_true(threw);
 
     _("We're no longer logged in.");
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -1,27 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 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/record.js");
+Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/log4moz.js");
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   let guidSvc = new FakeGUIDService();
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
 
   let collectionsHelper = track_collections_helper();
   let upd = collectionsHelper.with_updated_collection;
   let collections = collectionsHelper.collections;
-  
+
   function wasCalledHandler(wbo) {
     let handler = wbo.handler();
     return function() {
       wbo.wasCalled = true;
       handler.apply(this, arguments);
     };
   }
 
@@ -58,39 +61,39 @@ function run_test() {
     "/1.1/johndoe/storage/meta": upd("meta", wasCalledHandler(metaColl)),
     "/1.1/johndoe/info/collections": collectionsHelper.handler
   });
 
   try {
     _("Log in.");
     Weave.Service.serverURL = TEST_SERVER_URL;
     Weave.Service.clusterURL = TEST_CLUSTER_URL;
-    
+
     _("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);
+    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
 
     _("Log in with an old secret phrase, is upgraded to Sync Key.");
     Weave.Service.login("johndoe", "ilovejane", "my old secret phrase!!1!");
+    _("End of login");
     do_check_true(Weave.Service.isLoggedIn);
-    do_check_true(Utils.isPassphrase(Weave.Service.passphrase));
-    do_check_true(Utils.isPassphrase(Weave.Service.syncKeyBundle.keyStr));
-    let syncKey = Weave.Service.passphrase;
+    do_check_true(Utils.isPassphrase(Identity.syncKey));
+    let syncKey = Identity.syncKey;
     Weave.Service.startOver();
 
     Weave.Service.serverURL = TEST_SERVER_URL;
     Weave.Service.clusterURL = TEST_CLUSTER_URL;
     Weave.Service.login("johndoe", "ilovejane", syncKey);
     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();
 
     _("Checking that remoteSetup returns true.");
     do_check_true(Weave.Service._remoteSetup());
 
     _("Verify that the meta record was uploaded.");
@@ -115,51 +118,51 @@ function run_test() {
 
     let metaModified = meta_global.modified;
 
     Weave.Service.sync();
     do_check_true(meta_global.wasCalled);
     do_check_eq(metaModified, meta_global.modified);
 
     _("Checking bad passphrases.");
-    let pp = Weave.Service.passphrase;
-    Weave.Service.passphrase = "notvalid";
+    let pp = Identity.syncKey;
+    Identity.syncKey = "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;
+    Identity.syncKey = pp;
     do_check_true(Weave.Service.verifyAndFetchSymmetricKeys());
-    
+
     // changePassphrase wipes our keys, and they're regenerated on next sync.
     _("Checking changed passphrase.");
     let existingDefault = CollectionKeys.keyForCollection();
     let existingKeysPayload = keysWBO.payload;
     let newPassphrase = "bbbbbabcdeabcdeabcdeabcdea";
     Weave.Service.changePassphrase(newPassphrase);
-    
+
     _("Local key cache is full, but different.");
     do_check_true(!!CollectionKeys._default);
     do_check_false(CollectionKeys._default.equals(existingDefault));
-    
+
     _("Server has new keys.");
     do_check_true(!!keysWBO.payload);
     do_check_true(!!keysWBO.modified);
     do_check_neq(keysWBO.payload, existingKeysPayload);
 
     // Try to screw up HMAC calculation.
     // Re-encrypt keys with a new random keybundle, and upload them to the
     // server, just as might happen with a second client.
     _("Attempting to screw up HMAC by re-encrypting keys.");
     let keys = CollectionKeys.asWBO();
-    let b = new BulkKeyBundle("hmacerror", "hmacerror");
+    let b = new BulkKeyBundle("hmacerror");
     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
@@ -61,29 +61,23 @@ function sync_httpd_setup(handlers) {
   let cl = new ServerCollection();
   handlers["/1.1/johndoe/storage/clients"] =
     upd("clients", cl.handler());
   
   return httpd_setup(handlers);
 }
 
 function setUp() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
-  Service.serverURL = TEST_SERVER_URL;
-  Service.clusterURL = TEST_CLUSTER_URL;
-  // So that we can poke at meta/global.
-  new FakeCryptoService();
-
+  new SyncTestingInfrastructure("johndoe", "ilovejane",
+                                "abcdeabcdeabcdeabcdeabcdea");
   // Ensure that the server has valid keys so that logging in will work and not
   // result in a server wipe, rendering many of these tests useless.
   generateNewKeys();
   let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Service.syncKeyBundle);
+  serverKeys.encrypt(Identity.syncKeyBundle);
   return serverKeys.upload(Service.cryptoKeysURL).success;
 }
 
 const PAYLOAD = 42;
 
 
 function run_test() {
   initTestLogging("Trace");
@@ -255,17 +249,17 @@ add_test(function test_enabledRemotely()
   });
   setUp();
 
   // We need to be very careful how we do this, so that we don't trigger a
   // fresh start!
   try {
     _("Upload some keys to avoid a fresh start.");
     let wbo = CollectionKeys.generateNewKeysWBO();
-    wbo.encrypt(Service.syncKeyBundle);
+    wbo.encrypt(Identity.syncKeyBundle);
     do_check_eq(200, wbo.upload(Service.cryptoKeysURL).status);
 
     _("Engine is disabled.");
     do_check_false(engine.enabled);
 
     _("Sync.");
     Weave.Service.sync();
 
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -53,35 +53,35 @@ function run_test() {
     _("Credentials won't check out because we're not configured yet.");
     Status.resetSync();
     do_check_false(Service.verifyLogin());
     do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
     do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME);
 
     _("Try again with username and password set.");
     Status.resetSync();
-    Service.username = "johndoe";
-    Service.password = "ilovejane";
+    setBasicCredentials("johndoe", "ilovejane", null);
     do_check_false(Service.verifyLogin());
     do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
 
     _("verifyLogin() has found out the user's cluster URL, though.");
     do_check_eq(Service.clusterURL, "http://localhost:8080/api/");
 
     _("Success if passphrase is set.");
     Status.resetSync();
-    Service.passphrase = "foo";
+    Identity.syncKey = "foo";
     do_check_true(Service.verifyLogin());
     do_check_eq(Status.service, STATUS_OK);
     do_check_eq(Status.login, LOGIN_SUCCEEDED);
 
     _("If verifyLogin() encounters a server error, it flips on the backoff flag and notifies observers on a 503 with Retry-After.");
     Status.resetSync();
-    Service.username = "janedoe";
+    Identity.account = "janedoe";
+    Service._updateCachedURLs();
     do_check_false(Status.enforceBackoff);
     let backoffInterval;    
     Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
       Svc.Obs.remove("weave:service:backoff:interval", observe);
       backoffInterval = subject;
     });
     do_check_false(Service.verifyLogin());
     do_check_true(Status.enforceBackoff);
--- a/services/sync/tests/unit/test_service_wipeServer.js
+++ b/services/sync/tests/unit/test_service_wipeServer.js
@@ -26,18 +26,18 @@ FakeCollection.prototype = {
   }
 };
 
 function setUpTestFixtures() {
   let cryptoService = new FakeCryptoService();
 
   Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
-  Service.username = "johndoe";
-  Service.passphrase = "aabcdeabcdeabcdeabcdeabcde";
+
+  setBasicCredentials("johndoe", null, "aabcdeabcdeabcdeabcdeabcde");
 }
 
 
 function run_test() {
   initTestLogging("Trace");
   run_next_test();
 }
 
@@ -50,16 +50,17 @@ add_test(function test_wipeServer_list_s
   let server = httpd_setup({
     "/1.1/johndoe/storage/steam": steam_coll.handler(),
     "/1.1/johndoe/storage/diesel": diesel_coll.handler(),
     "/1.1/johndoe/storage/petrol": httpd_handler(404, "Not Found")
   });
 
   try {
     setUpTestFixtures();
+    new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
 
     _("Confirm initial environment.");
     do_check_false(steam_coll.deleted);
     do_check_false(diesel_coll.deleted);
 
     _("wipeServer() will happily ignore the non-existent collection and use the timestamp of the last DELETE that was successful.");
     let timestamp = Service.wipeServer(["steam", "diesel", "petrol"]);
     do_check_eq(timestamp, diesel_coll.timestamp);
@@ -83,16 +84,17 @@ add_test(function test_wipeServer_list_5
   let server = httpd_setup({
     "/1.1/johndoe/storage/steam": steam_coll.handler(),
     "/1.1/johndoe/storage/petrol": httpd_handler(503, "Service Unavailable"),
     "/1.1/johndoe/storage/diesel": diesel_coll.handler()
   });
 
   try {
     setUpTestFixtures();
+    new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
 
     _("Confirm initial environment.");
     do_check_false(steam_coll.deleted);
     do_check_false(diesel_coll.deleted);
 
     _("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 {
@@ -130,16 +132,17 @@ add_test(function test_wipeServer_all_su
   }
 
   let server = httpd_setup({
     "/1.1/johndoe/storage": storageHandler
   });
   setUpTestFixtures();
 
   _("Try deletion.");
+  new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
   let returnedTimestamp = Service.wipeServer();
   do_check_true(deleted);
   do_check_eq(returnedTimestamp, serverTimestamp);
 
   server.stop(run_next_test);
   Svc.Prefs.resetBranch("");
 });
 
@@ -161,16 +164,17 @@ add_test(function test_wipeServer_all_40
   }
 
   let server = httpd_setup({
     "/1.1/johndoe/storage": storageHandler
   });
   setUpTestFixtures();
 
   _("Try deletion.");
+  new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
   let returnedTimestamp = Service.wipeServer();
   do_check_true(deleted);
   do_check_eq(returnedTimestamp, serverTimestamp);
 
   server.stop(run_next_test);
   Svc.Prefs.resetBranch("");
 });
 
@@ -189,16 +193,17 @@ add_test(function test_wipeServer_all_50
   let server = httpd_setup({
     "/1.1/johndoe/storage": storageHandler
   });
   setUpTestFixtures();
 
   _("Try deletion.");
   let error;
   try {
+    new SyncTestingInfrastructure("johndoe", "irrelevant", "irrelevant");
     Service.wipeServer();
     do_throw("Should have thrown!");
   } catch (ex) {
     error = ex;
   }
   do_check_eq(error.status, 503);
 
   server.stop(run_next_test);
--- a/services/sync/tests/unit/test_status_checkSetup.js
+++ b/services/sync/tests/unit/test_status_checkSetup.js
@@ -1,45 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 
 function run_test() {
+  initTestLogging("Trace");
+
   try {
-    _("Verify initial setup.");
-    do_check_eq(ID.get("WeaveID"), null);
-    do_check_eq(ID.get("WeaveCryptoID"), null);
+    _("Ensure fresh config.");
+    Identity.deleteSyncCredentials();
 
     _("Fresh setup, we're not configured.");
     do_check_eq(Status.checkSetup(), CLIENT_NOT_CONFIGURED);
     do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME);
     Status.resetSync();
 
     _("Let's provide a username.");
-    Svc.Prefs.set("username", "johndoe");
+    Identity.username = "johndoe";
     do_check_eq(Status.checkSetup(), CLIENT_NOT_CONFIGURED);
     do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
     Status.resetSync();
 
-    _("checkSetup() created a WeaveID identity.");
-    let id = ID.get("WeaveID");
-    do_check_true(!!id);
+    do_check_neq(Identity.username, null);
 
     _("Let's provide a password.");
-    id.password = "carotsalad";
+    Identity.basicPassword = "carotsalad";
     do_check_eq(Status.checkSetup(), CLIENT_NOT_CONFIGURED);
     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.keyStr = "a-bcdef-abcde-acbde-acbde-acbde";
+    Identity.syncKey = "a-bcdef-abcde-acbde-acbde-acbde";
+    _("checkSetup()");
     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
@@ -90,17 +90,17 @@ function test_toFetch() {
     // Read file from disk
     toFetch = [Utils.makeGUID(), Utils.makeGUID()];
     syncTesting.fakeFilesystem.fakeContents[filename] = JSON.stringify(toFetch);
     engine.loadToFetch();
     do_check_eq(engine.toFetch.length, 2);
     do_check_eq(engine.toFetch[0], toFetch[0]);
     do_check_eq(engine.toFetch[1], toFetch[1]);
   } finally {
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
+    Svc.Prefs.resetBranch("");
   }
 }
 
 function test_previousFailed() {
   _("SyncEngine.previousFailed corresponds to file on disk");
   let syncTesting = new SyncTestingInfrastructure();
   const filename = "weave/failed/steam.json";
   let engine = makeSteamEngine();
@@ -120,17 +120,17 @@ function test_previousFailed() {
     // Read file from disk
     previousFailed = [Utils.makeGUID(), Utils.makeGUID()];
     syncTesting.fakeFilesystem.fakeContents[filename] = JSON.stringify(previousFailed);
     engine.loadPreviousFailed();
     do_check_eq(engine.previousFailed.length, 2);
     do_check_eq(engine.previousFailed[0], previousFailed[0]);
     do_check_eq(engine.previousFailed[1], previousFailed[1]);
   } finally {
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
+    Svc.Prefs.resetBranch("");
   }
 }
 
 function test_resetClient() {
   _("SyncEngine.resetClient resets lastSync and toFetch");
   let syncTesting = new SyncTestingInfrastructure();
   let engine = makeSteamEngine();
   try {
@@ -145,17 +145,16 @@ function test_resetClient() {
     engine.previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
 
     engine.resetClient();
     do_check_eq(engine.lastSync, 0);
     do_check_eq(engine.lastSyncLocal, 0);
     do_check_eq(engine.toFetch.length, 0);
     do_check_eq(engine.previousFailed.length, 0);
   } finally {
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
     Svc.Prefs.resetBranch("");
   }
 }
 
 function test_wipeServer() {
   _("SyncEngine.wipeServer deletes server data and resets the client.");
   let syncTesting = new SyncTestingInfrastructure();
   Svc.Prefs.set("serverURL", TEST_SERVER_URL);
@@ -177,17 +176,16 @@ function test_wipeServer() {
     _("Wipe server data and reset client.");
     engine.wipeServer();
     do_check_eq(steamCollection.payload, undefined);
     do_check_eq(engine.lastSync, 0);
     do_check_eq(engine.toFetch.length, 0);
 
   } finally {
     server.stop(do_test_finished);
-    syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
     Svc.Prefs.resetBranch("");
   }
 }
 
 function run_test() {
   test_url_attributes();
   test_syncID();
   test_lastSync();
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -13,16 +13,23 @@ function makeRotaryEngine() {
 
 function cleanAndGo(server) {
   Svc.Prefs.resetBranch("");
   Svc.Prefs.set("log.logger.engine.rotary", "Trace");
   Records.clearCache();
   server.stop(run_next_test);
 }
 
+function configureService(username, password) {
+  Service.clusterURL = TEST_CLUSTER_URL;
+
+  Identity.account = username || "foo";
+  Identity.basicPassword = password || "password";
+}
+
 function createServerAndConfigureClient() {
   let engine = new RotaryEngine();
 
   let contents = {
     meta: {global: {engines: {rotary: {version: engine.version,
                                        syncID:  engine.syncID}}}},
     crypto: {},
     rotary: {}
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -44,24 +44,22 @@ function sync_httpd_setup() {
     "/1.1/johndoe/storage/crypto/keys":
       upd("crypto", (new ServerWBO("keys")).handler()),
     "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
     "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "null")
   });
 }
 
 function setUp() {
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   Service.clusterURL = TEST_CLUSTER_URL;
 
   generateNewKeys();
   let serverKeys = CollectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Service.syncKeyBundle);
+  serverKeys.encrypt(Identity.syncKeyBundle);
   return serverKeys.upload(Service.cryptoKeysURL).success;
 }
 
 function cleanUpAndGo(server) {
   Utils.nextTick(function () {
     Service.startOver();
     if (server) {
       server.stop(run_next_test);
@@ -208,24 +206,26 @@ add_test(function test_masterpassword_lo
 });
 
 add_test(function test_calculateBackoff() {
   do_check_eq(Status.backoffInterval, 0);
 
   // Test no interval larger than the maximum backoff is used if
   // Status.backoffInterval is smaller.
   Status.backoffInterval = 5;
-  let backoffInterval = Utils.calculateBackoff(50, MAXIMUM_BACKOFF_INTERVAL);
+  let backoffInterval = Utils.calculateBackoff(50, MAXIMUM_BACKOFF_INTERVAL,
+                                               Status.backoffInterval);
 
   do_check_eq(backoffInterval, MAXIMUM_BACKOFF_INTERVAL);
 
   // Test Status.backoffInterval is used if it is 
   // larger than MAXIMUM_BACKOFF_INTERVAL.
   Status.backoffInterval = MAXIMUM_BACKOFF_INTERVAL + 10;
-  backoffInterval = Utils.calculateBackoff(50, MAXIMUM_BACKOFF_INTERVAL);
+  backoffInterval = Utils.calculateBackoff(50, MAXIMUM_BACKOFF_INTERVAL,
+                                           Status.backoffInterval);
   
   do_check_eq(backoffInterval, MAXIMUM_BACKOFF_INTERVAL + 10);
 
   cleanUpAndGo();
 });
 
 add_test(function test_scheduleNextSync_nowOrPast() {
   Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() {
@@ -461,47 +461,47 @@ add_test(function test_autoconnect_nextS
   waitForZeroTimer(function () {
     do_check_eq(SyncScheduler.nextSync, expectedSync);
     do_check_true(SyncScheduler.syncTimer.delay >= expectedInterval);
 
     Svc.Obs.remove("weave:service:login:start", onLoginStart);
     cleanUpAndGo();
   });
 
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
   SyncScheduler.delayedAutoConnect(0);
 });
 
 add_test(function test_autoconnect_mp_locked() {
   let server = sync_httpd_setup();
   setUp();
 
   // Pretend user did not unlock master password.
   let origLocked = Utils.mpLocked;
   Utils.mpLocked = function() true;
 
-  let origPP = Service.__lookupGetter__("passphrase");
-  delete Service.passphrase;
-  Service.__defineGetter__("passphrase", function() {
+  let origGetter = Identity.__lookupGetter__("syncKey");
+  let origSetter = Identity.__lookupSetter__("syncKey");
+  delete Identity.syncKey;
+  Identity.__defineGetter__("syncKey", function() {
     _("Faking Master Password entry cancelation.");
     throw "User canceled Master Password entry";
   });
 
   // A locked master password will still trigger a sync, but then we'll hit
   // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
   Svc.Obs.add("weave:service:login:error", function onLoginError() {
     Svc.Obs.remove("weave:service:login:error", onLoginError);
     Utils.nextTick(function aLittleBitAfterLoginError() {
       do_check_eq(Status.login, MASTER_PASSWORD_LOCKED);
 
       Utils.mpLocked = origLocked;
-      delete Service.passphrase;
-      Service.__defineGetter__("passphrase", origPP);
+      delete Identity.syncKey;
+      Identity.__defineGetter__("syncKey", origGetter);
+      Identity.__defineSetter__("syncKey", origSetter);
 
       cleanUpAndGo(server);
     });
   });
 
   SyncScheduler.delayedAutoConnect(0);
 });
 
@@ -844,20 +844,18 @@ add_test(function test_sync_503_Retry_Af
   do_check_true(SyncScheduler.nextSync >= Date.now() + minimumExpectedDelay);
   do_check_true(SyncScheduler.syncTimer.delay >= minimumExpectedDelay);
 
   cleanUpAndGo(server);
 });
 
 add_test(function test_loginError_recoverable_reschedules() {
   _("Verify that a recoverable login error schedules a new sync.");
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
-  Service.serverURL  = TEST_SERVER_URL;
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
+  Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   Service.persistLogin();
   Status.resetSync(); // reset Status.login
 
   Svc.Obs.add("weave:service:login:error", function onLoginError() {
     Svc.Obs.remove("weave:service:login:error", onLoginError);
     Utils.nextTick(function aLittleBitAfterLoginError() {
       do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
@@ -888,20 +886,18 @@ add_test(function test_loginError_recove
   do_check_eq(Status.checkSetup(), STATUS_OK);
   do_check_eq(Status.login, LOGIN_SUCCEEDED);
 
   SyncScheduler.scheduleNextSync(0);
 });
 
 add_test(function test_loginError_fatal_clearsTriggers() {
   _("Verify that a fatal login error clears sync triggers.");
-  Service.username = "johndoe";
-  Service.password = "ilovejane";
-  Service.passphrase = "abcdeabcdeabcdeabcdeabcdea";
-  Service.serverURL  = TEST_SERVER_URL;
+  setBasicCredentials("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
+  Service.serverURL = TEST_SERVER_URL;
   Service.clusterURL = TEST_CLUSTER_URL;
   Service.persistLogin();
   Status.resetSync(); // reset Status.login
 
   let server = httpd_setup({
     "/1.1/johndoe/info/collections": httpd_handler(401, "Unauthorized")
   });
 
--- a/services/sync/tests/unit/test_syncstoragerequest.js
+++ b/services/sync/tests/unit/test_syncstoragerequest.js
@@ -52,27 +52,26 @@ add_test(function test_user_agent_mobile
     server.stop(run_next_test);
   });
 });
 
 add_test(function test_auth() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
-  let id = new Identity(PWDMGR_PASSWORD_REALM, "johndoe");
-  id.password = "ilovejane";
-  ID.set("WeaveID", id);
+  setBasicCredentials("johndoe", "ilovejane", "XXXXXXXXX");
 
   let request = new SyncStorageRequest(STORAGE_REQUEST_RESOURCE_URL);
   request.get(function (error) {
     do_check_eq(error, null);
     do_check_eq(this.response.status, 200);
     do_check_true(basic_auth_matches(handler.request, "johndoe", "ilovejane"));
 
-    ID.del("WeaveID");
+    Svc.Prefs.reset("");
+
     server.stop(run_next_test);
   });
 });
 
 /**
  *  The X-Weave-Timestamp header updates SyncStorageRequest.serverTime.
  */
 add_test(function test_weave_timestamp() {
--- a/services/sync/tests/unit/test_upgrade_old_sync_key.js
+++ b/services/sync/tests/unit/test_upgrade_old_sync_key.js
@@ -1,9 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/main.js");
 var btoa = Cu.import("resource://services-sync/util.js").btoa;
 
 // Test upgrade of a dashed old-style sync key.
 function run_test() {
   const PBKDF2_KEY_BYTES = 16;
   initTestLogging("Trace");
 
@@ -16,21 +20,22 @@ function run_test() {
   // Still not a modern passphrase...
   do_check_false(Utils.isPassphrase(normalized));
 
   // ... but different.
   do_check_neq(normalized, passphrase);
   do_check_eq(normalized, "abcdeabcdeabcdeabcde");
 
   // Now run through the upgrade.
+  Identity.account = "johndoe";
   Weave.Service.syncID = "1234567890";
-  Weave.Service.passphrase = normalized;     // UI normalizes.
-  do_check_false(Utils.isPassphrase(Weave.Service.passphrase));
+  Identity.syncKey = normalized; // UI normalizes.
+  do_check_false(Utils.isPassphrase(Identity.syncKey));
   Weave.Service.upgradeSyncKey(Weave.Service.syncID);
-  let upgraded = Weave.Service.passphrase;
+  let upgraded = Identity.syncKey;
   _("Upgraded: " + upgraded);
   do_check_true(Utils.isPassphrase(upgraded));
 
   // Now let's verify that it's been derived correctly, from the normalized
   // version, and the encoded sync ID.
   _("Sync ID: " + Weave.Service.syncID);
   let derivedKeyStr =
     Utils.derivePresentableKeyFromPassphrase(normalized,
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,78 +1,85 @@
 [DEFAULT]
 head = head_appinfo.js head_helpers.js head_http_server.js
 tail =
 
 [test_load_modules.js]
 
+# The manifest is roughly ordered from low-level to high-level. When making
+# systemic sweeping changes, this makes it easier to identify errors closer to
+# the source.
+
+# Ensure we can import everything.
+[test_load_modules.js]
+
+# util contains a bunch of functionality used throughout.
+[test_utils_atob.js]
+[test_utils_catch.js]
+[test_utils_deepCopy.js]
+[test_utils_deepEquals.js]
+[test_utils_deferGetSet.js]
+[test_utils_deriveKey.js]
+[test_utils_encodeBase32.js]
+[test_utils_ensureOneOpen.js]
+[test_utils_getErrorString.js]
+[test_utils_getIcon.js]
+[test_utils_hkdfExpand.js]
+[test_utils_httpmac.js]
+[test_utils_json.js]
+[test_utils_lazyStrings.js]
+[test_utils_lock.js]
+[test_utils_makeGUID.js]
+[test_utils_makeURI.js]
+[test_utils_namedTimer.js]
+[test_utils_notify.js]
+[test_utils_passphrase.js]
+[test_utils_pbkdf2.js]
+[test_utils_sha1.js]
+[test_utils_stackTrace.js]
+[test_utils_utf8.js]
+
+# We have a number of other libraries that are pretty much standalone.
 [test_Observers.js]
 [test_Preferences.js]
-[test_addons_engine.js]
-[test_addons_reconciler.js]
-[test_addons_store.js]
-[test_addons_tracker.js]
 [test_async_chain.js]
 [test_async_querySpinningly.js]
-[test_auth_manager.js]
-[test_bookmark_batch_fail.js]
-[test_bookmark_engine.js]
-[test_bookmark_legacy_microsummaries_support.js]
-[test_bookmark_livemarks.js]
-[test_bookmark_order.js]
-[test_bookmark_places_query_rewriting.js]
-[test_bookmark_record.js]
-[test_bookmark_smart_bookmarks.js]
-[test_bookmark_store.js]
-[test_bookmark_tracker.js]
-[test_clients_engine.js]
-[test_clients_escape.js]
-[test_collection_inc_get.js]
-[test_collections_recovery.js]
-[test_corrupt_keys.js]
-[test_engine.js]
-[test_engine_abort.js]
-[test_enginemanager.js]
-[test_errorhandler.js]
-[test_errorhandler_filelog.js]
-# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
-skip-if = os == "android"
-[test_errorhandler_sync_checkServerError.js]
-# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
-skip-if = os == "android"
-[test_forms_store.js]
-[test_forms_tracker.js]
-[test_history_engine.js]
-[test_history_store.js]
-[test_history_tracker.js]
-[test_hmac_error.js]
 [test_httpd_sync_server.js]
-[test_interval_triggers.js]
+[test_log4moz.js]
+[test_restrequest.js]
 [test_jpakeclient.js]
 # Bug 618233: this test produces random failures on Windows 7.
 # Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
 skip-if = os == "win" || os == "android"
-[test_keys.js]
-[test_log4moz.js]
-[test_node_reassignment.js]
-[test_notifications.js]
-[test_password_store.js]
-[test_password_tracker.js]
-[test_places_guid_downgrade.js]
-[test_prefs_store.js]
-[test_prefs_tracker.js]
-[test_records_crypto.js]
-[test_records_crypto_generateEntry.js]
-[test_records_wbo.js]
+
+# HTTP layers.
 [test_resource.js]
 [test_resource_async.js]
 [test_resource_ua.js]
-[test_restrequest.js]
-[test_score_triggers.js]
-[test_sendcredentials_controller.js]
+[test_syncstoragerequest.js]
+
+# Generic Sync types.
+[test_collection_inc_get.js]
+[test_collections_recovery.js]
+[test_identity_manager.js]
+[test_keys.js]
+[test_records_crypto.js]
+[test_records_wbo.js]
+
+# Engine APIs.
+[test_engine.js]
+[test_engine_abort.js]
+[test_enginemanager.js]
+[test_syncengine.js]
+[test_syncengine_sync.js]
+# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
+skip-if = os == "android"
+[test_tracker_addChanged.js]
+
+# Service semantics.
 [test_service_attributes.js]
 [test_service_changePassword.js]
 [test_service_checkAccount.js]
 [test_service_cluster.js]
 [test_service_createAccount.js]
 [test_service_detect_upgrade.js]
 [test_service_getStorageInfo.js]
 [test_service_login.js]
@@ -87,45 +94,58 @@ skip-if = os == "win" || os == "android"
 # Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
 skip-if = os == "android"
 [test_service_sync_updateEnabledEngines.js]
 # Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
 skip-if = os == "android"
 [test_service_verifyLogin.js]
 [test_service_wipeClient.js]
 [test_service_wipeServer.js]
+
+[test_corrupt_keys.js]
+[test_errorhandler.js]
+[test_errorhandler_filelog.js]
+# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
+skip-if = os == "android"
+[test_errorhandler_sync_checkServerError.js]
+# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
+skip-if = os == "android"
+[test_hmac_error.js]
+[test_interval_triggers.js]
+[test_node_reassignment.js]
+[test_notifications.js]
+[test_score_triggers.js]
+[test_sendcredentials_controller.js]
 [test_status.js]
 [test_status_checkSetup.js]
-[test_syncengine.js]
-[test_syncengine_sync.js]
-# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
-skip-if = os == "android"
 [test_syncscheduler.js]
-[test_syncstoragerequest.js]
+[test_upgrade_old_sync_key.js]
+
+# Finally, we test each engine.
+[test_addons_engine.js]
+[test_addons_reconciler.js]
+[test_addons_store.js]
+[test_addons_tracker.js]
+[test_bookmark_batch_fail.js]
+[test_bookmark_engine.js]
+[test_bookmark_legacy_microsummaries_support.js]
+[test_bookmark_livemarks.js]
+[test_bookmark_order.js]
+[test_bookmark_places_query_rewriting.js]
+[test_bookmark_record.js]
+[test_bookmark_smart_bookmarks.js]
+[test_bookmark_store.js]
+[test_bookmark_tracker.js]
+[test_clients_engine.js]
+[test_clients_escape.js]
+[test_forms_store.js]
+[test_forms_tracker.js]
+[test_history_engine.js]
+[test_history_store.js]
+[test_history_tracker.js]
+[test_places_guid_downgrade.js]
+[test_password_store.js]
+[test_password_tracker.js]
+[test_prefs_store.js]
+[test_prefs_tracker.js]
 [test_tab_engine.js]
 [test_tab_store.js]
 [test_tab_tracker.js]
-[test_tracker_addChanged.js]
-[test_upgrade_old_sync_key.js]
-[test_utils_atob.js]
-[test_utils_catch.js]
-[test_utils_deepCopy.js]
-[test_utils_deepEquals.js]
-[test_utils_deferGetSet.js]
-[test_utils_deriveKey.js]
-[test_utils_encodeBase32.js]
-[test_utils_ensureOneOpen.js]
-[test_utils_getErrorString.js]
-[test_utils_getIcon.js]
-[test_utils_hkdfExpand.js]
-[test_utils_httpmac.js]
-[test_utils_json.js]
-[test_utils_lazyStrings.js]
-[test_utils_lock.js]
-[test_utils_makeGUID.js]
-[test_utils_makeURI.js]
-[test_utils_namedTimer.js]
-[test_utils_notify.js]
-[test_utils_passphrase.js]
-[test_utils_pbkdf2.js]
-[test_utils_sha1.js]
-[test_utils_stackTrace.js]
-[test_utils_utf8.js]
--- a/services/sync/tps/extensions/tps/modules/sync.jsm
+++ b/services/sync/tps/extensions/tps/modules/sync.jsm
@@ -97,19 +97,19 @@ var TPS = {
   SetupSyncAccount: function TPS__SetupSyncAccount() {
     try {
       let serverURL = prefs.getCharPref('tps.account.serverURL');
       if (serverURL) {
         Weave.Service.serverURL = serverURL;
       }
     }
     catch(e) {}
-    Weave.Service.account = prefs.getCharPref('tps.account.username');
-    Weave.Service.password = prefs.getCharPref('tps.account.password');
-    Weave.Service.passphrase = prefs.getCharPref('tps.account.passphrase');
+    Weave.Identity.account       = prefs.getCharPref('tps.account.username');
+    Weave.Identity.basicPassword = prefs.getCharPref('tps.account.password');
+    Weave.Identity.syncKey       = prefs.getCharPref('tps.account.passphrase');
     Weave.Svc.Obs.notify("weave:service:setup-complete");
   },
 
   Sync: function TPS__Sync(options) {
     Logger.logInfo('Mozmill starting sync operation: ' + options);
     switch(options) {
       case SYNC_WIPE_REMOTE:
         Weave.Svc.Prefs.set("firstSync", "wipeRemote");
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/modules/tps.jsm
@@ -779,27 +779,27 @@ let TPS =
     }
 
     Logger.logInfo("Setting client credentials.");
     if (account["admin-secret"]) {
       // if admin-secret is specified, we'll dynamically create
       // a new sync account
       Weave.Svc.Prefs.set("admin-secret", account["admin-secret"]);
       let suffix = account["account-suffix"];
-      Service.account = "tps" + suffix + "@mozilla.com";
-      Service.password = "tps" + suffix + "tps" + suffix;
-      Service.passphrase = Weave.Utils.generatePassphrase();
-      Service.createAccount(Service.account,
-                            Service.password,
+      Weave.Identity.account = "tps" + suffix + "@mozilla.com";
+      Weave.Identity.basicPassword = "tps" + suffix + "tps" + suffix;
+      Weave.Identity.syncKey = Weave.Utils.generatePassphrase();
+      Service.createAccount(Weave.Identity.account,
+                            Weave.Identity.basicPassword,
                             "dummy1", "dummy2");
     } else if (account["username"] && account["password"] &&
                account["passphrase"]) {
-      Service.account = account["username"];
-      Service.password = account["password"];
-      Service.passphrase = account["passphrase"];
+      Weave.Identity.account = account["username"];
+      Weave.Identity.basicPassword = account["password"];
+      Weave.Identity.syncKey = account["passphrase"];
     } else {
       this.DumpError("Must specify admin-secret, or " +
                      "username/password/passphrase in the config file");
       return;
     }
 
     Service.login();
     Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status not OK");