Merge mozilla-central to mozilla-inbound
authorEd Morley <bmo@edmorley.co.uk>
Sat, 31 Mar 2012 15:28:55 +0100
changeset 94097 fc71d9a33c593db1ad757d140d8975e99d27a229
parent 94096 b39493b4caa79bd42405aae7920bf08e06a5864a (current diff)
parent 94034 dc227fe382351bd23077e51600b8c427a54db1fe (diff)
child 94098 f9dbb20938024eb6d952a59432704ff4e3090133
push id886
push userlsblakk@mozilla.com
push dateMon, 04 Jun 2012 19:57:52 +0000
treeherdermozilla-beta@bbd8d5efd6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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
Merge mozilla-central to mozilla-inbound
mobile/android/base/GeckoAppShell.java
services/sync/tests/unit/test_auth_manager.js
services/sync/tests/unit/test_records_crypto_generateEntry.js
services/sync/tests/unit/test_utils_ensureOneOpen.js
widget/android/AndroidBridge.cpp
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1332452348000">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1332870813000">
   <emItems>
       <emItem  blockID="i58" id="webmaster@buzzzzvideos.info">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i41" id="{99079a25-328f-4bd4-be04-00955acaa0a7}">
                         <versionRange  minVersion="0.1" maxVersion="4.3.1.00" severity="1">
                     </versionRange>
@@ -80,16 +80,20 @@
       <emItem  blockID="i53" id="{a3a5c777-f583-4fef-9380-ab4add1bc2a8}">
                         <versionRange  minVersion="2.0.3" maxVersion="2.0.3">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i59" id="ghostviewer@youtube2.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i78" id="socialnetworktools@mozilla.doslash.org">
+                        <versionRange  minVersion="0" maxVersion="*">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i70" id="psid-vhvxQHMZBOzUZA@jetpack">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i68" id="flashupdate@adobe.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
@@ -106,16 +110,18 @@
       <emItem  blockID="i56" id="flash@adobe.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i55" id="youtube@youtube7.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i47" id="youtube@youtube2.com">
+                        </emItem>
       <emItem  blockID="i22" id="ShopperReports@ShopperReports.com">
                         <versionRange  minVersion="3.1.22.0" maxVersion="3.1.22.0">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i44" id="sigma@labs.mozilla">
                         </emItem>
       <emItem  blockID="i5" id="support@daemon-tools.cc">
                         <versionRange  minVersion=" " maxVersion="1.0.0.5">
@@ -138,18 +144,20 @@
       <emItem  blockID="i20" id="{AB2CE124-6272-4b12-94A9-7303C7397BD1}">
                         <versionRange  minVersion="0.1" maxVersion="5.2.0.7164" severity="1">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i76" id="crossriderapp3924@crossrider.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
-      <emItem  blockID="i47" id="youtube@youtube2.com">
-                        </emItem>
+      <emItem  blockID="i79" id="GifBlock@facebook.com">
+                        <versionRange  minVersion="0" maxVersion="*">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i62" id="jid0-EcdqvFOgWLKHNJPuqAnawlykCGZ@jetpack">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i17" id="{3252b9ae-c69a-4eaf-9502-dc9c1f6c009e}">
                         <versionRange  minVersion="2.2" maxVersion="2.2">
                     </versionRange>
                   </emItem>
@@ -188,42 +196,21 @@
           </pluginItem>
       <pluginItem  blockID="p33">
       <match name="name" exp="[0-6]\.0\.[01]\d{2}\.\d+" />            <match name="filename" exp="npdeploytk.dll" />              <versionRange  severity="1">
                   </versionRange>
           </pluginItem>
     </pluginItems>
 
   <gfxItems>
-    <gfxBlacklistEntry  blockID="g35">
-      <os>WINNT 6.1</os>
-      <vendor>0x10de</vendor>
-              <devices>
+    <gfxBlacklistEntry  blockID="g35">      <os>WINNT 6.1</os>      <vendor>0x10de</vendor>              <devices>
                       <device>0x0a6c</device>
                   </devices>
-            <feature>DIRECT2D</feature>
-      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>
-      <driverVersion>8.17.12.5896</driverVersion>
-      <driverVersionComparator>LESS_THAN_OR_EQUAL</driverVersionComparator>
-    </gfxBlacklistEntry>
-    <gfxBlacklistEntry  blockID="g36">
-      <os>WINNT 6.1</os>
-      <vendor>0x10de</vendor>
-              <devices>
+            <feature>DIRECT2D</feature>      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>      <driverVersion>8.17.12.5896</driverVersion>      <driverVersionComparator>LESS_THAN_OR_EQUAL</driverVersionComparator>    </gfxBlacklistEntry>
+    <gfxBlacklistEntry  blockID="g36">      <os>WINNT 6.1</os>      <vendor>0x10de</vendor>              <devices>
                       <device>0x0a6c</device>
                   </devices>
-            <feature>DIRECT3D_9_LAYERS</feature>
-      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>
-      <driverVersion>8.17.12.5896</driverVersion>
-      <driverVersionComparator>LESS_THAN_OR_EQUAL</driverVersionComparator>
-    </gfxBlacklistEntry>
-    <gfxBlacklistEntry  blockID="g37">
-      <os>WINNT 5.1</os>
-      <vendor>0x10de</vendor>
-            <feature>DIRECT3D_9_LAYERS</feature>
-      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>
-      <driverVersion>7.0.0.0</driverVersion>
-      <driverVersionComparator>GREATER_THAN_OR_EQUAL</driverVersionComparator>
-    </gfxBlacklistEntry>
+            <feature>DIRECT3D_9_LAYERS</feature>      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>      <driverVersion>8.17.12.5896</driverVersion>      <driverVersionComparator>LESS_THAN_OR_EQUAL</driverVersionComparator>    </gfxBlacklistEntry>
+    <gfxBlacklistEntry  blockID="g37">      <os>WINNT 5.1</os>      <vendor>0x10de</vendor>            <feature>DIRECT3D_9_LAYERS</feature>      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>      <driverVersion>7.0.0.0</driverVersion>      <driverVersionComparator>GREATER_THAN_OR_EQUAL</driverVersionComparator>    </gfxBlacklistEntry>
     </gfxItems>
 
 
 </blocklist>
\ No newline at end of file
--- a/browser/base/content/abouthome/aboutHome.css
+++ b/browser/base/content/abouthome/aboutHome.css
@@ -42,17 +42,16 @@
 %endif
 
 html {
   font: message-box;
   font-size: 100%;
   background-color: hsl(0,0%,90%);
   background-image: url(chrome://browser/content/abouthome/noise.png),
                     -moz-linear-gradient(hsla(0,0%,100%,.7), hsla(0,0%,100%,.4));
-  background-attachment: fixed;
   color: #000;
   height: 100%;
 }
 
 body {
   margin: 0;
   display: -moz-box;
   -moz-box-orient: vertical;
--- a/browser/base/content/abouthome/aboutHome.xhtml
+++ b/browser/base/content/abouthome/aboutHome.xhtml
@@ -92,17 +92,17 @@
     <div class="spacer"/>
 
     <div id="launcher" session="true">
       <button class="launchButton" id="downloads">&abouthome.downloadsButton.label;</button>
       <button class="launchButton" id="bookmarks">&abouthome.bookmarksButton.label;</button>
       <button class="launchButton" id="history">&abouthome.historyButton.label;</button>
       <button class="launchButton" id="apps" hidden="true">&abouthome.appsButton.label;</button>
       <button class="launchButton" id="addons">&abouthome.addonsButton.label;</button>
-      <button class="launchButton" id="sync">&syncBrand.shortName.label;</button>
+      <button class="launchButton" id="sync">&abouthome.syncButton.label;</button>
       <button class="launchButton" id="settings">&abouthome.settingsButton.label;</button>
       <div id="restorePreviousSessionSeparator"/>
       <button class="launchButton" id="restorePreviousSession">&historyRestoreLastSession.label;</button>
     </div>
 
     <a id="aboutMozilla" href="http://www.mozilla.org/about/"/>
   </body>
 </html>
--- a/browser/base/content/browser-tabview.js
+++ b/browser/base/content/browser-tabview.js
@@ -96,19 +96,16 @@ let TabView = {
     goSetCommandEnabled("Browser:ToggleTabView", window.toolbar.visible);
     if (!window.toolbar.visible)
       return;
 
     if (this._initialized)
       return;
 
     if (this.firstUseExperienced) {
-      if ((gBrowser.tabs.length - gBrowser.visibleTabs.length) > 0)
-        this._setBrowserKeyHandlers();
-
       // ___ visibility
       let sessionstore =
         Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
 
       let data = sessionstore.getWindowValue(window, this.VISIBILITY_IDENTIFIER);
       if (data && data == "true") {
         this.show();
       } else {
@@ -136,16 +133,28 @@ let TabView = {
         this._tabCloseEventListener = function(event) {
           if (!self._window && gBrowser.visibleTabs.length == 0)
             self._closedLastVisibleTabBeforeFrameInitialized = true;
         };
         gBrowser.tabContainer.addEventListener(
           "TabShow", this._tabShowEventListener, false);
         gBrowser.tabContainer.addEventListener(
           "TabClose", this._tabCloseEventListener, false);
+
+       if (this._tabBrowserHasHiddenTabs()) {
+         this._setBrowserKeyHandlers();
+       } else {
+         // for restoring last session and undoing recently closed window
+         this._SSWindowStateReadyListener = function (event) {
+           if (this._tabBrowserHasHiddenTabs())
+             this._setBrowserKeyHandlers();
+         }.bind(this);
+         window.addEventListener(
+           "SSWindowStateReady", this._SSWindowStateReadyListener, false);
+        }
       }
     }
 
     Services.prefs.addObserver(this.PREF_BRANCH, this, false);
 
     this._initialized = true;
   },
 
@@ -169,16 +178,20 @@ let TabView = {
     if (this._tabShowEventListener)
       gBrowser.tabContainer.removeEventListener(
         "TabShow", this._tabShowEventListener, false);
 
     if (this._tabCloseEventListener)
       gBrowser.tabContainer.removeEventListener(
         "TabClose", this._tabCloseEventListener, false);
 
+    if (this._SSWindowStateReadyListener)
+      window.removeEventListener(
+        "SSWindowStateReady", this._SSWindowStateReadyListener, false);
+
     this._initialized = false;
   },
 
   // ----------
   // Creates the frame and calls the callback once it's loaded. 
   // If the frame already exists, calls the callback immediately. 
   _initFrame: function TabView__initFrame(callback) {
     let hasCallback = typeof callback == "function";
@@ -225,16 +238,22 @@ let TabView = {
           "TabShow", self._tabShowEventListener, false);
         self._tabShowEventListener = null;
       }
       if (self._tabCloseEventListener) {
         gBrowser.tabContainer.removeEventListener(
           "TabClose", self._tabCloseEventListener, false);
         self._tabCloseEventListener = null;
       }
+      if (self._SSWindowStateReadyListener) {
+        window.removeEventListener(
+          "SSWindowStateReady", self._SSWindowStateReadyListener, false);
+        self._SSWindowStateReadyListener = null;
+      }
+
       self._initFrameCallbacks.forEach(function (cb) cb());
       self._initFrameCallbacks = [];
     }, false);
 
     this._iframe.setAttribute("src", "chrome://browser/content/tabview.html");
     this._deck.appendChild(this._iframe);
 
     // ___ create tooltip
@@ -277,16 +296,21 @@ let TabView = {
   toggle: function TabView_toggle() {
     if (this.isVisible())
       this.hide();
     else 
       this.show();
   },
 
   // ----------
+  _tabBrowserHasHiddenTabs: function TabView_tabBrowserHasHiddenTabs() {
+    return (gBrowser.tabs.length - gBrowser.visibleTabs.length) > 0;
+  },
+
+  // ----------
   updateContextMenu: function TabView_updateContextMenu(tab, popup) {
     let separator = document.getElementById("context_tabViewNamedGroups");
     let isEmpty = true;
 
     while (popup.firstChild && popup.firstChild != separator)
       popup.removeChild(popup.firstChild);
 
     let self = this;
@@ -338,18 +362,17 @@ let TabView = {
   _setBrowserKeyHandlers: function TabView__setBrowserKeyHandlers() {
     if (this._browserKeyHandlerInitialized)
       return;
 
     this._browserKeyHandlerInitialized = true;
 
     let self = this;
     window.addEventListener("keypress", function(event) {
-      if (self.isVisible() ||
-          (gBrowser.tabs.length - gBrowser.visibleTabs.length) == 0)
+      if (self.isVisible() || !self._tabBrowserHasHiddenTabs())
         return;
 
       let charCode = event.charCode;
       // Control (+ Shift) + `
       if (event.ctrlKey && !event.metaKey && !event.altKey &&
           (charCode == 96 || charCode == 126)) {
         event.stopPropagation();
         event.preventDefault();
--- 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.basicPassword =
+          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/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -96,17 +96,17 @@ let gSyncPane = {
     if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
         Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
     } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
                Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
       this.needsUpdate();
     } else {
       this.page = PAGE_HAS_ACCOUNT;
-      document.getElementById("accountName").value = Weave.Service.account;
+      document.getElementById("accountName").value = Weave.Identity.account;
       document.getElementById("syncComputerName").value = Weave.Clients.localName;
       document.getElementById("tosPP").hidden = this._usingCustomServer;
     }
   },
 
   startOver: function (showDialog) {
     if (showDialog) {
       let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
--- a/browser/components/tabview/test/Makefile.in
+++ b/browser/components/tabview/test/Makefile.in
@@ -165,19 +165,21 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug685692.js \
                  browser_tabview_bug686654.js \
                  browser_tabview_bug696602.js \
                  browser_tabview_bug697390.js \
                  browser_tabview_bug705621.js \
                  browser_tabview_bug706430.js \
                  browser_tabview_bug706736.js \
                  browser_tabview_bug707466.js \
+                 browser_tabview_bug712203.js \
                  browser_tabview_bug715454.js \
                  browser_tabview_bug716880.js \
                  browser_tabview_bug728887.js \
+                 browser_tabview_bug733115.js \
                  browser_tabview_click_group.js \
                  browser_tabview_dragdrop.js \
                  browser_tabview_exit_button.js \
                  browser_tabview_expander.js \
                  browser_tabview_firstrun_pref.js \
                  browser_tabview_group.js \
                  browser_tabview_launch.js \
                  browser_tabview_multiwindow_search.js \
--- a/browser/components/tabview/test/browser_tabview_bug608153.js
+++ b/browser/components/tabview/test/browser_tabview_bug608153.js
@@ -30,19 +30,8 @@ function test() {
       is(cw.GroupItems.getActiveGroupItem(), groupItemOne, "Group one is active");
       is(gBrowser.selectedTab, pinnedTab, "Selected tab is the pinned tab");
 
       finish();
     });
   });
 }
 
-function goToNextGroup() {
-  let utils =
-    QueryInterface(Ci.nsIInterfaceRequestor).
-      getInterface(Ci.nsIDOMWindowUtils);
-
-  const masks = Ci.nsIDOMNSEvent;
-  let mval = 0;
-  mval |= masks.CONTROL_MASK;
-
-  utils.sendKeyEvent("keypress", 0, 96, mval);
-}
--- a/browser/components/tabview/test/browser_tabview_bug626525.js
+++ b/browser/components/tabview/test/browser_tabview_bug626525.js
@@ -141,19 +141,8 @@ function openTabContextPopup(win, tab) {
   win.document.getElementById("tabContextMenu").openPopup(
     tab, "end_after", 0, 0, true, false, {target: tab});
 }
 
 function closeTabContextPopup(win) {
   win.document.getElementById("tabContextMenu").hidePopup();
 }
 
-function goToNextGroup(win) {
-  let utils =
-    win.QueryInterface(Ci.nsIInterfaceRequestor).
-      getInterface(Ci.nsIDOMWindowUtils);
-
-  const masks = Ci.nsIDOMNSEvent;
-  let mval = 0;
-  mval |= masks.CONTROL_MASK;
-
-  utils.sendKeyEvent("keypress", 0, 96, mval);
-}
new file mode 100644
--- /dev/null
+++ b/browser/components/tabview/test/browser_tabview_bug712203.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test() {
+  waitForExplicitFinish();
+
+  // create two groups and each group has one tab item
+  let newState = {
+    windows: [{
+      tabs: [{
+        entries: [{ url: "http://www.google.com" }],
+        hidden: true,
+        attributes: {},
+        extData: {
+          "tabview-tab":
+            '{"bounds":{"left":21,"top":29,"width":204,"height":153},' +
+            '"userSize":null,"url":"http://www.google.com","groupID":1,' +
+            '"imageData":null,"title":null}'
+        }
+      },{
+        entries: [{ url: "about:blank" }],
+        hidden: false,
+        attributes: {},
+        extData: {
+          "tabview-tab":
+            '{"bounds":{"left":315,"top":29,"width":111,"height":84},' +
+            '"userSize":null,"url":"about:blank","groupID":2,' +
+            '"imageData":null,"title":null}'
+        },
+      }],
+      selected:2,
+      _closedTabs: [],
+      extData: {
+        "tabview-groups": '{"nextID":3,"activeGroupId":2}',
+        "tabview-group":
+          '{"1":{"bounds":{"left":15,"top":5,"width":280,"height":232},' +
+          '"userSize":null,"title":"","id":1},' +
+          '"2":{"bounds":{"left":309,"top":5,"width":267,"height":226},' +
+          '"userSize":null,"title":"","id":2}}',
+        "tabview-ui": '{"pageBounds":{"left":0,"top":0,"width":788,"height":548}}'
+      }, sizemode:"normal"
+    }]
+  };
+
+  newWindowWithState(newState, function(win) {
+    registerCleanupFunction(function () win.close());
+
+    let selectedTab = win.gBrowser.selectedTab;
+
+    win.addEventListener("tabviewframeinitialized", function onInit() {
+      win.removeEventListener("tabviewframeinitialized", onInit, false);
+      // ensure the TabView.contentWindow is set.
+      executeSoon(function() {
+        let cw = win.TabView.getContentWindow();
+
+        is(cw.GroupItems.groupItems.length, 2, "There are two groups");
+        is(cw.GroupItems.getActiveGroupItem().id, 1, "The active group item is 1");
+        isnot(selectedTab, win.gBrowser.selectedTab, "The selected tab is different");
+
+        finish();
+      });
+    }, false);
+
+    goToNextGroup(win);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/tabview/test/browser_tabview_bug733115.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  waitForExplicitFinish();
+
+  showTabView(function() {
+    registerCleanupFunction(function() {
+      if (gBrowser.tabs[1])
+        gBrowser.removeTab(gBrowser.tabs[1]);
+      TabView.hide();
+    });
+
+    let contentWindow = TabView.getContentWindow();
+    let groupItem = createEmptyGroupItem(contentWindow, 200, 200, 20);
+    contentWindow.GroupItems.setActiveGroupItem(groupItem);
+
+    // A new tab should be added to the active group and tabview should zoom into it.
+    is(groupItem.getChildren().length, 0, "This group doesn't have any tabitems");
+    whenTabViewIsHidden(function() {
+      is(groupItem.getChildren().length, 1, "This group has one tabitem");
+
+      showTabView(function() {
+        // Ensure that no new tab is added to this non-empty tab group.
+        whenTabViewIsHidden(function() {
+          let tabItems = groupItem.getChildren();
+          is(tabItems.length, 1, "This group has one tabitem");
+          is(tabItems[0].tab, gBrowser.selectedTab, "The same tab");
+
+          tabItems[0].close();
+          groupItem.close();
+
+          finish();
+        });
+        EventUtils.synthesizeKey("VK_ENTER", {}, contentWindow);
+      });
+    });
+    EventUtils.synthesizeKey("VK_ENTER", {}, contentWindow);
+  });
+}
+
--- a/browser/components/tabview/test/head.js
+++ b/browser/components/tabview/test/head.js
@@ -247,17 +247,16 @@ function whenSearchIsDisabled(callback, 
   }
 
   contentWindow.addEventListener("tabviewsearchdisabled", function onSearchDisabled() {
     contentWindow.removeEventListener("tabviewsearchdisabled", onSearchDisabled, false);
     callback();
   }, false);
 }
 
-
 // ----------
 function hideGroupItem(groupItem, callback) {
   if (groupItem.hidden) {
     if (callback)
       callback();
     return;
   }
 
@@ -382,16 +381,31 @@ function togglePrivateBrowsing(callback)
 
   let pb = Cc["@mozilla.org/privatebrowsing;1"].
            getService(Ci.nsIPrivateBrowsingService);
 
   pb.privateBrowsingEnabled = !pb.privateBrowsingEnabled;
 }
 
 // ----------
+function goToNextGroup(win) {
+  win = win || window;
+
+  let utils =
+    win.QueryInterface(Ci.nsIInterfaceRequestor).
+      getInterface(Ci.nsIDOMWindowUtils);
+
+  const masks = Ci.nsIDOMNSEvent;
+  let mval = 0;
+  mval |= masks.CONTROL_MASK;
+
+  utils.sendKeyEvent("keypress", 0, 96, mval);
+}
+
+// ----------
 function whenAppTabIconAdded(callback, win) {
   win = win || window;
 
   let contentWindow = win.TabView.getContentWindow();
   let groupItems = contentWindow.GroupItems.groupItems;
   let groupItem = groupItems[(groupItems.length - 1)];
 
   groupItem.addSubscriber("appTabIconAdded", function onAppTabIconAdded() {
--- a/browser/components/tabview/ui.js
+++ b/browser/components/tabview/ui.js
@@ -1188,16 +1188,17 @@ let UI = {
           }
         });
 
         return match && match[1];
       }
 
       let preventDefault = true;
       let activeTab;
+      let activeGroupItem;
       let norm = null;
       switch (event.keyCode) {
         case KeyEvent.DOM_VK_RIGHT:
           norm = function(a, me){return a.x > me.x};
           break;
         case KeyEvent.DOM_VK_LEFT:
           norm = function(a, me){return a.x < me.x};
           break;
@@ -1205,36 +1206,45 @@ let UI = {
           norm = function(a, me){return a.y > me.y};
           break;
         case KeyEvent.DOM_VK_UP:
           norm = function(a, me){return a.y < me.y}
           break;
       }
 
       if (norm != null) {
-        var nextTab = getClosestTabBy(norm);
+        let nextTab = getClosestTabBy(norm);
         if (nextTab) {
           if (nextTab.isStacked && !nextTab.parent.expanded)
             nextTab = nextTab.parent.getChild(0);
           self.setActive(nextTab);
         }
       } else {
         switch(event.keyCode) {
           case KeyEvent.DOM_VK_ESCAPE:
-            let activeGroupItem = GroupItems.getActiveGroupItem();
+            activeGroupItem = GroupItems.getActiveGroupItem();
             if (activeGroupItem && activeGroupItem.expanded)
               activeGroupItem.collapse();
             else
               self.exit();
             break;
           case KeyEvent.DOM_VK_RETURN:
           case KeyEvent.DOM_VK_ENTER:
-            activeTab = self.getActiveTab();
-            if (activeTab)
-              activeTab.zoomIn();
+            activeGroupItem = GroupItems.getActiveGroupItem();
+            if (activeGroupItem) {
+              activeTab = self.getActiveTab();
+
+              if (!activeTab || activeTab.parent != activeGroupItem)
+                activeTab = activeGroupItem.getActiveTab();
+
+              if (activeTab)
+                activeTab.zoomIn();
+              else
+                activeGroupItem.newTab();
+            }
             break;
           case KeyEvent.DOM_VK_TAB:
             // tab/shift + tab to go to the next tab.
             activeTab = self.getActiveTab();
             if (activeTab) {
               let tabItems = (activeTab.parent ? activeTab.parent.getChildren() :
                               [activeTab]);
               let length = tabItems.length;
--- a/browser/devtools/debugger/debugger.js
+++ b/browser/devtools/debugger/debugger.js
@@ -306,16 +306,20 @@ var StackFrames = {
       objClient.getSignature(function SF_getSignature(aResponse) {
         for (let i = 0; i < aResponse.parameters.length; i++) {
           let param = aResponse.parameters[i];
           let paramVar = localScope.addVar(param);
           let paramVal = frame.arguments[i];
           paramVar.setGrip(paramVal);
           this._addExpander(paramVar, paramVal);
         }
+        // Signal that call parameters have been fetched.
+        let evt = document.createEvent("Event");
+        evt.initEvent("Debugger:FetchedParameters", true, false);
+        document.documentElement.dispatchEvent(evt);
       }.bind(this));
     }
   },
 
   /**
    * Update the source editor current debug location based on the selected frame
    * and script.
    */
@@ -464,44 +468,35 @@ var SourceScripts = {
    *        The thread client.
    * @param function aCallback
    *        The next function in the initialization sequence.
    */
   connect: function SS_connect(aThreadClient, aCallback) {
     DebuggerView.Scripts.addChangeListener(this.onChange);
 
     this.activeThread = aThreadClient;
-    aThreadClient.addListener("paused", this.onPaused);
     aThreadClient.addListener("scriptsadded", this.onScripts);
     aThreadClient.addListener("scriptscleared", this.onScriptsCleared);
     this.clearLabelsCache();
     this.onScriptsCleared();
+    // Retrieve the list of scripts known to the server from before the client
+    // was ready to handle new script notifications.
+    this.activeThread.fillScripts();
     aCallback && aCallback();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function TS_disconnect() {
-    this.activeThread.removeListener("paused", this.onPaused);
     this.activeThread.removeListener("scriptsadded", this.onScripts);
     this.activeThread.removeListener("scriptscleared", this.onScriptsCleared);
   },
 
   /**
-   * Handler for the thread client's paused notification. This is triggered only
-   * once, to retrieve the list of scripts known to the server from before the
-   * client was ready to handle new script notifications.
-   */
-  onPaused: function SS_onPaused() {
-    this.activeThread.removeListener("paused", this.onPaused);
-    this.activeThread.fillScripts();
-  },
-
-  /**
    * Handler for the debugger client's unsolicited newScript notification.
    */
   onNewScript: function SS_onNewScript(aNotification, aPacket) {
     this._addScript({ url: aPacket.url, startLine: aPacket.startLine });
   },
 
   /**
    * Handler for the thread client's scriptsadded notification.
@@ -652,16 +647,15 @@ var SourceScripts = {
       window.editor.setText(aScript.text);
       window.updateEditorBreakpoints();
       StackFrames.updateEditor();
     }
     window.editor.resetUndo();
   }
 };
 
-SourceScripts.onPaused = SourceScripts.onPaused.bind(SourceScripts);
 SourceScripts.onScripts = SourceScripts.onScripts.bind(SourceScripts);
 SourceScripts.onNewScript = SourceScripts.onNewScript.bind(SourceScripts);
 SourceScripts.onScriptsCleared = SourceScripts.onScriptsCleared.bind(SourceScripts);
 SourceScripts.onChange = SourceScripts.onChange.bind(SourceScripts);
 
 window.addEventListener("DOMContentLoaded", initDebugger, false);
 window.addEventListener("unload", shutdownDebugger, false);
--- a/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js
@@ -23,17 +23,17 @@ function test()
   let SourceEditor = tempScope.SourceEditor;
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.debuggerWindow;
 
-    gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+    gPane.activeThread.addOneTimeListener("framesadded", function() {
       Services.tm.currentThread.dispatch({ run: onScriptsAdded }, 0);
     });
     gDebuggee.firstCall();
   });
 
   function onScriptsAdded()
   {
     gScripts = gDebugger.DebuggerView.Scripts;
--- a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js
@@ -21,17 +21,17 @@ function test()
   let contextMenu = null;
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.debuggerWindow;
 
-    gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+    gPane.activeThread.addOneTimeListener("framesadded", function() {
       Services.tm.currentThread.dispatch({ run: onScriptsAdded }, 0);
     });
     gDebuggee.firstCall();
   });
 
   function onScriptsAdded()
   {
     let scripts = gDebugger.DebuggerView.Scripts._scripts;
--- a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js
+++ b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js
@@ -27,18 +27,17 @@ function test_early_debugger_statement(a
 {
   let paused = function(aEvent, aPacket) {
     ok(false, "Pause shouldn't be called before we've attached!\n");
     finish_test();
   };
   gClient.addListener("paused", paused);
   // This should continue without nesting an event loop and calling
   // the onPaused hook, because we haven't attached yet.
-  // TODO: uncomment this when bug 723563 is fixed.
-  //gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
+  gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
 
   gClient.removeListener("paused", paused);
 
   // Now attach and resume...
   gClient.request({ to: aActor.threadActor, type: "attach" }, function(aResponse) {
     gClient.request({ to: aActor.threadActor, type: "resume" }, function(aResponse) {
       test_debugger_statement(aActor);
     });
--- a/browser/devtools/debugger/test/browser_dbg_listtabs.js
+++ b/browser/devtools/debugger/test/browser_dbg_listtabs.js
@@ -82,17 +82,17 @@ function test_attach_removed_tab()
   removeTab(gTab2);
   gTab2 = null;
   gClient.addListener("paused", function(aEvent, aPacket) {
     ok(false, "Attaching to an exited tab actor shouldn't generate a pause.");
     finish_test();
   });
 
   gClient.request({ to: gTab2Actor, type: "attach" }, function(aResponse) {
-    is(aResponse.error, "noSuchActor", "Tab should be gone.");
+    is(aResponse.type, "exited", "Tab should consider itself exited.");
     finish_test();
   });
 }
 
 function finish_test()
 {
   gClient.close(function() {
     finish();
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-01.js
@@ -80,16 +80,17 @@ function testSimpleCall() {
 
   gDebuggee.simpleCall();
 }
 
 function resumeAndFinish() {
   gDebugger.StackFrames.activeThread.resume(function() {
     let vs = gDebugger.DebuggerView.Scripts;
     let ss = gDebugger.SourceScripts;
+    ss.onScriptsCleared();
 
     is(ss._trimUrlQuery("a/b/c.d?test=1&random=4"), "a/b/c.d",
       "Trimming the url query isn't done properly.");
 
     let urls = [
       { href: "ici://some.address.com/random/", leaf: "subrandom/" },
       { href: "ni://another.address.org/random/subrandom/", leaf: "page.html" },
       { href: "san://interesting.address.gro/random/", leaf: "script.js" },
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-07.js
@@ -20,19 +20,18 @@ function test()
     gDebugger = gPane.debuggerWindow;
 
     testFrameParameters();
   });
 }
 
 function testFrameParameters()
 {
-  // scriptsadded is fired last when switching to a paused state, so the
-  // property view will have had a chance to fetch the call parameters.
-  gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+  gDebugger.addEventListener("Debugger:FetchedParameters", function test() {
+    gDebugger.removeEventListener("Debugger:FetchedParameters", test, false);
     Services.tm.currentThread.dispatch({ run: function() {
 
       var frames = gDebugger.DebuggerView.Stackframes._frames,
           childNodes = frames.childNodes,
           localScope = gDebugger.DebuggerView.Properties.localScope,
           localNodes = localScope.querySelector(".details").childNodes;
 
       is(gDebugger.StackFrames.activeThread.state, "paused",
@@ -65,17 +64,17 @@ function testFrameParameters()
       is(localNodes[6].querySelector(".info").textContent, "null",
         "Should have the right property value for 'eArg'.");
 
       is(localNodes[7].querySelector(".info").textContent, "undefined",
         "Should have the right property value for 'fArg'.");
 
       resumeAndFinish();
     }}, 0);
-  });
+  }, false);
 
   EventUtils.sendMouseEvent({ type: "click" },
     content.document.querySelector("button"),
     content.window);
 }
 
 function resumeAndFinish() {
   gPane.activeThread.addOneTimeListener("framescleared", function() {
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-08.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-08.js
@@ -20,19 +20,18 @@ function test()
     gDebugger = gPane.debuggerWindow;
 
     testFrameParameters();
   });
 }
 
 function testFrameParameters()
 {
-  // scriptsadded is fired last when switching to a paused state, so the
-  // property view will have had a chance to fetch the call parameters.
-  gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+  gDebugger.addEventListener("Debugger:FetchedParameters", function test() {
+    gDebugger.removeEventListener("Debugger:FetchedParameters", test, false);
     Services.tm.currentThread.dispatch({ run: function() {
 
       var frames = gDebugger.DebuggerView.Stackframes._frames,
           localScope = gDebugger.DebuggerView.Properties.localScope,
           localNodes = localScope.querySelector(".details").childNodes;
 
       is(gDebugger.StackFrames.activeThread.state, "paused",
         "Should only be getting stack frames while paused.");
@@ -81,17 +80,17 @@ function testFrameParameters()
 
         is(localNodes[1].querySelector(".property > .title > .value")
                         .textContent, 5,
           "Should have the right argument length.");
 
         resumeAndFinish();
       }, 100);
     }}, 0);
-  });
+  }, false);
 
   EventUtils.synthesizeMouseAtCenter(content.document.querySelector("button"),
                                      {},
                                      content.window);
 }
 
 function resumeAndFinish() {
   gPane.activeThread.addOneTimeListener("framescleared", function() {
--- a/browser/devtools/debugger/test/browser_dbg_script-switching.js
+++ b/browser/devtools/debugger/test/browser_dbg_script-switching.js
@@ -26,17 +26,17 @@ function test()
     gPane = aPane;
     gDebugger = gPane.debuggerWindow;
 
     testScriptsDisplay();
   });
 }
 
 function testScriptsDisplay() {
-  gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+  gPane.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
       gScripts = gDebugger.DebuggerView.Scripts._scripts;
 
       is(gDebugger.StackFrames.activeThread.state, "paused",
         "Should only be getting stack frames while paused.");
 
       is(gScripts.itemCount, 2, "Found the expected number of scripts.");
 
--- a/browser/devtools/debugger/test/browser_dbg_stack-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-05.js
@@ -21,17 +21,17 @@ function test() {
     gPane = aPane;
     gDebugger = gPane.debuggerWindow;
 
     testRecurse();
   });
 }
 
 function testRecurse() {
-  gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+  gPane.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
       let frames = gDebugger.DebuggerView.Stackframes._frames;
       let childNodes = frames.childNodes;
 
       is(frames.querySelectorAll(".dbg-stackframe").length, 4,
         "Correct number of frames.");
 
       is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length,
--- a/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js
+++ b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js
@@ -26,17 +26,17 @@ function test()
     gPane = aPane;
     gDebugger = gPane.debuggerWindow;
 
     testScriptsDisplay();
   });
 }
 
 function testScriptsDisplay() {
-  gPane.activeThread.addOneTimeListener("scriptsadded", function() {
+  gPane.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
       gScripts = gDebugger.DebuggerView.Scripts._scripts;
 
       is(gDebugger.StackFrames.activeThread.state, "paused",
         "Should only be getting stack frames while paused.");
 
       is(gScripts.itemCount, 2, "Found the expected number of scripts.");
 
--- a/browser/devtools/webconsole/test/browser_gcli_break.js
+++ b/browser/devtools/webconsole/test/browser_gcli_break.js
@@ -68,17 +68,17 @@ function testCreateCommands() {
   type("break add line");
   is(requisition.getStatus().toString(), "ERROR", "break add line is ERROR");
 
   let pane = DebuggerUI.toggleDebugger();
   pane.onConnected = function test_onConnected(aPane) {
     // Wait for the initial resume.
     aPane.debuggerWindow.gClient.addOneTimeListener("resumed", function() {
       delete aPane.onConnected;
-      aPane.debuggerWindow.gClient.activeThread.addOneTimeListener("scriptsadded", function() {
+      aPane.debuggerWindow.gClient.activeThread.addOneTimeListener("framesadded", function() {
         type("break add line " + TEST_URI + " " + content.wrappedJSObject.line0);
         is(requisition.getStatus().toString(), "VALID", "break add line is VALID");
         requisition.exec();
 
         type("break list");
         is(requisition.getStatus().toString(), "VALID", "break list is VALID");
         requisition.exec();
 
--- a/browser/locales/en-US/chrome/browser/aboutHome.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutHome.dtd
@@ -19,8 +19,9 @@
 <!ENTITY abouthome.defaultSnippet2.v1 "It's easy to customize your Firefox exactly the way you want it. <a>Choose from thousands of add-ons</a>.">
 
 <!ENTITY abouthome.bookmarksButton.label "Bookmarks">
 <!ENTITY abouthome.historyButton.label   "History">
 <!ENTITY abouthome.settingsButton.label  "Settings">
 <!ENTITY abouthome.addonsButton.label    "Add-ons">
 <!ENTITY abouthome.appsButton.label      "Marketplace">
 <!ENTITY abouthome.downloadsButton.label "Downloads">
+<!ENTITY abouthome.syncButton.label      "&syncBrand.shortName.label;">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -25,17 +25,17 @@ runningState=Running
 # LOCALIZATION NOTE (localScope): The label that is displayed in the variables
 # pane as a header on the local scope container.
 localScope=Local
 
 # LOCALIZATION NOTE (globalScope): The label that is displayed in the variables
 # pane as a header on the globel scope container.
 globalScope=Global
 
-# LOCALIZATION NOTE (localScope): The label that is displayed in the variables
+# LOCALIZATION NOTE (withScope): The label that is displayed in the variables
 # pane as a header on the container for identifiers in a with block.
 withScope=With block
 
 # LOCALIZATION NOTE (closureScope): The label that is displayed in the variables
 # pane as a header on container for identifiers in a closure scope.
 closureScope=Closure
 
 # LOCALIZATION NOTE (emptyText): The text that is displayed in the stack frames
--- a/browser/locales/en-US/searchplugins/google.xml
+++ b/browser/locales/en-US/searchplugins/google.xml
@@ -8,17 +8,17 @@
 #else
 #define GOOGLE_CLIENT_PARAM <MozParam name="client" condition="defaultEngine" trueValue="firefox-a" falseValue="firefox"/>
 #endif
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>Google</ShortName>
 <Description>Google Search</Description>
 <InputEncoding>UTF-8</InputEncoding>
 <Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
-<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
 <Url type="text/html" method="GET" template="https://www.google.com/search">
 #expand   __GOOGLE_PARAMS__
 #expand   __GOOGLE_CLIENT_PARAM__
 </Url>
 <!-- Keyword search URL is the same as the default, but with an additional parameter -->
 <Url type="application/x-moz-keywordsearch" method="GET" template="https://www.google.com/search">
 #expand   __GOOGLE_PARAMS__
 #expand   __GOOGLE_CLIENT_PARAM__
--- a/content/base/src/nsGenericElement.cpp
+++ b/content/base/src/nsGenericElement.cpp
@@ -4979,16 +4979,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
     }
 
     nsAutoString classes;
     const nsAttrValue* classAttrValue = tmp->GetClasses();
     if (classAttrValue) {
       classes.AppendLiteral(" class='");
       nsAutoString classString;
       classAttrValue->ToString(classString);
+      classString.ReplaceChar(PRUnichar('\n'), PRUnichar(' '));
       classes.Append(classString);
       classes.AppendLiteral("'");
     }
 
     const char* nsuri = nsid < ArrayLength(kNSURIs) ? kNSURIs[nsid] : "";
     PR_snprintf(name, sizeof(name), "nsGenericElement%s %s%s%s %s",
                 nsuri,
                 localName.get(),
--- a/dom/base/ScreenOrientation.h
+++ b/dom/base/ScreenOrientation.h
@@ -7,17 +7,17 @@
 
 namespace mozilla {
 namespace dom {
 
 // Make sure that any change here is also made in
 // * mobile/android/base/GeckoScreenOrientationListener.java
 // * embedding/android/GeckoScreenOrientationListener.java
 enum ScreenOrientation {
-  eScreenOrientation_Current            = 0,
+  eScreenOrientation_None               = 0,
   eScreenOrientation_PortraitPrimary    = 1,  // 00000001
   eScreenOrientation_PortraitSecondary  = 2,  // 00000010
   eScreenOrientation_Portrait           = 3,  // 00000011
   eScreenOrientation_LandscapePrimary   = 4,  // 00000100
   eScreenOrientation_LandscapeSecondary = 8,  // 00001000
   eScreenOrientation_Landscape          = 12, // 00001100
   eScreenOrientation_EndGuard
 };
@@ -25,17 +25,17 @@ enum ScreenOrientation {
 /**
  * ScreenOrientationWrapper is a class wrapping ScreenOrientation so it can be
  * used with Observer<T> which is taking a class, not an enum.
  * C++11 should make this useless.
  */
 class ScreenOrientationWrapper {
 public:
   ScreenOrientationWrapper()
-    : orientation(eScreenOrientation_Current)
+    : orientation(eScreenOrientation_None)
   {}
 
   ScreenOrientationWrapper(ScreenOrientation aOrientation)
     : orientation(aOrientation)
   {}
 
   ScreenOrientation orientation;
 };
@@ -48,15 +48,15 @@ namespace IPC {
 /**
  * Screen orientation serializer.
  * Note that technically, 5, 6, 7, 9, 10 and 11 are illegal values but will
  * not make the serializer to fail. We might want to write our own serializer.
  */
 template <>
 struct ParamTraits<mozilla::dom::ScreenOrientation>
   : public EnumSerializer<mozilla::dom::ScreenOrientation,
-                          mozilla::dom::eScreenOrientation_Current,
+                          mozilla::dom::eScreenOrientation_None,
                           mozilla::dom::eScreenOrientation_EndGuard>
 {};
 
 } // namespace IPC
 
 #endif // mozilla_dom_ScreenOrientation_h
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -2243,15 +2243,15 @@ nsDOMWindowUtils::GetPlugins(JSContext* 
 
   nsresult rv;
   nsCOMPtr<nsIDocument> doc = do_QueryInterface(ddoc, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsTArray<nsIObjectLoadingContent*> plugins;
   doc->GetPlugins(plugins);
 
-  JSObject* jsPlugins = nsnull;
-  rv = nsTArrayToJSArray(cx, plugins, &jsPlugins);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  *aPlugins = OBJECT_TO_JSVAL(jsPlugins);
+  JSObject* jsPlugins = nsnull;
+  rv = nsTArrayToJSArray(cx, plugins, &jsPlugins);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  *aPlugins = OBJECT_TO_JSVAL(jsPlugins);
   return NS_OK;
 }
--- a/dom/base/nsScreen.cpp
+++ b/dom/base/nsScreen.cpp
@@ -82,38 +82,42 @@ nsScreen::Initialize()
                                "dom.screenEnabledProperty.enabled");
   Preferences::AddBoolVarCache(&sAllowScreenBrightnessProperty,
                                "dom.screenBrightnessProperty.enabled");
 }
 
 /* static */ already_AddRefed<nsScreen>
 nsScreen::Create(nsPIDOMWindow* aWindow)
 {
+  MOZ_ASSERT(aWindow);
+
   if (!sInitialized) {
     Initialize();
   }
 
   if (!aWindow->GetDocShell()) {
     return nsnull;
   }
 
-  nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(aWindow);
+  nsCOMPtr<nsIScriptGlobalObject> sgo =
+    do_QueryInterface(static_cast<nsPIDOMWindow*>(aWindow));
   NS_ENSURE_TRUE(sgo, nsnull);
 
   nsRefPtr<nsScreen> screen = new nsScreen();
   screen->BindToOwner(aWindow);
   screen->mIsChrome = IsChromeType(aWindow->GetDocShell());
 
   hal::RegisterScreenOrientationObserver(screen);
   hal::GetCurrentScreenOrientation(&(screen->mOrientation));
 
   return screen.forget();
 }
 
 nsScreen::nsScreen()
+  : mEventListener(nsnull)
 {
 }
 
 nsScreen::~nsScreen()
 {
   hal::UnregisterScreenOrientationObserver(this);
 }
 
@@ -375,17 +379,17 @@ nsScreen::SetMozBrightness(double aBrigh
 }
 
 void
 nsScreen::Notify(const ScreenOrientationWrapper& aOrientation)
 {
   ScreenOrientation previousOrientation = mOrientation;
   mOrientation = aOrientation.orientation;
 
-  NS_ASSERTION(mOrientation != eScreenOrientation_Current &&
+  NS_ASSERTION(mOrientation != eScreenOrientation_None &&
                mOrientation != eScreenOrientation_EndGuard &&
                mOrientation != eScreenOrientation_Portrait &&
                mOrientation != eScreenOrientation_Landscape,
                "Invalid orientation value passed to notify method!");
 
   if (mOrientation != previousOrientation) {
     // TODO: use an helper method, see bug 720768.
     nsRefPtr<nsDOMEvent> event = new nsDOMEvent(nsnull, nsnull);
@@ -406,17 +410,17 @@ nsScreen::Notify(const ScreenOrientation
     }
   }
 }
 
 NS_IMETHODIMP
 nsScreen::GetMozOrientation(nsAString& aOrientation)
 {
   switch (mOrientation) {
-    case eScreenOrientation_Current:
+    case eScreenOrientation_None:
     case eScreenOrientation_EndGuard:
     case eScreenOrientation_Portrait:
     case eScreenOrientation_Landscape:
       NS_ASSERTION(false, "Shouldn't be used when getting value!");
       return NS_ERROR_FAILURE;
     case eScreenOrientation_PortraitPrimary:
       aOrientation.AssignLiteral("portrait-primary");
       break;
@@ -428,8 +432,91 @@ nsScreen::GetMozOrientation(nsAString& a
       break;
     case eScreenOrientation_LandscapeSecondary:
       aOrientation.AssignLiteral("landscape-secondary");
       break;
   }
 
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsScreen::MozLockOrientation(const nsAString& aOrientation, bool* aReturn)
+{
+  ScreenOrientation orientation;
+
+  if (aOrientation.EqualsLiteral("portrait")) {
+    orientation = eScreenOrientation_Portrait;
+  } else if (aOrientation.EqualsLiteral("portrait-primary")) {
+    orientation = eScreenOrientation_PortraitPrimary;
+  } else if (aOrientation.EqualsLiteral("portrait-secondary")) {
+    orientation = eScreenOrientation_PortraitSecondary;
+  } else if (aOrientation.EqualsLiteral("landscape")) {
+    orientation = eScreenOrientation_Landscape;
+  } else if (aOrientation.EqualsLiteral("landscape-primary")) {
+    orientation = eScreenOrientation_LandscapePrimary;
+  } else if (aOrientation.EqualsLiteral("landscape-secondary")) {
+    orientation = eScreenOrientation_LandscapeSecondary;
+  } else {
+    *aReturn = false;
+    return NS_OK;
+  }
+
+  if (!GetOwner()) {
+    *aReturn = false;
+    return NS_OK;
+  }
+
+  if (!IsChromeType(GetOwner()->GetDocShell())) {
+    bool fullscreen;
+    GetOwner()->GetFullScreen(&fullscreen);
+    if (!fullscreen) {
+      *aReturn = false;
+      return NS_OK;
+    }
+
+    nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(GetOwner());
+    if (!target) {
+      *aReturn = false;
+      return NS_OK;
+    }
+
+    if (!mEventListener) {
+      mEventListener = new FullScreenEventListener();
+    }
+
+    target->AddSystemEventListener(NS_LITERAL_STRING("mozfullscreenchange"),
+                                   mEventListener, true);
+  }
+
+  *aReturn = hal::LockScreenOrientation(orientation);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScreen::MozUnlockOrientation()
+{
+  hal::UnlockScreenOrientation();
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS1(nsScreen::FullScreenEventListener, nsIDOMEventListener)
+
+NS_IMETHODIMP
+nsScreen::FullScreenEventListener::HandleEvent(nsIDOMEvent* aEvent)
+{
+#ifdef DEBUG
+  nsAutoString eventType;
+  aEvent->GetType(eventType);
+
+  MOZ_ASSERT(eventType.EqualsLiteral("mozfullscreenchange"));
+#endif
+
+  nsCOMPtr<nsIDOMEventTarget> target;
+  aEvent->GetCurrentTarget(getter_AddRefs(target));
+
+  target->RemoveSystemEventListener(NS_LITERAL_STRING("mozfullscreenchange"),
+                                    this, true);
+
+  hal::UnlockScreenOrientation();
+
+  return NS_OK;
+}
--- a/dom/base/nsScreen.h
+++ b/dom/base/nsScreen.h
@@ -74,23 +74,34 @@ protected:
   nsresult GetRect(nsRect& aRect);
   nsresult GetAvailRect(nsRect& aRect);
 
   bool mIsChrome;
 
   mozilla::dom::ScreenOrientation mOrientation;
 
 private:
+  class FullScreenEventListener : public nsIDOMEventListener
+  {
+  public:
+    FullScreenEventListener() {};
+
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIDOMEVENTLISTENER
+  };
+
   nsScreen();
   virtual ~nsScreen();
 
   static bool sInitialized;
   static bool sAllowScreenEnabledProperty;
   static bool sAllowScreenBrightnessProperty;
 
   static void Initialize();
 
   bool IsWhiteListed();
 
+  nsRefPtr<FullScreenEventListener> mEventListener;
+
   NS_DECL_EVENT_HANDLER(mozorientationchange)
 };
 
 #endif /* nsScreen_h___ */
--- a/dom/interfaces/base/nsIDOMScreen.idl
+++ b/dom/interfaces/base/nsIDOMScreen.idl
@@ -34,17 +34,17 @@
  * 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 ***** */
 
 #include "nsIDOMEventTarget.idl"
 
-[scriptable, uuid(6366afc9-0072-4231-a4ec-98cd65f350ef)]
+[scriptable, uuid(8a66b30c-9a32-4b17-ab4e-ca8b7b588243)]
 interface nsIDOMScreen : nsIDOMEventTarget
 {
   readonly attribute long             top;
   readonly attribute long             left;
   readonly attribute long             width;
   readonly attribute long             height;
   readonly attribute long             pixelDepth;
   readonly attribute long             colorDepth;
@@ -79,9 +79,19 @@ interface nsIDOMScreen : nsIDOMEventTarg
   /**
    * Returns the current screen orientation.
    * Can be: landscape-primary, landscape-secondary,
    *         portrait-primary or portrait-secondary.
    */
   readonly attribute DOMString       mozOrientation;
 
   attribute nsIDOMEventListener      onmozorientationchange;
+
+  /**
+   * Lock screen orientation to the specified type.
+   */
+  boolean mozLockOrientation(in DOMString orientation);
+
+  /**
+   * Unlock the screen orientation.
+   */
+  void mozUnlockOrientation();
 };
--- a/dom/tests/mochitest/browser-frame/test_browserFrame4.html
+++ b/dom/tests/mochitest/browser-frame/test_browserFrame4.html
@@ -14,16 +14,19 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 <!--
   Test that an iframe with the |mozbrowser| attribute emits
   mozbrowserX events when this page is in the whitelist.
 -->
 
 <script type="application/javascript;version=1.7">
 
+// Bug 740626: This test fails on Windows XP
+if (navigator.userAgent.indexOf("Windows NT 5.1") == -1) {
+
 SimpleTest.waitForExplicitFinish();
 
 function runTest() {
   browserFrameHelpers.setEnabledPref(true);
   browserFrameHelpers.addToWhitelist();
 
   // Load emptypage1 into the iframe, wait for that to finish loading, then
   // call runTest2.
@@ -145,12 +148,14 @@ function runTest2() {
     SimpleTest.finish();
   }
 
   waitForAllCallbacks();
 }
 
 addEventListener('load', function() { SimpleTest.executeSoon(runTest); });
 
+}
+
 </script>
 
 </body>
 </html>
--- a/editor/libeditor/text/tests/test_bug597331.html
+++ b/editor/libeditor/text/tests/test_bug597331.html
@@ -18,16 +18,20 @@ https://bugzilla.mozilla.org/show_bug.cg
 line2
 line3
 </textarea>
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 597331 **/
+
+// Bug 718316: This test fails on Windows 7
+if (navigator.userAgent.indexOf("Windows NT 6.1") == -1) {
+
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(function() {
   SimpleTest.executeSoon(function() {
     var t = document.querySelector("textarea");
     t.focus();
     t.selectionStart = 4;
     t.selectionEnd = 4;
     SimpleTest.executeSoon(function() {
@@ -60,12 +64,14 @@ addLoadEvent(function() {
           SimpleTest.finish();
         });
       }, false);
       synthesizeKey("VK_LEFT", {});
     });
   });
 });
 
+}
+
 </script>
 </pre>
 </body>
 </html>
--- a/editor/libeditor/text/tests/test_bug600570.html
+++ b/editor/libeditor/text/tests/test_bug600570.html
@@ -18,16 +18,19 @@ https://bugzilla.mozilla.org/show_bug.cg
 aaa
 [bbb]</textarea>
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 600570 **/
 
+// Bug 718316: This test fails on Windows 7
+if (navigator.userAgent.indexOf("Windows NT 6.1") == -1) {
+
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   var t = document.querySelector("textarea");
   t.value = "[aaa\nbbb]";
   t.focus();
   synthesizeKey("A", {accelKey: true});
 
   SimpleTest.executeSoon(function() {
@@ -69,12 +72,14 @@ SimpleTest.waitForFocus(function() {
       },
       function() {
         SimpleTest.finish();
       }
     );
   });
 });
 
+}
+
 </script>
 </pre>
 </body>
 </html>
--- a/embedding/android/GeckoAppShell.java
+++ b/embedding/android/GeckoAppShell.java
@@ -1850,9 +1850,17 @@ public class GeckoAppShell
 
     public static void enableScreenOrientationNotifications() {
         GeckoScreenOrientationListener.getInstance().enableNotifications();
     }
 
     public static void disableScreenOrientationNotifications() {
         GeckoScreenOrientationListener.getInstance().disableNotifications();
     }
+
+    public static void lockScreenOrientation(int aOrientation) {
+        GeckoScreenOrientationListener.getInstance().lockScreenOrientation(aOrientation);
+    }
+
+    public static void unlockScreenOrientation() {
+        GeckoScreenOrientationListener.getInstance().unlockScreenOrientation();
+    }
 }
--- a/embedding/android/GeckoScreenOrientationListener.java
+++ b/embedding/android/GeckoScreenOrientationListener.java
@@ -3,37 +3,43 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.content.Context;
 import android.util.Log;
 import android.view.OrientationEventListener;
 import android.view.Surface;
+import android.content.pm.ActivityInfo;
 
 public class GeckoScreenOrientationListener
 {
+  private static final String LOGTAG = "GeckoScreenOrientationListener";
+
   static class OrientationEventListenerImpl extends OrientationEventListener {
     public OrientationEventListenerImpl(Context c) {
       super(c);
     }
 
     @Override
     public void onOrientationChanged(int aOrientation) {
       GeckoScreenOrientationListener.getInstance().updateScreenOrientation();
     }
   }
 
   static private GeckoScreenOrientationListener sInstance = null;
 
   // Make sure that any change in dom/base/ScreenOrientation.h happens here too.
+  static public final short eScreenOrientation_None               = 0;
   static public final short eScreenOrientation_PortraitPrimary    = 1;
   static public final short eScreenOrientation_PortraitSecondary  = 2;
+  static public final short eScreenOrientation_Portrait           = 3;
   static public final short eScreenOrientation_LandscapePrimary   = 4;
   static public final short eScreenOrientation_LandscapeSecondary = 8;
+  static public final short eScreenOrientation_Landscape          = 12;
 
   private short mOrientation;
   private OrientationEventListenerImpl mListener = null;
 
   // Whether the listener should be listening to changes.
   private boolean mShouldBeListening = false;
   // Whether the listener should notify Gecko that a change happened.
   private boolean mShouldNotify      = false;
@@ -102,21 +108,56 @@ public class GeckoScreenOrientationListe
       mOrientation = eScreenOrientation_PortraitPrimary;
     } else if (rotation == Surface.ROTATION_180) {
       mOrientation = eScreenOrientation_PortraitSecondary;
     } else if (rotation == Surface.ROTATION_270) {
       mOrientation = eScreenOrientation_LandscapeSecondary;
     } else if (rotation == Surface.ROTATION_90) {
       mOrientation = eScreenOrientation_LandscapePrimary;
     } else {
-      Log.e("GeckoScreenOrientationListener", "Unexpected value received! (" + rotation + ")");
+      Log.e(LOGTAG, "Unexpected value received! (" + rotation + ")");
       return;
     }
 
     if (mShouldNotify && mOrientation != previousOrientation) {
       GeckoAppShell.sendEventToGecko(new GeckoEvent(mOrientation));
     }
   }
 
   public short getScreenOrientation() {
     return mOrientation;
   }
+
+  public void lockScreenOrientation(int aOrientation) {
+    int orientation = 0;
+
+    switch (aOrientation) {
+      case eScreenOrientation_PortraitPrimary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+        break;
+      case eScreenOrientation_PortraitSecondary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+        break;
+      case eScreenOrientation_Portrait:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
+        break;
+      case eScreenOrientation_LandscapePrimary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        break;
+      case eScreenOrientation_LandscapeSecondary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+        break;
+      case eScreenOrientation_Landscape:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
+        break;
+      default:
+        Log.e(LOGTAG, "Unexpected value received! (" + aOrientation + ")");
+    }
+
+    GeckoApp.mAppContext.setRequestedOrientation(orientation);
+    updateScreenOrientation();
+  }
+
+  public void unlockScreenOrientation() {
+    GeckoApp.mAppContext.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+    updateScreenOrientation();
+  }
 }
--- a/hal/Hal.cpp
+++ b/hal/Hal.cpp
@@ -561,10 +561,24 @@ GetCurrentScreenOrientation(dom::ScreenO
 
 void
 NotifyScreenOrientationChange(const dom::ScreenOrientation& aScreenOrientation)
 {
   sScreenOrientationObservers.CacheInformation(dom::ScreenOrientationWrapper(aScreenOrientation));
   sScreenOrientationObservers.BroadcastCachedInformation();
 }
 
+bool
+LockScreenOrientation(const dom::ScreenOrientation& aOrientation)
+{
+  AssertMainThread();
+  RETURN_PROXY_IF_SANDBOXED(LockScreenOrientation(aOrientation));
+}
+
+void
+UnlockScreenOrientation()
+{
+  AssertMainThread();
+  PROXY_IF_SANDBOXED(UnlockScreenOrientation());
+}
+
 } // namespace hal
 } // namespace mozilla
--- a/hal/Hal.h
+++ b/hal/Hal.h
@@ -318,16 +318,27 @@ void UnregisterScreenOrientationObserver
 void GetCurrentScreenOrientation(dom::ScreenOrientation* aScreenOrientation);
 
 /**
  * Notify of a change in the screen orientation.
  * @param aScreenOrientation The new screen orientation.
  */
 void NotifyScreenOrientationChange(const dom::ScreenOrientation& aScreenOrientation);
 
+/**
+ * Lock the screen orientation to the specific orientation.
+ * @return Whether the lock has been accepted.
+ */
+bool LockScreenOrientation(const dom::ScreenOrientation& aOrientation);
+
+/**
+ * Unlock the screen orientation.
+ */
+void UnlockScreenOrientation();
+
 } // namespace MOZ_HAL_NAMESPACE
 } // namespace mozilla
 
 #ifdef MOZ_DEFINED_HAL_NAMESPACE
 # undef MOZ_DEFINED_HAL_NAMESPACE
 # undef MOZ_HAL_NAMESPACE
 #endif
 
--- a/hal/android/AndroidHal.cpp
+++ b/hal/android/AndroidHal.cpp
@@ -212,11 +212,34 @@ GetCurrentScreenOrientation(dom::ScreenO
     return;
   }
 
   dom::ScreenOrientationWrapper orientationWrapper;
   bridge->GetScreenOrientation(orientationWrapper);
   *aScreenOrientation = orientationWrapper.orientation;
 }
 
+bool
+LockScreenOrientation(const dom::ScreenOrientation& aOrientation)
+{
+  AndroidBridge* bridge = AndroidBridge::Bridge();
+  if (!bridge) {
+    return false;
+  }
+
+  bridge->LockScreenOrientation(dom::ScreenOrientationWrapper(aOrientation));
+  return true;
+}
+
+void
+UnlockScreenOrientation()
+{
+  AndroidBridge* bridge = AndroidBridge::Bridge();
+  if (!bridge) {
+    return;
+  }
+
+  bridge->UnlockScreenOrientation();
+}
+
 } // hal_impl
 } // mozilla
 
--- a/hal/fallback/ScreenOrientationFallback.cpp
+++ b/hal/fallback/ScreenOrientationFallback.cpp
@@ -36,10 +36,21 @@ GetCurrentScreenOrientation(dom::ScreenO
   screenMgr->GetPrimaryScreen(getter_AddRefs(screen));
   screen->GetRect(&screenLeft, &screenTop, &screenWidth, &screenHeight);
 
   *aScreenOrientation = screenWidth >= screenHeight
                           ? dom::eScreenOrientation_LandscapePrimary
                           : dom::eScreenOrientation_PortraitPrimary;
 }
 
+bool
+LockScreenOrientation(const dom::ScreenOrientation& aOrientation)
+{
+  return false;
+}
+
+void
+UnlockScreenOrientation()
+{
+}
+
 } // hal_impl
 } // mozilla
--- a/hal/sandbox/PHal.ipdl
+++ b/hal/sandbox/PHal.ipdl
@@ -139,16 +139,19 @@ parent:
     DisableWakeLockNotifications();
     sync GetWakeLockInfo(nsString aTopic)
       returns (WakeLockInformation aWakeLockInfo);
 
     EnableScreenOrientationNotifications();
     DisableScreenOrientationNotifications();
     sync GetCurrentScreenOrientation()
       returns (ScreenOrientation aScreenOrientation);
+    sync LockScreenOrientation(ScreenOrientation aOrientation)
+      returns (bool allowed);
+    UnlockScreenOrientation();
 
 child:
     NotifySensorChange(SensorData aSensorData);
 
 parent:    
     EnableSensorNotifications(SensorType aSensor);
     DisableSensorNotifications(SensorType aSensor);
 
--- a/hal/sandbox/SandboxHal.cpp
+++ b/hal/sandbox/SandboxHal.cpp
@@ -106,16 +106,30 @@ DisableScreenOrientationNotifications()
 
 void
 GetCurrentScreenOrientation(ScreenOrientation* aScreenOrientation)
 {
   Hal()->SendGetCurrentScreenOrientation(aScreenOrientation);
 }
 
 bool
+LockScreenOrientation(const dom::ScreenOrientation& aOrientation)
+{
+  bool allowed;
+  Hal()->SendLockScreenOrientation(aOrientation, &allowed);
+  return allowed;
+}
+
+void
+UnlockScreenOrientation()
+{
+  Hal()->SendUnlockScreenOrientation();
+}
+
+bool
 GetScreenEnabled()
 {
   bool enabled = false;
   Hal()->SendGetScreenEnabled(&enabled);
   return enabled;
 }
 
 void
@@ -316,16 +330,30 @@ public:
   }
 
   NS_OVERRIDE virtual bool
   RecvGetCurrentScreenOrientation(ScreenOrientation* aScreenOrientation) {
     hal::GetCurrentScreenOrientation(aScreenOrientation);
     return true;
   }
 
+  NS_OVERRIDE virtual bool
+  RecvLockScreenOrientation(const dom::ScreenOrientation& aOrientation, bool* aAllowed)
+  {
+    *aAllowed = hal::LockScreenOrientation(aOrientation);
+    return true;
+  }
+
+  NS_OVERRIDE virtual bool
+  RecvUnlockScreenOrientation()
+  {
+    hal::UnlockScreenOrientation();
+    return true;
+  }
+
   void Notify(const ScreenOrientationWrapper& aScreenOrientation) {
     unused << SendNotifyScreenOrientationChange(aScreenOrientation.orientation);
   }
 
   NS_OVERRIDE virtual bool
   RecvGetScreenEnabled(bool *enabled)
   {
     *enabled = hal::GetScreenEnabled();
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2047,9 +2047,17 @@ public class GeckoAppShell
 
     public static void enableScreenOrientationNotifications() {
         GeckoScreenOrientationListener.getInstance().enableNotifications();
     }
 
     public static void disableScreenOrientationNotifications() {
         GeckoScreenOrientationListener.getInstance().disableNotifications();
     }
+
+    public static void lockScreenOrientation(int aOrientation) {
+        GeckoScreenOrientationListener.getInstance().lockScreenOrientation(aOrientation);
+    }
+
+    public static void unlockScreenOrientation() {
+        GeckoScreenOrientationListener.getInstance().unlockScreenOrientation();
+    }
 }
--- a/mobile/android/base/GeckoScreenOrientationListener.java
+++ b/mobile/android/base/GeckoScreenOrientationListener.java
@@ -3,37 +3,43 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.content.Context;
 import android.util.Log;
 import android.view.OrientationEventListener;
 import android.view.Surface;
+import android.content.pm.ActivityInfo;
 
 public class GeckoScreenOrientationListener
 {
+  private static final String LOGTAG = "GeckoScreenOrientationListener";
+
   static class OrientationEventListenerImpl extends OrientationEventListener {
     public OrientationEventListenerImpl(Context c) {
       super(c);
     }
 
     @Override
     public void onOrientationChanged(int aOrientation) {
       GeckoScreenOrientationListener.getInstance().updateScreenOrientation();
     }
   }
 
   static private GeckoScreenOrientationListener sInstance = null;
 
   // Make sure that any change in dom/base/ScreenOrientation.h happens here too.
+  static public final short eScreenOrientation_None               = 0;
   static public final short eScreenOrientation_PortraitPrimary    = 1;
   static public final short eScreenOrientation_PortraitSecondary  = 2;
+  static public final short eScreenOrientation_Portrait           = 3;
   static public final short eScreenOrientation_LandscapePrimary   = 4;
   static public final short eScreenOrientation_LandscapeSecondary = 8;
+  static public final short eScreenOrientation_Landscape          = 12;
 
   private short mOrientation;
   private OrientationEventListenerImpl mListener = null;
 
   // Whether the listener should be listening to changes.
   private boolean mShouldBeListening = false;
   // Whether the listener should notify Gecko that a change happened.
   private boolean mShouldNotify      = false;
@@ -102,21 +108,56 @@ public class GeckoScreenOrientationListe
       mOrientation = eScreenOrientation_PortraitPrimary;
     } else if (rotation == Surface.ROTATION_180) {
       mOrientation = eScreenOrientation_PortraitSecondary;
     } else if (rotation == Surface.ROTATION_270) {
       mOrientation = eScreenOrientation_LandscapeSecondary;
     } else if (rotation == Surface.ROTATION_90) {
       mOrientation = eScreenOrientation_LandscapePrimary;
     } else {
-      Log.e("GeckoScreenOrientationListener", "Unexpected value received! (" + rotation + ")");
+      Log.e(LOGTAG, "Unexpected value received! (" + rotation + ")");
       return;
     }
 
     if (mShouldNotify && mOrientation != previousOrientation) {
       GeckoAppShell.sendEventToGecko(GeckoEvent.createScreenOrientationEvent(mOrientation));
     }
   }
 
   public short getScreenOrientation() {
     return mOrientation;
   }
+
+  public void lockScreenOrientation(int aOrientation) {
+    int orientation = 0;
+
+    switch (aOrientation) {
+      case eScreenOrientation_PortraitPrimary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+        break;
+      case eScreenOrientation_PortraitSecondary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+        break;
+      case eScreenOrientation_Portrait:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
+        break;
+      case eScreenOrientation_LandscapePrimary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        break;
+      case eScreenOrientation_LandscapeSecondary:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+        break;
+      case eScreenOrientation_Landscape:
+        orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
+        break;
+      default:
+        Log.e(LOGTAG, "Unexpected value received! (" + aOrientation + ")");
+    }
+
+    GeckoApp.mAppContext.setRequestedOrientation(orientation);
+    updateScreenOrientation();
+  }
+
+  public void unlockScreenOrientation() {
+    GeckoApp.mAppContext.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+    updateScreenOrientation();
+  }
 }
--- a/mobile/locales/en-US/searchplugins/google.xml
+++ b/mobile/locales/en-US/searchplugins/google.xml
@@ -1,13 +1,13 @@
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>Google</ShortName>
 <InputEncoding>UTF-8</InputEncoding>
 <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABvxJREFUeNqUV1tsFVUUXWdm7qMPaKEUChJekgBioFEehhJBEkUjQQkGP5QPSSDCD4nhYZAPYiQaQyTyowJRCBoxEhOx+IMi5aFG5VEIELGFUiilFFrae3sf8zjHfc7M3Dtz70VkyOYMc4ez1l57n733MCEE6FJ/yevg+T5s+qETlgOYpg0zbWJpfRV2vP4oCq/uFSsh9uxBmeCIgsOgZ8wzf1Pahn4BbM8sMtP7fZQQTAuCn76RxruH70KPx6HFYmCe7f+rF5/+fLOIQO3nu6DNfwYOy8OyAhLBNWiSyDXGhBbcsPFiEiISAQwyfyWTz94/1IFS1+Dt22gznbxkeU+CXj3gWYjA9Xs2HMFINgYuoDblJJJgOnqzAl+e7CraIDp9GlBfD5tU8Encz1Di3yECtsNh2VyttiPgSCMmihC9+v3puyVVMObPIxU0FeuHMUnCCBGgzDNNh7wWlIQCtu0QCe6SoLePXrxXkgCqq3JJVpiE/0UAhQRGVOowsxlwTVcK2FINIiFXqUZvximJL6W34YZOBkELyMwLToETsKIQzBpTBjNjEQkLFh1BZZbjKsHFfdIIyLRdow2JsDKEzCkALvwtRGDh1CqUMcclIWsAgVsUEhka0FlnojSBRNNx8lKEzrpVcG8VPPctFIKqMh0rG2rw3o+36GxrsMlrLoPPCZw7ePmJoUXgt/d+BbPtKjJ1MVxpGIqeEXG0T6iASbqb9H+NhI1RrUmMupLEpAt9iA7YOSKqPggRdqsvZWPGlrO40mOraMqYSvChMY6f3p6OaWMH5ZP2Xh++WTUfF5dUIjk6RpsKZIl0loBlumTkartrWj0TmHOkC0sOtKP6TpaqJxHoHbBEdXlICDS3J7Fi12Wcuz6gZH+JPN/22kSMHRbPvZOykvjwxAbctVtQVkZKaZzAPVAf3LtX4LYk4ZGh+zlNXXhrdwtY7apjYvfKyVg8oxb/92rrbcXWY+upCiUUeJQKpjy6pkcgGyCQtgP3niJp716dgj6ivWzHeSzb3oxz1xIPBL/S04rNR9YhpSUgDEo8jYCFC2559cIR7ioPjlB/vHvhHc9AOTR0TajS0XjmDg6d6sLYmhjG1USx6MnhWPNicQfc17wPSUGeK3B3Z9vblJRVJKhs5Ej4xj0SXBQUooig1GHugaTmhI6eFDq7+zFvSvV9io573i3h7iY7IfO840I+lySgSNi+InLlyKnjE1EhiPEsYtxEVJiQZKKUyxGyS22l6/7qmWswJFYHqlXI2kwllzQ/8+XRM7kfEo+MKFQkf6/FnDR8i6s1Q4QyOPJ7Ky5eLSZRVzkCnyz8DJMG1yOdoYSi6UKRcFwSMgGl5Um4IbF8RXwluGv68Mde2GI4FgzHVNYwpQYNk4fh+ZmjEY8wTBg9pLgF61E8N34hOvq78DclpSqxKgeE67UXBkkitxY8k8RUNxxXHcHyRdMwa+ooTB4/DA9zbZy9AdNqp2Prnx+A0VGU441qPCLvteWTcERRbpSshKH2nOmGlaZJiELDjEGIVz9e8r2dzXux88IXNDmRV0y4igS8thw3HLJKmo67ilIEuNWPVHcTMj1nkLl3DproA7M7ILI3qUMyDJq4CTVT15ck8crBlbiUuKxqg+yMjggrYAXAg401RyDdfRI951bDEB2IxsqhGzp0MUCsknDooJtZA6lUBJGRyzHyqY+LCBxuO4E3f3lHqeAw7qkALxThQhXs6qoJpG7/iq4Ti1FZYSIep2NIVU4NWHLaJdM0akoGVYAox0D71+iwdTwy96MQgWfHzUWGToQKgeb1fy8MPgleYmpV88Ct39YiTgBRXdB8S4WFE7CjQ5Y6oXYjAlStDNpcvpO6egADnX8UqSBrg+p+Xm3IqCMpVALyoimVKdPM5A3wgesEDAUsCJSTh45FZhveqqtn1PJIHY3IGki0HCyejEJd0K0L0vvQp49qDCxXjw0rQVkudPJUggj1I3eVd5sHsRSSGIEzHiGicVIiCid5u1gBb4DhKJhMEfQ6MCzKHKgYOZs2LicCpLRFkmuO6gn5WMkoUWCEQUYfKYJmAlGGyjELQuDf/3NCtWSBEsAoAc5ZPgcGjX+VwCtJ/gpiUe4aL1PENEFGq7wHryCViCxqUTF2fojAjlPfEleRl9sHksAByd2PxUAI5PvDZm1EJw0ZZuICfZHZ0Gm6Ycx3QHNDRAo4tmxTNRg8Yy2MyroceFP7WRztOAM1YyHvXdGnkAgQ8d7J1QFuJpFsaUSqtRFO32VoJCdjzJOOEtCoQnT00xhc/0YIvLmrBQv2r0Uv71dFSPVmVvg9xvKfQkE1PAJy7ZXfN6GM7jzjJRIdlWglokMnFiVdU9tZLP1uswfOVQhccFbsPWd5QtwNvlh3nP0rwACsnu1i3ddmfAAAAABJRU5ErkJggg==</Image>
-<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
 <Url type="text/html" method="GET" template="https://www.google.com/m">
   <Param name="q" value="{searchTerms}"/>
   <Param name="ie" value="utf-8"/>
   <Param name="oe" value="utf-8"/>
   <Param name="aq" value="t"/>
   <!-- Dynamic parameters -->
   <Param name="rls" value="{moz:distributionID}:{moz:locale}:{moz:official}"/>
 </Url>
--- a/mobile/xul/chrome/content/sync.js
+++ b/mobile/xul/chrome/content/sync.js
@@ -330,39 +330,39 @@ let WeaveGlue = {
       Weave.Service.serverURL = this.setupData.serverURL;
 
     // We might still be in the middle of a sync from before Sync was disabled, so
     // let's force the UI into a state that the Sync code feels comfortable
     this.observe(null, "", "");
 
     // Now try to re-connect. If successful, this will reset the UI into the
     // correct state automatically.
-    Weave.Service.login(Weave.Service.username, this.setupData.password, this.setupData.synckey);
+    Weave.Service.login(Weave.Identity.username, this.setupData.password, this.setupData.synckey);
   },
 
   connect: function connect(aSetupData) {
     // Use setup data to pre-configure manual fields
     if (aSetupData)
       this.setupData = aSetupData;
 
     // Cause the Sync system to reset internals if we seem to be switching accounts
-    if (this.setupData.account != Weave.Service.account)
+    if (this.setupData.account != Weave.Identity.account)
       Weave.Service.startOver();
 
     // Remove any leftover connection error string
     this._elements.connect.removeAttribute("desc");
 
     // Reset the custom server URL, if we have one
     if (this.setupData.serverURL && this.setupData.serverURL.length)
       Weave.Service.serverURL = this.setupData.serverURL;
 
     // Sync will use the account value and munge it into a username, as needed
-    Weave.Service.account = this.setupData.account;
-    Weave.Service.password = this.setupData.password;
-    Weave.Service.passphrase = this.setupData.synckey;
+    Weave.Identity.account = this.setupData.account;
+    Weave.Identity.basicPassword = this.setupData.password;
+    Weave.Identity.syncKey = this.setupData.synckey;
     Weave.Service.persistLogin();
     Weave.Svc.Obs.notify("weave:service:setup-complete");
     setTimeout(function () { Weave.Service.sync(); }, 0);
   },
 
   disconnect: function disconnect() {
     // Save credentials for undo
     let undoData = this.setupData;
@@ -500,17 +500,17 @@ let WeaveGlue = {
           sync.setAttribute("title", self._bundle.GetStringFromName("lastSyncInProgress2.label"));
       } else {
         connect.firstChild.disabled = false;
         sync.firstChild.disabled = false;
       }
     }, 0, this);
 
     // Dynamically generate some strings
-    let accountStr = this._bundle.formatStringFromName("account.label", [Weave.Service.account], 1);
+    let accountStr = this._bundle.formatStringFromName("account.label", [Weave.Identity.account], 1);
     disconnect.setAttribute("title", accountStr);
 
     // Show the day-of-week and time (HH:MM) of last sync
     let lastSync = Weave.Svc.Prefs.get("lastSync");
     if (lastSync != null) {
       let syncDate = new Date(lastSync).toLocaleFormat("%a %R");
       let dateStr = this._bundle.formatStringFromName("lastSync2.label", [syncDate], 1);
       sync.setAttribute("title", dateStr);
@@ -584,19 +584,19 @@ let WeaveGlue = {
 
     let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
     let url = formatter.formatURLPref("app.sync.tutorialURL");
     BrowserUI.newTab(url, Browser.selectedTab);
   },
 
   loadSetupData: function _loadSetupData() {
     this.setupData = {};
-    this.setupData.account = Weave.Service.account || "";
-    this.setupData.password = Weave.Service.password || "";
-    this.setupData.synckey = Weave.Service.passphrase || "";
+    this.setupData.account = Weave.Identity.account || "";
+    this.setupData.password = Weave.Identity.basicPassword || "";
+    this.setupData.synckey = Weave.Identity.syncKey || "";
 
     let serverURL = Weave.Service.serverURL;
     let defaultPrefs = Services.prefs.getDefaultBranch(null);
     if (serverURL == defaultPrefs.getCharPref("services.sync.serverURL"))
       serverURL = "";
     this.setupData.serverURL = serverURL;
   }
 };
@@ -647,19 +647,19 @@ let SyncPairDevice = {
         this.code2.value.length == PIN_PART_LENGTH &&
         this.code3.value.length == PIN_PART_LENGTH);
   },
 
   connect: function connect() {
     let self = this;
     let jpake = this.jpake = 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};
         jpake.sendAndComplete(credentials);
       },
       onComplete: function onComplete() {
         self.jpake = null;
         self.close();
 
         // Schedule a Sync for soonish to fetch the data uploaded by the
--- 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/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -803,16 +803,21 @@ BookmarksStore.prototype = {
     let itemId = this.idForGUID(record.id);
     if (itemId <= 0) {
       this._log.debug("Item " + record.id + " already removed");
       return;
     }
     this.removeById(itemId, record.id);
   },
 
+  _taggableTypes: ["bookmark", "microsummary", "query"],
+  isTaggable: function isTaggable(recordType) {
+    return this._taggableTypes.indexOf(recordType) != -1;
+  },
+
   update: function BStore_update(record) {
     let itemId = this.idForGUID(record.id);
 
     if (itemId <= 0) {
       this._log.debug("Skipping update for unknown item: " + record.id);
       return;
     }
 
@@ -848,17 +853,21 @@ BookmarksStore.prototype = {
       case "title":
         PlacesUtils.bookmarks.setItemTitle(itemId, val);
         break;
       case "bmkUri":
         PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val));
         break;
       case "tags":
         if (Array.isArray(val)) {
-          this._tagURI(PlacesUtils.bookmarks.getBookmarkURI(itemId), val);
+          if (this.isTaggable(remoteRecordType)) {
+            this._tagID(itemId, val);
+          } else {
+            this._log.debug("Remote record type is invalid for tags: " + remoteRecordType);
+          }
         }
         break;
       case "keyword":
         PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val);
         break;
       case "description":
         if (val) {
           PlacesUtils.annotations.setItemAnnotation(
@@ -1212,25 +1221,56 @@ BookmarksStore.prototype = {
       finally {
         node.containerOpen = false;
       }
     }
 
     return items;
   },
 
-  _tagURI: function BStore_tagURI(bmkURI, tags) {
+  /**
+   * Associates the URI of the item with the provided ID with the
+   * provided array of tags.
+   * If the provided ID does not identify an item with a URI,
+   * returns immediately.
+   */
+  _tagID: function _tagID(itemID, tags) {
+    if (!itemID || !tags) {
+      return;
+    }
+
+    try {
+      let u = PlacesUtils.bookmarks.getBookmarkURI(itemID);
+      this._tagURI(u, tags);
+    } catch (e) {
+      this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. " +
+                     Utils.exceptionStr(e));
+
+      // I guess it doesn't have a URI. Don't try to tag it.
+      return;
+    }
+  },
+
+  /**
+   * Associate the provided URI with the provided array of tags.
+   * If the provided URI is falsy, returns immediately.
+   */
+  _tagURI: function _tagURI(bookmarkURI, tags) {
+    if (!bookmarkURI || !tags) {
+      return;
+    }
+
     // Filter out any null/undefined/empty tags.
     tags = tags.filter(function(t) t);
 
     // Temporarily tag a dummy URI to preserve tag ids when untagging.
     let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
     PlacesUtils.tagging.tagURI(dummyURI, tags);
-    PlacesUtils.tagging.untagURI(bmkURI, null);
-    PlacesUtils.tagging.tagURI(bmkURI, tags);
+    PlacesUtils.tagging.untagURI(bookmarkURI, null);
+    PlacesUtils.tagging.tagURI(bookmarkURI, tags);
     PlacesUtils.tagging.untagURI(dummyURI, null);
   },
 
   getAllIDs: function BStore_getAllIDs() {
     let items = {"menu": true,
                  "toolbar": true};
     for each (let guid in kSpecialIds.guids) {
       if (guid != "places" && guid != "tags")
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -195,17 +195,17 @@ ClientEngine.prototype = {
    * number of arguments and description.
    */
   _commands: {
     resetAll:    { args: 0, desc: "Clear temporary local data for all engines" },
     resetEngine: { args: 1, desc: "Clear temporary local data for engine" },
     wipeAll:     { args: 0, desc: "Delete all client data for all engines" },
     wipeEngine:  { args: 1, desc: "Delete all client data for engine" },
     logout:      { args: 0, desc: "Log out client" },
-    displayURI:  { args: 2, desc: "Instruct a client to display a URI" }
+    displayURI:  { args: 3, desc: "Instruct a client to display a URI" },
   },
 
   /**
    * Remove any commands for the local client and mark it for upload.
    */
   clearCommands: function clearCommands() {
     delete this.localCommands;
     this._tracker.addChangedID(this.localID);
@@ -281,17 +281,17 @@ ClientEngine.prototype = {
             // Fallthrough
           case "wipeEngine":
             Weave.Service.wipeClient(engines);
             break;
           case "logout":
             Weave.Service.logout();
             return false;
           case "displayURI":
-            this._handleDisplayURI(args[0], args[1]);
+            this._handleDisplayURI.apply(this, args);
             break;
           default:
             this._log.debug("Received an unknown command: " + command);
             break;
         }
       }
 
       return true;
@@ -345,45 +345,53 @@ ClientEngine.prototype = {
    * If an unknown client ID is specified, sendCommand() will throw an
    * Error object.
    *
    * @param uri
    *        URI (as a string) to send and display on the remote client
    * @param clientId
    *        ID of client to send the command to. If not defined, will be sent
    *        to all remote clients.
+   * @param title
+   *        Title of the page being sent.
    */
-  sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId) {
-    this._log.info("Sending URI to client: " + uri + " -> " + clientId);
-    this.sendCommand("displayURI", [uri, this.syncID], clientId);
+  sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) {
+    this._log.info("Sending URI to client: " + uri + " -> " +
+                   clientId + " (" + title + ")");
+    this.sendCommand("displayURI", [uri, this.localID, title], clientId);
 
     Clients._tracker.score += SCORE_INCREMENT_XLARGE;
   },
 
   /**
    * Handle a single received 'displayURI' command.
    *
    * Interested parties should observe the "weave:engine:clients:display-uri"
    * topic. The callback will receive an object as the subject parameter with
    * the following keys:
    *
-   *   uri       URI (string) that is requested for display
-   *   clientId  ID of client that sent the command
+   *   uri       URI (string) that is requested for display.
+   *   clientId  ID of client that sent the command.
+   *   title     Title of page that loaded URI (likely) corresponds to.
    *
    * The 'data' parameter to the callback will not be defined.
    *
    * @param uri
    *        String URI that was received
    * @param clientId
    *        ID of client that sent URI
+   * @param title
+   *        String title of page that URI corresponds to. Older clients may not
+   *        send this.
    */
-  _handleDisplayURI: function _handleDisplayURI(uri, clientId) {
-    this._log.info("Received a URI for display: " + uri + " from " + clientId);
+  _handleDisplayURI: function _handleDisplayURI(uri, clientId, title) {
+    this._log.info("Received a URI for display: " + uri + " (" + title +
+                   ") from " + clientId);
 
-    let subject = { uri: uri, client: clientId };
+    let subject = {uri: uri, client: clientId, title: title};
     Svc.Obs.notify("weave:engine:clients:display-uri", subject);
   }
 };
 
 function ClientStore(name) {
   Store.call(this, name);
 }
 ClientStore.prototype = {
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -134,45 +134,59 @@ function TabStore(name) {
 }
 TabStore.prototype = {
   __proto__: Store.prototype,
 
   itemExists: function TabStore_itemExists(id) {
     return id == Clients.localID;
   },
 
+  /**
+   * Return the recorded last used time of the provided tab, or
+   * 0 if none is present.
+   * The result will always be an integer value.
+   */
+  tabLastUsed: function tabLastUsed(tab) {
+    // weaveLastUsed will only be set if the tab was ever selected (or
+    // opened after Sync was running).
+    let weaveLastUsed = tab.extData && tab.extData.weaveLastUsed;
+    if (!weaveLastUsed) {
+      return 0;
+    }
+    return parseInt(weaveLastUsed, 10) || 0;
+  },
+
   getAllTabs: function getAllTabs(filter) {
     let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i");
 
     let allTabs = [];
 
     let currentState = JSON.parse(Svc.Session.getBrowserState());
+    let tabLastUsed = this.tabLastUsed;
     currentState.windows.forEach(function(window) {
       window.tabs.forEach(function(tab) {
         // Make sure there are history entries to look at.
         if (!tab.entries.length)
           return;
         // Until we store full or partial history, just grab the current entry.
         // index is 1 based, so make sure we adjust.
         let entry = tab.entries[tab.index - 1];
 
         // Filter out some urls if necessary. SessionStore can return empty
         // tabs in some cases - easiest thing is to just ignore them for now.
         if (!entry.url || filter && filteredUrls.test(entry.url))
           return;
 
-        // weaveLastUsed will only be set if the tab was ever selected (or
-        // opened after Weave was running). So it might not ever be set.
         // I think it's also possible that attributes[.image] might not be set
         // so handle that as well.
         allTabs.push({
           title: entry.title || "",
           urlHistory: [entry.url],
           icon: tab.attributes && tab.attributes.image || "",
-          lastUsed: tab.extData && tab.extData.weaveLastUsed || 0
+          lastUsed: tabLastUsed(tab)
         });
       });
     });
 
     return allTabs;
   },
 
   createRecord: function createRecord(id, collection) {
--- 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.
@@ -83,18 +83,21 @@ WBORecord.prototype = {
 
   upload: function upload(uri) {
     return new Resource(uri).put(this);
   },
 
   // Take a base URI string, with trailing slash, and return the URI of this
   // WBO based on collection and ID.
   uri: function(base) {
-    if (this.collection && this.id)
-      return Utils.makeURL(base + this.collection + "/" + this.id);
+    if (this.collection && this.id) {
+      let url = Utils.makeURI(base + this.collection + "/" + this.id);
+      url.QueryInterface(Ci.nsIURL);
+      return url;
+    }
     return null;
   },
 
   deserialize: function deserialize(json) {
     this.data = json.constructor.toString() == String ? JSON.parse(json) : json;
 
     try {
       // The payload is likely to be JSON, but if not, keep it as a string
@@ -193,63 +196,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 +294,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 +365,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 +386,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 +459,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 +528,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
@@ -202,38 +201,16 @@ let Utils = {
     return Utils.encodeBase64url(Utils.generateRandomBytes(9));
   },
 
   _base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i,
   checkGUID: function checkGUID(guid) {
     return !!guid && this._base64url_regex.test(guid);
   },
 
-  ensureOneOpen: let (windows = {}) function ensureOneOpen(window) {
-    // Close the other window if it exists
-    let url = window.location.href;
-    let other = windows[url];
-    if (other != null)
-      other.close();
-
-    // Save the new window for future closure
-    windows[url] = window;
-
-    // Actively clean up when the window is closed
-    window.addEventListener("unload", function() windows[url] = null, false);
-  },
-
-  // Returns a nsILocalFile representing a file relative to the current
-  // user's profile directory.  The argument should be a string with
-  // unix-style slashes for directory names (these slashes are automatically
-  // converted to platform-specific path separators).
-  getProfileFile: function getProfileFile(path) {
-    return FileUtils.getFile("ProfD", path.split("/"), true);
-  },
-
   /**
    * Add a simple getter/setter to an object that defers access of a property
    * to an inner property.
    *
    * @param obj
    *        Object to add properties to defer in its prototype
    * @param defer
    *        Property of obj to defer to
@@ -287,80 +264,56 @@ let Utils = {
     // Do the same for b's keys but skip those that we already checked
     for (let k in b)
       if (!(k in a) && !eq(a[k], b[k]))
         return false;
 
     return true;
   },
 
-  deepCopy: function Weave_deepCopy(thing, noSort) {
-    if (typeof(thing) != "object" || thing == null)
-      return thing;
-    let ret;
-
-    if (Array.isArray(thing)) {
-      ret = [];
-      for (let i = 0; i < thing.length; i++)
-        ret.push(Utils.deepCopy(thing[i], noSort));
-
-    } else {
-      ret = {};
-      let props = [p for (p in thing)];
-      if (!noSort)
-        props = props.sort();
-      props.forEach(function(k) ret[k] = Utils.deepCopy(thing[k], noSort));
-    }
-
-    return ret;
-  },
-
-  // Works on frames or exceptions, munges file:// URIs to shorten the paths
-  // FIXME: filename munging is sort of hackish, might be confusing if
-  // there are multiple extensions with similar filenames
-  formatFrame: function Utils_formatFrame(frame) {
-    let tmp = "<file:unknown>";
-
-    let file = frame.filename || frame.fileName;
-    if (file)
-      tmp = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
-
-    if (frame.lineNumber)
-      tmp += ":" + frame.lineNumber;
-    if (frame.name)
-      tmp = frame.name + "()@" + tmp;
-
-    return tmp;
-  },
-
   exceptionStr: function Weave_exceptionStr(e) {
     let message = e.message ? e.message : e;
     return message + " " + Utils.stackTrace(e);
   },
-
-  stackTraceFromFrame: function Weave_stackTraceFromFrame(frame) {
-    let output = [];
-    while (frame) {
-      let str = Utils.formatFrame(frame);
-      if (str)
-        output.push(str);
-      frame = frame.caller;
-    }
-    return output.join(" < ");
-  },
-
+  
   stackTrace: function Weave_stackTrace(e) {
     // Wrapped nsIException
-    if (e.location)
-      return "Stack trace: " + Utils.stackTraceFromFrame(e.location);
+    if (e.location){
+      let frame = e.location; 
+      let output = [];
+      while (frame) {
+      	// Works on frames or exceptions, munges file:// URIs to shorten the paths
+        // FIXME: filename munging is sort of hackish, might be confusing if
+        // there are multiple extensions with similar filenames
+        let str = "<file:unknown>";
+
+        let file = frame.filename || frame.fileName;
+        if (file){
+          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
+        }
 
+        if (frame.lineNumber){
+          str += ":" + frame.lineNumber;
+        }
+        if (frame.name){
+          str = frame.name + "()@" + str;
+        }
+
+        if (str){
+          output.push(str);
+        }
+        frame = frame.caller;
+      }
+      return "Stack trace: " + output.join(" < ");
+    }
     // Standard JS exception
-    if (e.stack)
+    if (e.stack){
       return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
         replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
+    }
 
     return "No traceback available";
   },
   
   // Generator and discriminator for HMAC exceptions.
   // Split these out in case we want to make them richer in future, and to 
   // avoid inevitable confusion if the message changes.
   throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
@@ -886,38 +839,32 @@ let Utils = {
       return Services.io.newURI(URIString, null, null);
     } catch (e) {
       let log = Log4Moz.repository.getLogger("Sync.Utils");
       log.debug("Could not create URI: " + Utils.exceptionStr(e));
       return null;
     }
   },
 
-  makeURL: function Weave_makeURL(URIString) {
-    let url = Utils.makeURI(URIString);
-    url.QueryInterface(Ci.nsIURL);
-    return url;
-  },
-
   /**
    * Load a json object from disk
    *
    * @param filePath
    *        Json file path load from weave/[filePath].json
    * @param that
    *        Object to use for logging and "this" for callback
    * @param callback
    *        Function to process json object as its first parameter
    */
   jsonLoad: function Utils_jsonLoad(filePath, that, callback) {
     filePath = "weave/" + filePath + ".json";
     if (that._log)
       that._log.trace("Loading json from disk: " + filePath);
 
-    let file = Utils.getProfileFile(filePath);
+    let file = FileUtils.getFile("ProfD", filePath.split("/"), true);
     if (!file.exists()) {
       callback.call(that);
       return;
     }
 
     let channel = NetUtil.newChannel(file);
     channel.contentType = "application/json";
 
@@ -952,17 +899,17 @@ let Utils = {
    * @param callback
    *        Function called when the write has been performed. Optional.
    */
   jsonSave: function Utils_jsonSave(filePath, that, obj, callback) {
     filePath = "weave/" + filePath + ".json";
     if (that._log)
       that._log.trace("Saving json to disk: " + filePath);
 
-    let file = Utils.getProfileFile(filePath);
+    let file = FileUtils.getFile("ProfD", filePath.split("/"), true);
     let json = typeof obj == "function" ? obj.call(that) : obj;
     let out = JSON.stringify(json);
 
     let fos = FileUtils.openSafeFileOutputStream(file);
     let is = this._utf8Converter.convertToInputStream(out);
     NetUtil.asyncCopy(is, fos, function (result) {
       if (typeof callback == "function") {
         callback.call(that);        
@@ -1228,22 +1175,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();
 }
 
 /*
@@ -451,8 +460,31 @@ RotaryEngine.prototype = {
 
     for (let [id, value] in Iterator(this._store.items)) {
       if (item.denomination == value) {
         return id;
       }
     }
   }
 };
+
+deepCopy: function deepCopy(thing, noSort) {
+  if (typeof(thing) != "object" || thing == null){
+    return thing;
+  }
+  let ret;
+
+  if (Array.isArray(thing)) {
+    ret = [];
+    for (let i = 0; i < thing.length; i++){
+      ret.push(deepCopy(thing[i], noSort));
+	}
+  } else {
+    ret = {};
+    let props = [p for (p in thing)];
+    if (!noSort){
+      props = props.sort();
+    }
+    props.forEach(function(k) ret[k] = deepCopy(thing[k], noSort));
+  }
+
+  return ret;
+};
\ No newline at end of file
--- 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.
@@ -424,17 +413,66 @@ add_test(function test_bookmark_guidMap_
   } catch (ex) {
     err = ex;
   }
   do_check_eq(err, "Nooo");
 
   server.stop(run_next_test);
 });
 
+add_test(function test_bookmark_is_taggable() {
+  let engine = new BookmarksEngine();
+  let store = engine._store;
+
+  do_check_true(store.isTaggable("bookmark"));
+  do_check_true(store.isTaggable("microsummary"));
+  do_check_true(store.isTaggable("query"));
+  do_check_false(store.isTaggable("folder"));
+  do_check_false(store.isTaggable("livemark"));
+  do_check_false(store.isTaggable(null));
+  do_check_false(store.isTaggable(undefined));
+  do_check_false(store.isTaggable(""));
+
+  run_next_test();
+});
+
+add_test(function test_bookmark_tag_but_no_uri() {
+  _("Ensure that a bookmark record with tags, but no URI, doesn't throw an exception.");
+
+  let engine = new BookmarksEngine();
+  let store = engine._store;
+
+  // We're simply checking that no exception is thrown, so
+  // no actual checks in this test.
+ 
+  store._tagURI(null, ["foo"]);
+  store._tagURI(null, null);
+  store._tagURI(Utils.makeURI("about:fake"), null);
+
+  let record = {
+    _parent:     PlacesUtils.bookmarks.toolbarFolder,
+    id:          Utils.makeGUID(),
+    description: "",
+    tags:        ["foo"],
+    title:       "Taggy tag",
+    type:        "folder"
+  };
+
+  // Because update() walks the cleartext.
+  record.cleartext = record;
+
+  store.create(record);
+  record.tags = ["bar"];
+  store.update(record);
+
+  run_next_test();
+});
 
 function run_test() {
   initTestLogging("Trace");
-  Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Sync.Engine.Bookmarks").level  = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Sync.Store.Bookmarks").level   = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Sync.Tracker.Bookmarks").level = Log4Moz.Level.Trace;
 
   generateNewKeys();
 
   run_next_test();
 }
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -51,17 +51,17 @@ let record631361 = {
 };
 
 // Clean up after other tests. Only necessary in XULRunner.
 store.wipe();
 
 function makeLivemark(p, mintGUID) {
   let b = new Livemark("bookmarks", p.id);
   // Copy here, because tests mutate the contents.
-  b.cleartext = Utils.deepCopy(p);
+  b.cleartext = deepCopy(p);
   
   if (mintGUID)
     b.id = Utils.makeGUID();
 
   return b;
 }
 
 
--- 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: {},
@@ -475,27 +474,30 @@ add_test(function test_send_uri_to_clien
   rec.name = "remote";
   store.create(rec);
   let remoteRecord = store.createRecord(remoteId, "clients");
 
   tracker.clearChangedIDs();
   let initialScore = tracker.score;
 
   let uri = "http://www.mozilla.org/";
-  Clients.sendURIToClientForDisplay(uri, remoteId);
+  let title = "Title of the Page";
+  Clients.sendURIToClientForDisplay(uri, remoteId, title);
 
   let newRecord = store._remoteClients[remoteId];
 
   do_check_neq(newRecord, undefined);
   do_check_eq(newRecord.commands.length, 1);
 
   let command = newRecord.commands[0];
   do_check_eq(command.command, "displayURI");
-  do_check_eq(command.args.length, 2);
+  do_check_eq(command.args.length, 3);
   do_check_eq(command.args[0], uri);
+  do_check_eq(command.args[1], Clients.localID);
+  do_check_eq(command.args[2], title);
 
   do_check_true(tracker.score > initialScore);
   do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE);
 
   _("Ensure unknown client IDs result in exception.");
   let unknownId = Utils.makeGUID();
   let error;
 
@@ -513,33 +515,35 @@ add_test(function test_send_uri_to_clien
 add_test(function test_receive_display_uri() {
   _("Ensure processing of received 'displayURI' commands works.");
 
   // We don't set up WBOs and perform syncing because other tests verify
   // the command API works as advertised. This saves us a little work.
 
   let uri = "http://www.mozilla.org/";
   let remoteId = Utils.makeGUID();
+  let title = "Page Title!";
 
   let command = {
     command: "displayURI",
-    args: [uri, remoteId],
+    args: [uri, remoteId, title],
   };
 
   Clients.localCommands = [command];
 
   // Received 'displayURI' command should result in the topic defined below
   // being called.
   let ev = "weave:engine:clients:display-uri";
 
   let handler = function(subject, data) {
     Svc.Obs.remove(ev, handler);
 
     do_check_eq(subject.uri, uri);
     do_check_eq(subject.client, remoteId);
+    do_check_eq(subject.title, title);
     do_check_eq(data, null);
 
     run_next_test();
   };
 
   Svc.Obs.add(ev, handler);
 
   do_check_true(Clients.processIncomingCommands());
--- 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)");
@@ -490,14 +483,16 @@ function run_test() {
   _("Testing URI construction.");
   let args = [];
   args.push("newer=" + 1234);
   args.push("limit=" + 1234);
   args.push("sort=" + 1234);
 
   let query = "?" + args.join("&");
 
-  let uri1 = Utils.makeURL("http://foo/" + query);
-  let uri2 = Utils.makeURL("http://foo/");
+  let uri1 = Utils.makeURI("http://foo/" + query)
+                  .QueryInterface(Ci.nsIURL);
+  let uri2 = Utils.makeURI("http://foo/")
+                  .QueryInterface(Ci.nsIURL);
   uri2.query = query;
   do_check_eq(uri1.query, uri2.query);
   server.stop(do_test_finished);
 }
--- 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);
@@ -656,18 +648,20 @@ add_test(function test_uri_construction(
   _("Testing URI construction.");
   let args = [];
   args.push("newer=" + 1234);
   args.push("limit=" + 1234);
   args.push("sort=" + 1234);
 
   let query = "?" + args.join("&");
 
-  let uri1 = Utils.makeURL("http://foo/" + query);
-  let uri2 = Utils.makeURL("http://foo/");
+  let uri1 = Utils.makeURI("http://foo/" + query)
+                  .QueryInterface(Ci.nsIURL);
+  let uri2 = Utils.makeURI("http://foo/")
+                  .QueryInterface(Ci.nsIURL);
   uri2.query = query;
   do_check_eq(uri1.query, uri2.query);
 
   run_next_test();
 });
 
 add_test(function test_new_channel() {
   _("Ensure a redirect to a new channel is handled properly.");
--- 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_tab_store.js
+++ b/services/sync/tests/unit/test_tab_store.js
@@ -1,11 +1,30 @@
 Cu.import("resource://services-sync/engines/tabs.js");
 Cu.import("resource://services-sync/util.js");
 
+function test_lastUsed() {
+  let store = new TabEngine()._store;
+
+  _("Check extraction of last used times from tab objects.");
+  let expected = [
+    [0,         {}],
+    [0,         {extData: null}],
+    [0,         {extData: {}}],
+    [0,         {extData: {weaveLastUsed: null}}],
+    [123456789, {extData: {weaveLastUsed: "123456789"}}],
+    [123456789, {extData: {weaveLastUsed: 123456789}}],
+    [123456789, {extData: {weaveLastUsed: 123456789.12}}]
+  ];
+
+  for each (let [ex, input] in expected) {
+    do_check_eq(ex, store.tabLastUsed(input));
+  }
+}
+
 function test_create() {
   let store = new TabEngine()._store;
 
   _("Create a first record");
   let rec = {id: "id1",
              clientName: "clientName1",
              cleartext: "cleartext1",
              modified: 1000};
@@ -56,17 +75,17 @@ function fakeSessionSvc(url, numtabs) {
               weaveLastUsed: 1
             }
           }]
         }]
       };
       if (numtabs) {
         let tabs = obj.windows[0].tabs;
         for (let i = 0; i < numtabs-1; i++)
-          tabs.push(Utils.deepCopy(tabs[0]));
+          tabs.push(deepCopy(tabs[0]));
       }
       return JSON.stringify(obj);
     }
   };
 };
 
 function test_getAllTabs() {
   let store = new TabEngine()._store, tabs;
@@ -107,12 +126,13 @@ function test_createRecord() {
   _("create a big record");
   fakeSessionSvc("http://foo.com", numtabs);
   record = store.createRecord("fake-guid");
   do_check_true(record instanceof TabSetRecord);
   do_check_eq(record.tabs.length, 256);
 }
 
 function run_test() {
+  test_lastUsed();
   test_create();
   test_getAllTabs();
   test_createRecord();
 }
--- 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/test_utils_deepCopy.js
+++ b/services/sync/tests/unit/test_utils_deepCopy.js
@@ -1,13 +1,13 @@
 Cu.import("resource://services-sync/util.js");
 
 function run_test() {
   let thing = {o: {foo: "foo", bar: ["bar"]}, a: ["foo", {bar: "bar"}]};
-  let ret = Utils.deepCopy(thing);
+  let ret = deepCopy(thing);
   do_check_neq(ret, thing)
   do_check_neq(ret.o, thing.o);
   do_check_neq(ret.o.bar, thing.o.bar);
   do_check_neq(ret.a, thing.a);
   do_check_neq(ret.a[1], thing.a[1]);
   do_check_eq(ret.o.foo, thing.o.foo);
   do_check_eq(ret.o.bar[0], thing.o.bar[0]);
   do_check_eq(ret.a[0], thing.a[0]);
deleted file mode 100644
--- a/services/sync/tests/unit/test_utils_ensureOneOpen.js
+++ /dev/null
@@ -1,35 +0,0 @@
-Cu.import("resource://services-sync/util.js");
-
-function WinMock(href) {
-  this.location = {href: href};
-  this._open = true;
-};
-WinMock.prototype = {
-  addEventListener: function(type, listener) {
-    this._listener = listener;
-  },
-  close: function() {
-    if (this._listener) {
-        this._listener();
-    }
-    this._open = false;
-  }
-};
-
-function run_test() {
-    let w1 = new WinMock("chrome://win/win.xul");
-    Utils.ensureOneOpen(w1);
-    do_check_true(w1._open);
-
-    let w2 = new WinMock("chrome://win/win.xul");
-    Utils.ensureOneOpen(w2);
-    do_check_false(w1._open);
-
-    // close w2 and test that ensureOneOpen doesn't
-    // close it again
-    w2.close();
-    w2._open = true;
-    let w3 = new WinMock("chrome://win/win.xul");
-    Utils.ensureOneOpen(w3);
-    do_check_true(w2._open);
-}
--- a/services/sync/tests/unit/test_utils_json.js
+++ b/services/sync/tests/unit/test_utils_json.js
@@ -64,17 +64,18 @@ add_test(function test_save_logging() {
     run_next_test();
   }));
 });
 
 add_test(function test_load_logging() {
   _("Verify that reads and read errors are logged.");
 
   // Write a file with some invalid JSON
-  let file = Utils.getProfileFile("weave/log.json");
+  let filePath = "weave/log.json";
+  let file = FileUtils.getFile("ProfD", filePath.split("/"), true);
   let fos = Cc["@mozilla.org/network/file-output-stream;1"]
               .createInstance(Ci.nsIFileOutputStream);
   let flags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
               | FileUtils.MODE_TRUNCATE;
   fos.init(file, flags, FileUtils.PERMS_FILE, fos.DEFER_OPEN);
   let stream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                  .createInstance(Ci.nsIConverterOutputStream);
   stream.init(fos, "UTF-8", 4096, 0x0000);
--- a/services/sync/tests/unit/test_utils_makeURI.js
+++ b/services/sync/tests/unit/test_utils_makeURI.js
@@ -1,17 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Make sure uri strings are converted to nsIURIs");
 Cu.import("resource://services-sync/util.js");
 
-// both the makeURI and makeURL functions are tested
-// in this file
-
 function run_test() {
   _test_makeURI();
-  _test_makeURL();
 }
 
 function _test_makeURI() {
   _("Check http uris");
   let uri1 = "http://mozillalabs.com/";
   do_check_eq(Utils.makeURI(uri1).spec, uri1);
   let uri2 = "http://www.mozillalabs.com/";
   do_check_eq(Utils.makeURI(uri2).spec, uri2);
@@ -60,40 +59,8 @@ function _test_makeURI() {
   let uria6 = "about:weave/#hash";
   do_check_eq(Utils.makeURI(uria6).spec, uria6);
 
   _("Invalid uris are undefined");
   do_check_eq(Utils.makeURI("mozillalabs.com"), undefined);
   do_check_eq(Utils.makeURI("chrome://badstuff"), undefined);
   do_check_eq(Utils.makeURI("this is a test"), undefined);
 }
-
-function _test_makeURL() {
-  _("Check http uri");
-  let uri = "http://mozillalabs.com/";
-  do_check_true(Utils.makeURL(uri) instanceof Ci.nsIURL);
-
-  _("Check https uri");
-  let uris = "https://mozillalabs.com/";
-  do_check_true(Utils.makeURL(uris) instanceof Ci.nsIURL);
-
-  let uric = "chrome://browser/content/browser.xul";
-  do_check_true(Utils.makeURL(uric) instanceof Ci.nsIURL);
-
-  _("Check about uri");
-  let uria = "about:weave";
-  let except1;
-  try {
-    Utils.makeURL(uria);
-  } catch(e) {
-    except1 = e;
-  }
-  do_check_true(!!except1);
-
-  _("Check invalid uri");
-  let except2;
-  try {
-    Utils.makeURL("mozillalabs.com");
-  } catch(e) {
-    except2 = e;
-  }
-  do_check_true(!!except2);
-}
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,78 +1,84 @@
 [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_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 +93,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");
--- a/services/sync/version.txt
+++ b/services/sync/version.txt
@@ -1,1 +1,1 @@
-1.15.0
+1.16.0
--- a/testing/mochitest/redirect.html
+++ b/testing/mochitest/redirect.html
@@ -9,15 +9,18 @@
       // which will get picked up when specifying --chrome or --a11y
       var element = document.createEvent("datacontainerevent");
       element.initEvent("contentEvent", true, false);
       element.setData("data", aURL + location.search);
       element.setData("type", "loadURI");
       document.dispatchEvent(element);
     }
 
-    redirect("chrome://mochikit/content/harness.xul");
+    function onLoad() {
+      redirect("chrome://mochikit/content/harness.xul");
+    }
   </script>
 </head>
-<body>
+
+<body onload="onLoad();">
 redirecting...
 </body>
 </html>
--- a/toolkit/components/satchel/nsFormHistory.js
+++ b/toolkit/components/satchel/nsFormHistory.js
@@ -116,25 +116,25 @@ FormHistory.prototype = {
                                         getService(Ci.nsIPrivateBrowsingService);
             else
                 this._privBrowsingSvc = null;
         }
         return this._privBrowsingSvc;
     },
 
 
-    log : function (message) {
+    log : function log(message) {
         if (!this.debug)
             return;
         dump("FormHistory: " + message + "\n");
         Services.console.logStringMessage("FormHistory: " + message);
     },
 
 
-    init : function() {
+    init : function init() {
         Services.prefs.addObserver("browser.formfill.", this, true);
 
         this.updatePrefs();
 
         this.dbStmts = {};
 
         this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
                               getService(Ci.nsIChromeFrameMessageManager);
@@ -170,30 +170,30 @@ FormHistory.prototype = {
     /* ---- nsIFormHistory2 interfaces ---- */
 
 
     get hasEntries() {
         return (this.countAllEntries() > 0);
     },
 
 
-    addEntry : function (name, value) {
+    addEntry : function addEntry(name, value) {
         if (!this.enabled ||
             this.privBrowsingSvc && this.privBrowsingSvc.privateBrowsingEnabled)
             return;
 
         this.log("addEntry for " + name + "=" + value);
 
         let now = Date.now() * 1000; // microseconds
 
         let [id, guid] = this.getExistingEntryID(name, value);
         let stmt;
 
         if (id != -1) {
-            // Update existing entry
+            // Update existing entry.
             let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE id = :id";
             let params = {
                             lastUsed : now,
                             id       : id
                          };
 
             try {
                 stmt = this.dbCreateStatement(query, params);
@@ -204,17 +204,17 @@ FormHistory.prototype = {
                 throw e;
             } finally {
                 if (stmt) {
                     stmt.reset();
                 }
             }
 
         } else {
-            // Add new entry
+            // Add new entry.
             guid = this.generateGUID();
 
             let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
                             "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
             let params = {
                             fieldname : name,
                             value     : value,
                             timesUsed : 1,
@@ -234,17 +234,17 @@ FormHistory.prototype = {
                 if (stmt) {
                     stmt.reset();
                 }
             }
         }
     },
 
 
-    removeEntry : function (name, value) {
+    removeEntry : function removeEntry(name, value) {
         this.log("removeEntry for " + name + "=" + value);
 
         let [id, guid] = this.getExistingEntryID(name, value);
         this.sendStringNotification("before-removeEntry", name, value, guid);
 
         let stmt;
         let query = "DELETE FROM moz_formhistory WHERE id = :id";
         let params = { id : id };
@@ -268,17 +268,17 @@ FormHistory.prototype = {
             if (stmt) {
                 stmt.reset();
             }
         }
         this.dbConnection.commitTransaction();
     },
 
 
-    removeEntriesForName : function (name) {
+    removeEntriesForName : function removeEntriesForName(name) {
         this.log("removeEntriesForName with name=" + name);
 
         this.sendStringNotification("before-removeEntriesForName", name);
 
         let stmt;
         let query = "DELETE FROM moz_formhistory WHERE fieldname = :fieldname";
         let params = { fieldname : name };
 
@@ -302,17 +302,17 @@ FormHistory.prototype = {
             if (stmt) {
                 stmt.reset();
             }
         }
         this.dbConnection.commitTransaction();
     },
 
 
-    removeAllEntries : function () {
+    removeAllEntries : function removeAllEntries() {
         this.log("removeAllEntries");
 
         this.sendNotification("before-removeAllEntries", null);
 
         let stmt;
         let query = "DELETE FROM moz_formhistory";
 
         try {
@@ -322,28 +322,28 @@ FormHistory.prototype = {
               timeDeleted: Date.now()
             });
 
             stmt = this.dbCreateStatement(query);
             stmt.execute();
             this.sendNotification("removeAllEntries", null);
         } catch (e) {
             this.dbConnection.rollbackTransaction();
-            this.log("removeEntriesForName failed: " + e);
+            this.log("removeAllEntries failed: " + e);
             throw e;
         } finally {
             if (stmt) {
                 stmt.reset();
             }
         }
         this.dbConnection.commitTransaction();
     },
 
 
-    nameExists : function (name) {
+    nameExists : function nameExists(name) {
         this.log("nameExists for name=" + name);
         let stmt;
         let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory WHERE fieldname = :fieldname";
         let params = { fieldname : name };
         try {
             stmt = this.dbCreateStatement(query, params);
             stmt.executeStep();
             return (stmt.row.numEntries > 0);
@@ -352,24 +352,24 @@ FormHistory.prototype = {
             throw e;
         } finally {
             if (stmt) {
                 stmt.reset();
             }
         }
     },
 
-    entryExists : function (name, value) {
+    entryExists : function entryExists(name, value) {
         this.log("entryExists for " + name + "=" + value);
         let [id, guid] = this.getExistingEntryID(name, value);
         this.log("entryExists: id=" + id);
         return (id != -1);
     },
 
-    removeEntriesByTimeframe : function (beginTime, endTime) {
+    removeEntriesByTimeframe : function removeEntriesByTimeframe(beginTime, endTime) {
         this.log("removeEntriesByTimeframe for " + beginTime + " to " + endTime);
 
         this.sendIntNotification("before-removeEntriesByTimeframe", beginTime, endTime);
 
         let stmt;
         let query = "DELETE FROM moz_formhistory WHERE firstUsed >= :beginTime AND firstUsed <= :endTime";
         let params = {
                         beginTime : beginTime,
@@ -394,30 +394,30 @@ FormHistory.prototype = {
         } finally {
             if (stmt) {
                 stmt.reset();
             }
         }
         this.dbConnection.commitTransaction();
     },
 
-    moveToDeletedTable : function (values, params) {
+    moveToDeletedTable : function moveToDeletedTable(values, params) {
 #ifdef ANDROID
-        this.log("move entries to deleted");
+        this.log("Moving entries to deleted table.");
 
         let stmt;
 
         try {
-            // move the entry to the deleted items table
+            // Move the entries to the deleted items table.
             let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted) ";
             if (values) query += values;
             stmt = this.dbCreateStatement(query, params);
             stmt.execute();
         } catch (e) {
-            this.log("move entry failed: " + e);
+            this.log("Moving deleted entries failed: " + e);
             throw e;
         } finally {
             if (stmt) {
                 stmt.reset();
             }
         }
 #endif
     },
@@ -451,17 +451,17 @@ FormHistory.prototype = {
     get DBConnection() {
         return this.dbConnection;
     },
 
 
     /* ---- nsIObserver interface ---- */
 
 
-    observe : function (subject, topic, data) {
+    observe : function observe(subject, topic, data) {
         switch(topic) {
         case "nsPref:changed":
             this.updatePrefs();
             break;
         case "idle-daily":
         case "formhistory-expire-now":
             this.expireOldEntries();
             break;
--- a/toolkit/devtools/debugger/server/dbg-browser-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-browser-actors.js
@@ -62,29 +62,24 @@ function createRootActor(aConnection)
 function BrowserRootActor(aConnection)
 {
   this.conn = aConnection;
   this._tabActors = new WeakMap();
   this._tabActorPool = null;
   this._actorFactories = null;
 
   this.onTabClosed = this.onTabClosed.bind(this);
-  this._onWindowCreated = this.onWindowCreated.bind(this);
   windowMediator.addListener(this);
 }
 
 BrowserRootActor.prototype = {
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function BRA_sayHello() {
-    // Create the tab actor for the selected tab right away so that it gets a
-    // chance to listen to onNewScript notifications.
-    this._preInitTabActor();
-
     return { from: "root",
              applicationType: "browser",
              traits: [] };
   },
 
   /**
    * Disconnects the actor from the browser window.
    */
@@ -118,16 +113,20 @@ BrowserRootActor.prototype = {
     let actorList = [];
 
     // Walk over open browser windows.
     let e = windowMediator.getEnumerator("navigator:browser");
     let selected;
     while (e.hasMoreElements()) {
       let win = e.getNext();
 
+      // Watch the window for tab closes so we can invalidate
+      // actors as needed.
+      this.watchWindow(win);
+
       // List the tabs in this browser.
       let selectedBrowser = win.getBrowser().selectedBrowser;
       let browsers = win.getBrowser().browsers;
       for each (let browser in browsers) {
         if (browser == selectedBrowser) {
           selected = actorList.length;
         }
         let actor = this._tabActors.get(browser);
@@ -179,70 +178,25 @@ BrowserRootActor.prototype = {
    * When a tab is closed, exit its tab actor.  The actor
    * will be dropped at the next listTabs request.
    */
   onTabClosed: function BRA_onTabClosed(aEvent) {
     this.exitTabActor(aEvent.target.linkedBrowser);
   },
 
   /**
-   * Handle location changes, by preinitializing a tab actor.
-   */
-  onWindowCreated: function BRA_onWindowCreated(evt) {
-    if (evt.target === this.browser.contentDocument) {
-      this._preInitTabActor();
-    }
-  },
-
-  /**
    * Exit the tab actor of the specified tab.
    */
   exitTabActor: function BRA_exitTabActor(aWindow) {
-    this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
     let actor = this._tabActors.get(aWindow);
     if (actor) {
       actor.exit();
     }
   },
 
-  /**
-   * Create the tab actor in the selected tab right away so that it gets a
-   * chance to listen to onNewScript notifications.
-   */
-  _preInitTabActor: function BRA__preInitTabActor() {
-    let actorPool = new ActorPool(this.conn);
-
-    // Walk over open browser windows.
-    let e = windowMediator.getEnumerator("navigator:browser");
-    while (e.hasMoreElements()) {
-      let win = e.getNext();
-
-      // Watch the window for tab closes so we can invalidate
-      // actors as needed.
-      this.watchWindow(win);
-
-      this.browser = win.getBrowser().selectedBrowser;
-      let actor = this._tabActors.get(this.browser);
-      if (actor) {
-        actor._detach();
-      }
-      actor = new BrowserTabActor(this.conn, this.browser);
-      actor.parentID = this.actorID;
-      this._tabActors.set(this.browser, actor);
-
-      actorPool.addActor(actor);
-    }
-
-    this._tabActorPool = actorPool;
-    this.conn.addActorPool(this._tabActorPool);
-
-    // Watch for globals being created in this tab.
-    this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
-  },
-
   // nsIWindowMediatorListener
   onWindowTitleChange: function BRA_onWindowTitleChange(aWindow, aTitle) { },
   onOpenWindow: function BRA_onOpenWindow(aWindow) { },
   onCloseWindow: function BRA_onCloseWindow(aWindow) {
     if (aWindow.getBrowser) {
       this.unwatchWindow(aWindow);
     }
   }
@@ -265,17 +219,16 @@ BrowserRootActor.prototype.requestTypes 
  *        The browser instance that contains this tab.
  */
 function BrowserTabActor(aConnection, aBrowser)
 {
   this.conn = aConnection;
   this._browser = aBrowser;
 
   this._onWindowCreated = this.onWindowCreated.bind(this);
-  this._attach();
 }
 
 // XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 BrowserTabActor.prototype = {
   get browser() { return this._browser; },
 
@@ -336,17 +289,17 @@ BrowserTabActor.prototype = {
     dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
     this._tabPool = new ActorPool(this.conn);
     this.conn.addActorPool(this._tabPool);
 
     // ... and a pool for context-lifetime actors.
     this._pushContext();
 
     // Watch for globals being created in this tab.
-    this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, false);
+    this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
 
     this._attached = true;
   },
 
   /**
    * Creates a thread actor and a pool for context-lifetime actors. It then sets
    * up the content window for debugging.
    */
@@ -388,17 +341,17 @@ BrowserTabActor.prototype = {
   /**
    * Does the actual work of detaching from a tab.
    */
   _detach: function BTA_detach() {
     if (!this.attached) {
       return;
     }
 
-    this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, false);
+    this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
 
     this._popContext();
 
     // Shut down actors that belong to this tab's pool.
     this.conn.removeActorPool(this._tabPool);
     this._tabPool = null;
 
     this._attached = false;
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -94,29 +94,22 @@ ThreadActor.prototype = {
     // for aGlobal's compartment.  Ideally this won't be necessary
     // medium- to long-term, and will be managed by the engine
     // instead.
 
     if (!this._dbg) {
       this._dbg = new Debugger();
     }
 
-    // TODO: Remove this horrible hack when bug 723563 is fixed.
-    // Make sure that a chrome window is not added as a debuggee when opening
-    // the debugger in an empty tab or during tests.
-    if (aGlobal.location &&
-        (aGlobal.location.protocol == "about:" ||
-         aGlobal.location.protocol == "chrome:")) {
-      return;
-    }
-
     this.dbg.addDebuggee(aGlobal);
     this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
     this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this);
     this.dbg.onNewScript = this.onNewScript.bind(this);
+    // Keep the debugger disabled until a client attaches.
+    this.dbg.enabled = false;
   },
 
   /**
    * Remove a debuggee global from the JSInspector.
    */
   removeDebugee: function TA_removeDebuggee(aGlobal) {
     try {
       this.dbg.removeDebuggee(aGlobal);
@@ -498,16 +491,23 @@ ThreadActor.prototype = {
     // Location not found in children, this is the innermost containing script.
     return aScript;
   },
 
   /**
    * Handle a protocol request to return the list of loaded scripts.
    */
   onScripts: function TA_onScripts(aRequest) {
+    // Get the script list from the debugger.
+    for (let s of this.d