Bug 730989 - Refactor identity and authentication in Sync; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Thu, 22 Mar 2012 15:49:50 -0700
changeset 90703 82c4a54e0437dd4f03e887bad846e188dd665791
parent 90702 e8b6b6b810ee2914fcdb694798ad1c81955dad72
child 90704 d6b9981245d2f2e4d042f48fa0333fcfd56ef648
push id22374
push usergszorc@mozilla.com
push dateFri, 30 Mar 2012 18:51:50 +0000
treeherdermozilla-central@cf60d15e8804 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs730989
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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");