Bug 1013064, backout e5dfe9801f76, 11f3a97d1d2c, e2374762f521, 91db8acb8d7e, d0050ba0b875 due to sync issues
authorNeil Deakin <neil@mozilla.com>
Mon, 21 Jul 2014 09:09:41 -0400
changeset 216069 f09caa4ea7c5335b679b8a9c60a5ca5b8fb90b76
parent 216068 d11fc9a23f6876cb5f1bdc5ea1bd227dccd39385
child 216070 42935adf0c7031cde1f913e1eeb5c9691141d53a
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1013064
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1013064, backout e5dfe9801f76, 11f3a97d1d2c, e2374762f521, 91db8acb8d7e, d0050ba0b875 due to sync issues
browser/base/content/aboutaccounts/aboutaccounts.js
browser/base/content/sync/customize.js
browser/base/content/sync/customize.xul
browser/base/content/sync/utils.js
browser/components/preferences/in-content/sync.js
browser/components/preferences/in-content/sync.xul
browser/components/preferences/sync.js
browser/components/preferences/sync.xul
browser/themes/linux/preferences/preferences.css
browser/themes/osx/preferences/preferences.css
browser/themes/windows/preferences/preferences.css
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsCommon.js
services/fxaccounts/moz.build
services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
services/fxaccounts/tests/xpcshell/xpcshell.ini
services/sync/Weave.js
services/sync/modules/browserid_identity.js
services/sync/modules/engines/passwords.js
services/sync/modules/identity.js
services/sync/modules/service.js
services/sync/modules/util.js
services/sync/tests/unit/test_browserid_identity.js
services/sync/tests/unit/test_password_mpenabled.js
services/sync/tests/unit/xpcshell.ini
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -7,19 +7,16 @@
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 
 let fxAccountsCommon = {};
 Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
 
-// for master-password utilities
-Cu.import("resource://services-sync/util.js");
-
 const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
 const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
 
 const OBSERVER_TOPICS = [
   fxAccountsCommon.ONVERIFIED_NOTIFICATION,
   fxAccountsCommon.ONLOGOUT_NOTIFICATION,
 ];
 
@@ -102,22 +99,16 @@ let wrapper = {
                   .wrappedJSObject;
 
     // Don't show about:accounts with FxA disabled.
     if (!weave.fxAccountsEnabled) {
       document.body.remove();
       return;
     }
 
-    // If a master-password is enabled, we want to encourage the user to
-    // unlock it.  Things still work if not, but the user will probably need
-    // to re-auth next startup (in which case we will get here again and
-    // re-prompt)
-    Utils.ensureMPUnlocked();
-
     let iframe = document.getElementById("remote");
     this.iframe = iframe;
     iframe.addEventListener("load", this);
 
     try {
       iframe.src = url || fxAccounts.getAccountsSignUpURI();
     } catch (e) {
       error("Couldn't init Firefox Account wrapper: " + e.message);
--- a/browser/base/content/sync/customize.js
+++ b/browser/base/content/sync/customize.js
@@ -1,11 +1,25 @@
 /* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let service = Cc["@mozilla.org/weave/service;1"]
+                .getService(Ci.nsISupports)
+                .wrappedJSObject;
+
+if (!service.allowPasswordsEngine) {
+  let checkbox = document.getElementById("fxa-pweng-chk");
+  checkbox.checked = false;
+  checkbox.disabled = true;
+}
+
 addEventListener("dialogaccept", function () {
   let pane = document.getElementById("sync-customize-pane");
   pane.writePreferences(true);
   window.arguments[0].accepted = true;
 });
--- a/browser/base/content/sync/customize.xul
+++ b/browser/base/content/sync/customize.xul
@@ -40,17 +40,18 @@
 
   <vbox align="start">
       <checkbox label="&engine.tabs.label;"
                 accesskey="&engine.tabs.accesskey;"
                 preference="engine.tabs"/>
       <checkbox label="&engine.bookmarks.label;"
                 accesskey="&engine.bookmarks.accesskey;"
                 preference="engine.bookmarks"/>
-      <checkbox label="&engine.passwords.label;"
+      <checkbox id="fxa-pweng-chk"
+                label="&engine.passwords.label;"
                 accesskey="&engine.passwords.accesskey;"
                 preference="engine.passwords"/>
       <checkbox label="&engine.history.label;"
                 accesskey="&engine.history.accesskey;"
                 preference="engine.history"/>
       <checkbox label="&engine.addons.label;"
                 accesskey="&engine.addons.accesskey;"
                 preference="engine.addons"/>
--- a/browser/base/content/sync/utils.js
+++ b/browser/base/content/sync/utils.js
@@ -82,16 +82,22 @@ let gSyncUtils = {
     this._openLink(Weave.Svc.Prefs.get(root + "termsURL"));
   },
 
   openPrivacyPolicy: function () {
     let root = this.fxAccountsEnabled ? "fxa." : "";
     this._openLink(Weave.Svc.Prefs.get(root + "privacyURL"));
   },
 
+  openMPInfoPage: function (event) {
+    event.stopPropagation();
+    let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+    this._openLink(baseURL + "sync-master-password");
+  },
+
   openFirstSyncProgressPage: function () {
     this._openLink("about:sync-progress");
   },
 
   /**
    * Prepare an invisible iframe with the passphrase backup document.
    * Used by both the print and saving methods.
    *
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -149,16 +149,27 @@ let gSyncPane = {
         document.getElementById("fxaEmailAddress1").textContent = data.email;
         document.getElementById("fxaEmailAddress2").textContent = data.email;
         document.getElementById("fxaEmailAddress3").textContent = data.email;
         document.getElementById("fxaSyncComputerName").value = Weave.Service.clientsEngine.localName;
         let engines = document.getElementById("fxaSyncEngines")
         for (let checkbox of engines.querySelectorAll("checkbox")) {
           checkbox.disabled = enginesListDisabled;
         }
+
+        let checkbox = document.getElementById("fxa-pweng-chk");
+        let help = document.getElementById("fxa-pweng-help");
+        let allowPasswordsEngine = service.allowPasswordsEngine;
+
+        if (!allowPasswordsEngine) {
+          checkbox.checked = false;
+        }
+
+        checkbox.disabled = !allowPasswordsEngine || enginesListDisabled;
+        help.hidden = allowPasswordsEngine || enginesListDisabled;
       });
     // If fxAccountEnabled is false and we are in a "not configured" state,
     // then fxAccounts is probably fully disabled rather than just unconfigured,
     // so handle this case.  This block can be removed once we remove support
     // for fxAccounts being disabled.
     } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
                Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -278,19 +278,30 @@
       <hbox id="fxaSyncEngines">
         <vbox align="start">
           <checkbox label="&engine.tabs.label;"
                     accesskey="&engine.tabs.accesskey;"
                     preference="engine.tabs"/>
           <checkbox label="&engine.bookmarks.label;"
                     accesskey="&engine.bookmarks.accesskey;"
                     preference="engine.bookmarks"/>
-          <checkbox label="&engine.passwords.label;"
-                    accesskey="&engine.passwords.accesskey;"
-                    preference="engine.passwords"/>
+          <hbox>
+            <checkbox id="fxa-pweng-chk"
+                      label="&engine.passwords.label;"
+                      accesskey="&engine.passwords.accesskey;"
+                      preference="engine.passwords"/>
+
+            <vbox id="fxa-pweng-help">
+              <spacer flex="1"/>
+              <hbox id="fxa-pweng-help-link">
+                <image onclick="gSyncUtils.openMPInfoPage(event);" />
+              </hbox>
+              <spacer flex="1"/>
+            </vbox>
+          </hbox>
           <checkbox label="&engine.history.label;"
                     accesskey="&engine.history.accesskey;"
                     preference="engine.history"/>
           <checkbox label="&engine.addons.label;"
                     accesskey="&engine.addons.accesskey;"
                     preference="engine.addons"/>
           <checkbox label="&engine.prefs.label;"
                     accesskey="&engine.prefs.accesskey;"
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -149,16 +149,27 @@ let gSyncPane = {
         document.getElementById("fxaEmailAddress1").textContent = data.email;
         document.getElementById("fxaEmailAddress2").textContent = data.email;
         document.getElementById("fxaEmailAddress3").textContent = data.email;
         document.getElementById("fxaSyncComputerName").value = Weave.Service.clientsEngine.localName;
         let engines = document.getElementById("fxaSyncEngines")
         for (let checkbox of engines.querySelectorAll("checkbox")) {
           checkbox.disabled = enginesListDisabled;
         }
+
+        let checkbox = document.getElementById("fxa-pweng-chk");
+        let help = document.getElementById("fxa-pweng-help");
+        let allowPasswordsEngine = service.allowPasswordsEngine;
+
+        if (!allowPasswordsEngine) {
+          checkbox.checked = false;
+        }
+
+        checkbox.disabled = !allowPasswordsEngine || enginesListDisabled;
+        help.hidden = allowPasswordsEngine || enginesListDisabled;
       });
     // If fxAccountEnabled is false and we are in a "not configured" state,
     // then fxAccounts is probably fully disabled rather than just unconfigured,
     // so handle this case.  This block can be removed once we remove support
     // for fxAccounts being disabled.
     } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
                Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -260,19 +260,30 @@
             <hbox id="fxaSyncEngines">
               <vbox>
                 <checkbox label="&engine.tabs.label;"
                           accesskey="&engine.tabs.accesskey;"
                           preference="engine.tabs"/>
                 <checkbox label="&engine.bookmarks.label;"
                           accesskey="&engine.bookmarks.accesskey;"
                           preference="engine.bookmarks"/>
-                <checkbox label="&engine.passwords.label;"
-                          accesskey="&engine.passwords.accesskey;"
-                          preference="engine.passwords"/>
+                <hbox>
+                  <checkbox id="fxa-pweng-chk"
+                            label="&engine.passwords.label;"
+                            accesskey="&engine.passwords.accesskey;"
+                            preference="engine.passwords"/>
+
+                  <vbox id="fxa-pweng-help">
+                    <spacer flex="1"/>
+                    <hbox id="fxa-pweng-help-link">
+                      <image onclick="gSyncUtils.openMPInfoPage(event);" />
+                    </hbox>
+                    <spacer flex="1"/>
+                  </vbox>
+                </hbox>
                 <checkbox label="&engine.history.label;"
                           accesskey="&engine.history.accesskey;"
                           preference="engine.history"/>
                 <checkbox label="&engine.addons.label;"
                           accesskey="&engine.addons.accesskey;"
                           preference="engine.addons"/>
                 <checkbox label="&engine.prefs.label;"
                           accesskey="&engine.prefs.accesskey;"
--- a/browser/themes/linux/preferences/preferences.css
+++ b/browser/themes/linux/preferences/preferences.css
@@ -166,9 +166,17 @@ label.small {
   margin: 5px;
   line-height: 1.2em;
 }
 
 #noFxaAccount > label:first-child {
   margin-bottom: 0.6em;
 }
 
+#fxa-pweng-help-link > label {
+  margin: 0;
+}
+
+#fxa-pweng-help-link > image {
+  list-style-image: url("chrome://global/skin/icons/question-16.png");
+}
+
 %endif
--- a/browser/themes/osx/preferences/preferences.css
+++ b/browser/themes/osx/preferences/preferences.css
@@ -228,9 +228,25 @@ html|a.inline-link:-moz-focusring {
   margin: 12px 4px;
   line-height: 1.2em;
 }
 
 #noFxaAccount > label:first-child {
   margin-bottom: 0.6em;
 }
 
+#fxa-pweng-help-link > label {
+  margin: 0;
+}
+
+#fxa-pweng-help-link > image {
+  width: 16px;
+  height: 16px;
+  list-style-image: url("chrome://global/skin/icons/question-16.png");
+}
+
+@media (min-resolution: 2dppx) {
+  #fxa-pweng-help-link > image {
+    list-style-image: url("chrome://global/skin/icons/question-32.png");
+  }
+}
+
 %endif
--- a/browser/themes/windows/preferences/preferences.css
+++ b/browser/themes/windows/preferences/preferences.css
@@ -156,9 +156,17 @@ label.small {
   margin: 6px;
   line-height: 1.2em;
 }
 
 #noFxaAccount > label:first-child {
   margin-bottom: 0.6em;
 }
 
+#fxa-pweng-help-link > label {
+  margin: 0;
+}
+
+#fxa-pweng-help-link > image {
+  list-style-image: url("chrome://global/skin/icons/question-16.png");
+}
+
 %endif
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -298,22 +298,17 @@ function FxAccountsInternal() {
   // able to abort all work on the first sign-in process.  The currentTimer and
   // currentAccountState are used for this purpose.
   // (XXX - should the timer be directly on the currentAccountState?)
   this.currentTimer = null;
   this.currentAccountState = new AccountState(this);
 
   // We don't reference |profileDir| in the top-level module scope
   // as we may be imported before we know where it is.
-  // We only want the fancy new LoginManagerStorage on desktop.
-#if defined(MOZ_B2G)
   this.signedInUserStorage = new JSONStorage({
-#else
-  this.signedInUserStorage = new LoginManagerStorage({
-#endif
     filename: DEFAULT_STORAGE_FILENAME,
     baseDir: OS.Constants.Path.profileDir,
   });
 }
 
 /**
  * The internal API's prototype.
  */
@@ -901,204 +896,16 @@ JSONStorage.prototype = {
       .then(CommonUtils.writeJSON.bind(null, contents, this.path));
   },
 
   get: function() {
     return CommonUtils.readJSON(this.path);
   }
 };
 
-/**
- * LoginManagerStorage constructor that creates instances that may set/get
- * from a combination of a clear-text JSON file and stored securely in
- * the nsILoginManager.
- *
- * @param options {
- *                  filename: of the plain-text file to write to
- *                  baseDir: directory where the file resides
- *                }
- * @return instance
- */
-
-function LoginManagerStorage(options) {
-  // we reuse the JSONStorage for writing the plain-text stuff.
-  this.jsonStorage = new JSONStorage(options);
-}
-
-LoginManagerStorage.prototype = {
-  // The fields in the credentials JSON object that are stored in plain-text
-  // in the profile directory.  All other fields are stored in the login manager,
-  // and thus are only available when the master-password is unlocked.
-
-  // a hook point for testing.
-  get _isLoggedIn() {
-    return Services.logins.isLoggedIn;
-  },
-
-  // Clear any data from the login manager.  Returns true if the login manager
-  // was unlocked (even if no existing logins existed) or false if it was
-  // locked (meaning we don't even know if it existed or not.)
-  _clearLoginMgrData: Task.async(function* () {
-    try { // Services.logins might be third-party and broken...
-      yield Services.logins.initializationPromise;
-      if (!this._isLoggedIn) {
-        return false;
-      }
-      let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
-      for (let login of logins) {
-        Services.logins.removeLogin(login);
-      }
-      return true;
-    } catch (ex) {
-      log.error("Failed to clear login data: ${}", ex);
-      return false;
-    }
-  }),
-
-  set: Task.async(function* (contents) {
-    if (!contents) {
-      // User is signing out - write the null to the json file.
-      yield this.jsonStorage.set(contents);
-
-      // And nuke it from the login manager.
-      let cleared = yield this._clearLoginMgrData();
-      if (!cleared) {
-        // just log a message - we verify that the email address matches when
-        // we reload it, so having a stale entry doesn't really hurt.
-        log.info("not removing credentials from login manager - not logged in");
-      }
-      return;
-    }
-
-    // We are saving actual data.
-    // Split the data into 2 chunks - one to go to the plain-text, and the
-    // other to write to the login manager.
-    let toWriteJSON = {version: contents.version};
-    let accountDataJSON = toWriteJSON.accountData = {};
-    let toWriteLoginMgr = {version: contents.version};
-    let accountDataLoginMgr = toWriteLoginMgr.accountData = {};
-    for (let [name, value] of Iterator(contents.accountData)) {
-      if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
-        accountDataJSON[name] = value;
-      } else {
-        accountDataLoginMgr[name] = value;
-      }
-    }
-    yield this.jsonStorage.set(toWriteJSON);
-
-    try { // Services.logins might be third-party and broken...
-      // and the stuff into the login manager.
-      yield Services.logins.initializationPromise;
-      // If MP is locked we silently fail - the user may need to re-auth
-      // next startup.
-      if (!this._isLoggedIn) {
-        log.info("not saving credentials to login manager - not logged in");
-        return;
-      }
-      // write the rest of the data to the login manager.
-      let loginInfo = new Components.Constructor(
-         "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
-      let login = new loginInfo(FXA_PWDMGR_HOST,
-                                null, // aFormSubmitURL,
-                                FXA_PWDMGR_REALM, // aHttpRealm,
-                                contents.accountData.email, // aUsername
-                                JSON.stringify(toWriteLoginMgr), // aPassword
-                                "", // aUsernameField
-                                "");// aPasswordField
-
-      let existingLogins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null,
-                                                      FXA_PWDMGR_REALM);
-      if (existingLogins.length) {
-        Services.logins.modifyLogin(existingLogins[0], login);
-      } else {
-        Services.logins.addLogin(login);
-      }
-    } catch (ex) {
-      log.error("Failed to save data to the login manager: ${}", ex);
-    }
-  }),
-
-  get: Task.async(function* () {
-    // we need to suck some data from the .json file in the profile dir and
-    // some other from the login manager.
-    let data = yield this.jsonStorage.get();
-    if (!data) {
-      // no user logged in, nuke the storage data incase we couldn't remove
-      // it previously and then we are done.
-      yield this._clearLoginMgrData();
-      return null;
-    }
-
-    // if we have encryption keys it must have been saved before we
-    // used the login manager, so re-save it.
-    if (data.accountData.kA || data.accountData.kB || data.keyFetchToken) {
-      // We need to migrate, but the MP might be locked (eg, on the first run
-      // with this enabled, we will get here very soon after startup, so will
-      // certainly be locked.)  This means we can't actually store the data in
-      // the login manager (and thus might lose it if we migrated now)
-      // So if the MP is locked, we *don't* migrate, but still just return
-      // the subset of data we now store in the JSON.
-      // This will cause sync to notice the lack of keys, force an unlock then
-      // re-fetch the account data to see if the keys are there.  At *that*
-      // point we will end up back here, but because the MP is now unlocked
-      // we can actually perform the migration.
-      if (!this._isLoggedIn) {
-        // return the "safe" subset but leave the storage alone.
-        log.info("account data needs migration to the login manager but the MP is locked.");
-        let result = {
-          version: data.version,
-          accountData: {},
-        };
-        for (let fieldName of FXA_PWDMGR_PLAINTEXT_FIELDS) {
-          result.accountData[fieldName] = data.accountData[fieldName];
-        }
-        return result;
-      }
-      // actually migrate - just calling .set() will split everything up.
-      log.info("account data is being migrated to the login manager.");
-      yield this.set(data);
-    }
-
-    try { // Services.logins might be third-party and broken...
-      // read the data from the login manager and merge it for return.
-      yield Services.logins.initializationPromise;
-
-      if (!this._isLoggedIn) {
-        log.info("returning partial account data as the login manager is locked.");
-        return data;
-      }
-
-      let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM);
-      if (logins.length == 0) {
-        // This could happen if the MP was locked when we wrote the data.
-        log.info("Can't find the rest of the credentials in the login manager");
-        return data;
-      }
-      let login = logins[0];
-      if (login.username == data.accountData.email) {
-        let lmData = JSON.parse(login.password);
-        if (lmData.version == data.version) {
-          // Merge the login manager data
-          copyObjectProperties(lmData.accountData, data.accountData);
-        } else {
-          log.info("version field in the login manager doesn't match - ignoring it");
-          yield this._clearLoginMgrData();
-        }
-      } else {
-        log.info("username in the login manager doesn't match - ignoring it");
-        yield this._clearLoginMgrData();
-      }
-    } catch (ex) {
-      log.error("Failed to get data from the login manager: ${}", ex);
-    }
-    return data;
-  }),
-
-}
-
 // A getter for the instance to export
 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
   let a = new FxAccounts();
 
   // XXX Bug 947061 - We need a strategy for resuming email verification after
   // browser restart
   a.loadAndPoll();
 
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -173,23 +173,10 @@ SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH
 SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NO_LONGER_SUPPORTED]   = ERROR_ENDPOINT_NO_LONGER_SUPPORTED;
 SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_LOGIN_METHOD]         = ERROR_INCORRECT_LOGIN_METHOD;
 SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_INCORRECT_KEY_RETRIEVAL_METHOD;
 SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_API_VERSION]          = ERROR_INCORRECT_API_VERSION;
 SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_EMAIL_CASE]           = ERROR_INCORRECT_EMAIL_CASE;
 SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMP_UNAVAILABLE]       = ERROR_SERVICE_TEMP_UNAVAILABLE;
 SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR]                  = ERROR_UNKNOWN;
 
-// FxAccounts has the ability to "split" the credentials between a plain-text
-// JSON file in the profile dir and in the login manager.
-// These constants relate to that.
-
-// The fields we save in the plaintext JSON.
-// See bug 1013064 comments 23-25 for why the sessionToken is "safe"
-this.FXA_PWDMGR_PLAINTEXT_FIELDS = ["email", "verified", "authAt",
-                                    "sessionToken", "uid"];
-// The pseudo-host we use in the login manager
-this.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
-// The realm we use in the login manager.
-this.FXA_PWDMGR_REALM = "Firefox Accounts credentials";
-
 // Allow this file to be imported via Components.utils.import().
 this.EXPORTED_SYMBOLS = Object.keys(this);
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -5,19 +5,16 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 PARALLEL_DIRS += ['interfaces']
 
 TEST_DIRS += ['tests']
 
 EXTRA_JS_MODULES += [
   'Credentials.jsm',
+  'FxAccounts.jsm',
   'FxAccountsClient.jsm',
   'FxAccountsCommon.js'
 ]
 
-EXTRA_PP_JS_MODULES += [
-  'FxAccounts.jsm',
-]
-
 # For now, we will only be using the FxA manager in B2G.
 if CONFIG['MOZ_B2G']:
   EXTRA_JS_MODULES += ['FxAccountsManager.jsm']
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -98,129 +98,16 @@ add_task(function test_MPLocked() {
 
   Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
   Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
 
   Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist");
   yield fxa.signOut(/* localOnly = */ true)
 });
 
-add_task(function test_migrationMPUnlocked() {
-  // first manually save a signedInUser.json to simulate a first-run with
-  // pre-migrated data.
-  let fxa = new FxAccounts({});
-
-  let creds = {
-    email: "test@example.com",
-    sessionToken: "sessionToken",
-    kA: "the kA value",
-    kB: "the kB value",
-    verified: true
-  };
-  let toWrite = {
-    version: fxa.version,
-    accountData: creds,
-  }
-
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
-  yield CommonUtils.writeJSON(toWrite, path);
-
-  // now load it - it should migrate.
-  let data = yield fxa.getSignedInUser();
-  Assert.deepEqual(data, creds, "we got all the data back");
-
-  // and verify it was actually migrated - re-read signedInUser back.
-  let data = yield CommonUtils.readJSON(path);
-
-  Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
-  Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
-  Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
-
-  Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
-  Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
-
-  let login = getLoginMgrData();
-  Assert.strictEqual(login.username, creds.email, "email matches");
-  let loginData = JSON.parse(login.password);
-  Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
-  Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
-  Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
-
-  Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
-  Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
-  Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
-
-  yield fxa.signOut(/* localOnly = */ true);
-  Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
-});
-
-add_task(function test_migrationMPLocked() {
-  // first manually save a signedInUser.json to simulate a first-run with
-  // pre-migrated data.
-  let fxa = new FxAccounts({});
-
-  let creds = {
-    email: "test@example.com",
-    sessionToken: "sessionToken",
-    kA: "the kA value",
-    kB: "the kB value",
-    verified: true
-  };
-  let toWrite = {
-    version: fxa.version,
-    accountData: creds,
-  }
-
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
-  yield CommonUtils.writeJSON(toWrite, path);
-
-  // pretend the MP is locked.
-  fxa.internal.signedInUserStorage.__defineGetter__("_isLoggedIn", function() false);
-
-  // now load it - it should *not* migrate, but should only give the JSON-safe
-  // data back.
-  let data = yield fxa.getSignedInUser();
-  Assert.ok(!data.kA);
-  Assert.ok(!data.kB);
-
-  // and verify the data on disk wan't migrated.
-  data = yield CommonUtils.readJSON(path);
-  Assert.deepEqual(data, toWrite);
-
-  // Now "unlock" and re-ask for the signedInUser - it should migrate.
-  fxa.internal.signedInUserStorage.__defineGetter__("_isLoggedIn", function() true);
-  data = yield fxa.getSignedInUser();
-  // this time we should have got all the data, not just the JSON-safe fields.
-  Assert.strictEqual(data.kA, creds.kA);
-  Assert.strictEqual(data.kB, creds.kB);
-
-  // And verify the data in the JSON was migrated
-  data = yield CommonUtils.readJSON(path);
-  Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
-  Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
-  Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
-
-  Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
-  Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
-
-  let login = getLoginMgrData();
-  Assert.strictEqual(login.username, creds.email, "email matches");
-  let loginData = JSON.parse(login.password);
-  Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
-  Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
-  Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
-
-  Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
-  Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
-  Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
-
-  yield fxa.signOut(/* localOnly = */ true);
-  Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
-});
-
 add_task(function test_consistentWithMPEdgeCases() {
   let fxa = new FxAccounts({});
 
   let creds1 = {
     email: "test@example.com",
     sessionToken: "sessionToken",
     kA: "the kA value",
     kB: "the kB value",
--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -1,12 +1,10 @@
 [DEFAULT]
 head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
 tail =
 
 [test_accounts.js]
 [test_client.js]
 [test_credentials.js]
-[test_loginmgr_storage.js]
-skip-if = appname == 'b2g' # login manager storage only used on desktop.
 [test_manager.js]
 run-if = appname == 'b2g'
 reason = FxAccountsManager is only available for B2G for now
--- a/services/sync/Weave.js
+++ b/services/sync/Weave.js
@@ -104,16 +104,26 @@ WeaveService.prototype = {
       let username = Services.prefs.getCharPref(SYNC_PREFS_BRANCH + "username");
       return !username || username.contains('@');
     } catch (_) {
       return true; // No username == only allow FxA to be configured.
     }
   },
 
   /**
+   * Returns whether the password engine is allowed. We explicitly disallow
+   * the password engine when a master password is used to ensure those can't
+   * be accessed without the master key.
+   */
+  get allowPasswordsEngine() {
+    // This doesn't apply to old-style sync, it's only an issue for FxA.
+    return !this.fxAccountsEnabled || !Utils.mpEnabled();
+  },
+
+  /**
    * Whether Sync appears to be enabled.
    *
    * This returns true if all the Sync preferences for storing account
    * and server configuration are populated.
    *
    * It does *not* perform a robust check to see if the client is working.
    * For that, you'll want to check Weave.Status.checkSetup().
    */
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -394,83 +394,41 @@ this.BrowserIDManager.prototype = {
     this._syncKeyUpdated = true;
     this._shouldHaveSyncKeyBundle = false;
   },
 
   /**
    * The current state of the auth credentials.
    *
    * This essentially validates that enough credentials are available to use
-   * Sync, although it effectively ignores the state of the master-password -
-   * if that's locked and that's the only problem we can see, say everything
-   * is OK - unlockAndVerifyAuthState will be used to perform the unlock
-   * and re-verification if necessary.
+   * Sync.
    */
   get currentAuthState() {
     if (this._authFailureReason) {
       this._log.info("currentAuthState returning " + this._authFailureReason +
                      " due to previous failure");
       return this._authFailureReason;
     }
     // TODO: need to revisit this. Currently this isn't ready to go until
     // both the username and syncKeyBundle are both configured and having no
     // username seems to make things fail fast so that's good.
     if (!this.username) {
       return LOGIN_FAILED_NO_USERNAME;
     }
 
     // No need to check this.syncKey as our getter for that attribute
     // uses this.syncKeyBundle
-    // If bundle creation started, but failed due to any reason other than
-    // the MP being locked...
-    if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle && !Utils.mpLocked()) {
-      // Return a state that says a re-auth is necessary so we can get keys.
-      return LOGIN_FAILED_LOGIN_REJECTED;
+    // If bundle creation started, but failed.
+    if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle) {
+      return LOGIN_FAILED_NO_PASSPHRASE;
     }
 
     return STATUS_OK;
   },
 
-  // Do we currently have keys, or do we have enough that we should be able
-  // to successfully fetch them?
-  _canFetchKeys: function() {
-    let userData = this._signedInUser;
-    // a keyFetchToken means we can almost certainly grab them.
-    // kA and kB means we already have them.
-    return userData && (userData.keyFetchToken || (userData.kA && userData.kB));
-  },
-
-  /**
-   * Verify the current auth state, unlocking the master-password if necessary.
-   *
-   * Returns a promise that resolves with the current auth state after
-   * attempting to unlock.
-   */
-  unlockAndVerifyAuthState: function() {
-    if (this._canFetchKeys()) {
-      return Promise.resolve(STATUS_OK);
-    }
-    // so no keys - ensure MP unlocked.
-    if (!Utils.ensureMPUnlocked()) {
-      // user declined to unlock, so we don't know if they are stored there.
-      return Promise.resolve(MASTER_PASSWORD_LOCKED);
-    }
-    // now we are unlocked we must re-fetch the user data as we may now have
-    // the details that were previously locked away.
-    return this._fxaService.getSignedInUser().then(
-      accountData => {
-        this._updateSignedInUser(accountData);
-        // If we still can't get keys it probably means the user authenticated
-        // without unlocking the MP or cleared the saved logins, so we've now
-        // lost them - the user will need to reauth before continuing.
-        return this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
-      }
-    );
-  },
-
   /**
    * Do we have a non-null, not yet expired token for the user currently
    * signed in?
    */
   hasValidToken: function() {
     if (!this._token) {
       return false;
     }
@@ -486,24 +444,16 @@ this.BrowserIDManager.prototype = {
     if (tokenServerURI.endsWith("/")) { // trailing slashes cause problems...
       tokenServerURI = tokenServerURI.slice(0, -1);
     }
     let log = this._log;
     let client = this._tokenServerClient;
     let fxa = this._fxaService;
     let userData = this._signedInUser;
 
-    // We need kA and kB for things to work.  If we don't have them, just
-    // return null for the token - sync calling unlockAndVerifyAuthState()
-    // before actually syncing will setup the error states if necessary.
-    if (!this._canFetchKeys()) {
-      log.info("_fetchTokenForUser has no keys to use.");
-      return null;
-    }
-
     log.info("Fetching assertion and token from: " + tokenServerURI);
 
     let maybeFetchKeys = () => {
       // This is called at login time and every time we need a new token - in
       // the latter case we already have kA and kB, so optimise that case.
       if (userData.kA && userData.kB) {
         return;
       }
@@ -569,18 +519,17 @@ this.BrowserIDManager.prototype = {
         // TODO: write tests to make sure that different auth error cases are handled here
         // properly: auth error getting assertion, auth error getting token (invalid generation
         // and client-state error)
         if (err instanceof AuthenticationError) {
           this._log.error("Authentication error in _fetchTokenForUser: " + err);
           // set it to the "fatal" LOGIN_FAILED_LOGIN_REJECTED reason.
           this._authFailureReason = LOGIN_FAILED_LOGIN_REJECTED;
         } else {
-          this._log.error("Non-authentication error in _fetchTokenForUser: "
-                          + (err.message || err));
+          this._log.error("Non-authentication error in _fetchTokenForUser: " + err.message);
           // for now assume it is just a transient network related problem.
           this._authFailureReason = LOGIN_FAILED_NETWORK_ERROR;
         }
         // Drop the sync key bundle, but still expect to have one.
         // This will arrange for us to be in the right 'currentAuthState'
         // such that UI will show the right error.
         this._shouldHaveSyncKeyBundle = true;
         Weave.Status.login = this._authFailureReason;
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -31,16 +31,38 @@ this.PasswordEngine = function PasswordE
 }
 PasswordEngine.prototype = {
   __proto__: SyncEngine.prototype,
   _storeObj: PasswordStore,
   _trackerObj: PasswordTracker,
   _recordObj: LoginRec,
   applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
 
+  get isAllowed() {
+    return Cc["@mozilla.org/weave/service;1"]
+             .getService(Ci.nsISupports)
+             .wrappedJSObject
+             .allowPasswordsEngine;
+  },
+
+  get enabled() {
+    // If we are disabled due to !isAllowed(), we must take care to ensure the
+    // engine has actually had the enabled setter called which reflects this state.
+    let prefVal = SyncEngine.prototype.__lookupGetter__("enabled").call(this);
+    let newVal = this.isAllowed && prefVal;
+    if (newVal != prefVal) {
+      this.enabled = newVal;
+    }
+    return newVal;
+  },
+
+  set enabled(val) {
+    SyncEngine.prototype.__lookupSetter__("enabled").call(this, this.isAllowed && val);
+  },
+
   _syncFinish: function _syncFinish() {
     SyncEngine.prototype._syncFinish.call(this);
 
     // Delete the weave credentials from the server once
     if (!Svc.Prefs.get("deletePwdFxA", false)) {
       try {
         let ids = [];
         for (let host of Utils.getSyncCredentialsHosts()) {
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -373,35 +373,16 @@ IdentityManager.prototype = {
     if (!this.syncKeyBundle) {
       return LOGIN_FAILED_INVALID_PASSPHRASE;
     }
 
     return STATUS_OK;
   },
 
   /**
-   * Verify the current auth state, unlocking the master-password if necessary.
-   *
-   * Returns a promise that resolves with the current auth state after
-   * attempting to unlock.
-   */
-  unlockAndVerifyAuthState: function() {
-    // Try to fetch the passphrase - this will prompt for MP unlock as a
-    // side-effect...
-    try {
-      this.syncKey;
-    } catch (ex) {
-      this._log.debug("Fetching passphrase threw " + ex +
-                      "; assuming master password locked.");
-      return Promise.resolve(MASTER_PASSWORD_LOCKED);
-    }
-    return Promise.resolve(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.
    */
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -679,31 +679,27 @@ Sync11Service.prototype = {
     }
 
     if (!this.identity.username) {
       this._log.warn("No username in verifyLogin.");
       this.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!
-    // So we ask the identity to verify the login state after unlocking the
-    // master password (ie, this call is expected to prompt for MP unlock
-    // if necessary) while we still have control.
-    let cb = Async.makeSpinningCallback();
-    this.identity.unlockAndVerifyAuthState().then(
-      result => cb(null, result),
-      cb
-    );
-    let unlockedState = cb.wait();
-    this._log.debug("Fetching unlocked auth state returned " + unlockedState);
-    if (unlockedState != STATUS_OK) {
-      this.status.login = unlockedState;
+    // Try to fetch the passphrase first, while we still have control.
+    try {
+      this.identity.syncKey;
+    } catch (ex) {
+      this._log.debug("Fetching passphrase threw " + ex +
+                      "; assuming master password locked.");
+      this.status.login = MASTER_PASSWORD_LOCKED;
       return false;
     }
 
     try {
       // Make sure we have a cluster to verify against.
       // This is a little weird, if we don't get a node we pretend
       // to succeed, since that probably means we just don't have storage.
       if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -14,23 +14,16 @@ Cu.import("resource://services-common/as
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 
-// FxAccountsCommon.js doesn't use a "namespace", so create one here.
-XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
-  let FxAccountsCommon = {};
-  Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
-  return FxAccountsCommon;
-});
-
 /*
  * Utility functions
  */
 
 this.Utils = {
   // Alias in functions from CommonUtils. These previously were defined here.
   // In the ideal world, references to these would be removed.
   nextTick: CommonUtils.nextTick,
@@ -596,21 +589,18 @@ this.Utils = {
    * reset when we drop sync credentials, etc.
    */
   getSyncCredentialsHosts: function() {
     // This is somewhat expensive and the result static, so we cache the result.
     if (this._syncCredentialsHosts) {
       return this._syncCredentialsHosts;
     }
     let result = new Set();
-    // the legacy sync host
+    // the legacy sync host.
     result.add(PWDMGR_HOST);
-    // the FxA host
-    result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
-    //
     // The FxA hosts - these almost certainly all have the same hostname, but
     // better safe than sorry...
     for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
                           "identity.fxaccounts.remote.signup.uri",
                           "identity.fxaccounts.remote.signin.uri",
                           "identity.fxaccounts.settings.uri"]) {
       let prefVal;
       try {
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -78,33 +78,19 @@ add_task(function test_initialializeWith
     browseridManager.initializeWithCurrentIdentity();
     yield browseridManager.whenReadyToAuthenticate.promise;
     do_check_true(!!browseridManager._token);
     do_check_true(browseridManager.hasValidToken());
     do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
   }
 );
 
-add_task(function test_initialializeWithNoKeys() {
-    _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
-    let identityConfig = makeIdentityConfig();
-    delete identityConfig.fxaccount.user.kA;
-    delete identityConfig.fxaccount.user.kB;
-    // there's no keyFetchToken by default, so the initialize should fail.
-    configureFxAccountIdentity(browseridManager, identityConfig);
-
-    yield browseridManager.initializeWithCurrentIdentity();
-    yield browseridManager.whenReadyToAuthenticate.promise;
-    do_check_eq(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
-    do_check_false(browseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys");
-});
 
 add_test(function test_getResourceAuthenticator() {
     _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
-    configureFxAccountIdentity(browseridManager);
     let authenticator = browseridManager.getResourceAuthenticator();
     do_check_true(!!authenticator);
     let req = {uri: CommonUtils.makeURI(
       "https://example.net/somewhere/over/the/rainbow"),
                method: 'GET'};
     let output = authenticator(req, 'GET');
     do_check_true('headers' in output);
     do_check_true('authorization' in output.headers);
@@ -249,17 +235,16 @@ add_test(function test_RESTResourceAuthe
       (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
 
   run_next_test();
 });
 
 add_task(function test_ensureLoggedIn() {
   configureFxAccountIdentity(browseridManager);
   yield browseridManager.initializeWithCurrentIdentity();
-  yield browseridManager.whenReadyToAuthenticate.promise;
   Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
   yield browseridManager.ensureLoggedIn();
   Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
   Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
             "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
 
   // arrange for no logged in user.
   let fxa = browseridManager._fxaService
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_password_mpenabled.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-sync/stages/enginesync.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/engines/passwords.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+function run_test() {
+  initTestLogging("Trace");
+  run_next_test();
+}
+
+add_test(function test_simple() {
+  ensureLegacyIdentityManager();
+  // Stub fxAccountsEnabled
+  let xpcs = Cc["@mozilla.org/weave/service;1"]
+             .getService(Components.interfaces.nsISupports)
+             .wrappedJSObject;
+  let fxaEnabledGetter = xpcs.__lookupGetter__("fxAccountsEnabled");
+  xpcs.__defineGetter__("fxAccountsEnabled", () => true);
+
+  // Stub mpEnabled.
+  let mpEnabledF = Utils.mpEnabled;
+  let mpEnabled = false;
+  Utils.mpEnabled = function() mpEnabled;
+
+  let manager = Service.engineManager;
+
+  Service.engineManager.register(PasswordEngine);
+  let engine = Service.engineManager.get("passwords");
+  let wipeCount = 0;
+  let engineWipeServerF = engine.wipeServer;
+  engine.wipeServer = function() {
+    ++wipeCount;
+  }
+
+  // A server for the metadata.
+  let server  = new SyncServer();
+  let johndoe = server.registerUser("johndoe", "password");
+  johndoe.createContents({
+    meta: {global: {engines: {passwords: {version: engine.version,
+                                          syncID: engine.syncID}}}},
+    crypto: {},
+    clients: {}
+  });
+  server.start();
+  setBasicCredentials("johndoe", "password", "abcdeabcdeabcdeabcdeabcdea");
+  Service.serverURL = server.baseURI;
+  Service.clusterURL = server.baseURI;
+
+  let engineSync = new EngineSynchronizer(Service);
+  engineSync._log.level = Log.Level.Trace;
+
+  function assertEnabled(expected, message) {
+    Assert.strictEqual(engine.enabled, expected, message);
+    // The preference *must* reflect the actual state.
+    Assert.strictEqual(Svc.Prefs.get("engine." + engine.prefName), expected,
+                       message + " (pref should match enabled state)");
+  }
+
+  try {
+    assertEnabled(true, "password engine should be enabled by default")
+    let engineMeta = Service.recordManager.get(engine.metaURL);
+    // This engine should be in the meta/global
+    Assert.notStrictEqual(engineMeta.payload.engines[engine.name], undefined,
+                          "The engine should appear in the metadata");
+    Assert.ok(!engineMeta.changed, "the metadata for the password engine hasn't changed");
+
+    // (pretend to) enable a master-password
+    mpEnabled = true;
+    // The password engine should be locally disabled...
+    assertEnabled(false, "if mp is locked the engine should be disabled");
+    // ...but not declined.
+    Assert.ok(!manager.isDeclined("passwords"), "password engine is not declined");
+    // Next time a sync would happen, we call _updateEnabledEngines(), which
+    // would remove the engine from the metadata - call that now.
+    engineSync._updateEnabledEngines();
+    // The global meta should no longer list the engine.
+    engineMeta = Service.recordManager.get(engine.metaURL);
+    Assert.strictEqual(engineMeta.payload.engines[engine.name], undefined,
+                       "The engine should have vanished");
+    // And we should have wiped the server data.
+    Assert.strictEqual(wipeCount, 1, "wipeServer should have been called");
+
+    // Now simulate an incoming meta/global indicating the engine should be
+    // enabled.  We should fail to actually enable it - the pref should remain
+    // false and we wipe the server for anything another device might have
+    // stored.
+    let meta = {
+      payload: {
+        engines: {
+          "passwords": {"version":1,"syncID":"yfBi2v7PpFO2"},
+        },
+      },
+    };
+    engineSync._updateEnabledFromMeta(meta, 3, manager);
+    Assert.strictEqual(wipeCount, 2, "wipeServer should have been called");
+    Assert.ok(!manager.isDeclined("passwords"), "password engine is not declined");
+    assertEnabled(false, "engine still not enabled locally");
+
+    // Let's turn the MP off - but *not* re-enable it locally.
+    mpEnabled = false;
+    // Just disabling the MP isn't enough to force it back to enabled.
+    assertEnabled(false, "engine still not enabled locally");
+    // Another incoming metadata record with the engine enabled should cause
+    // it to be enabled locally.
+    meta = {
+      payload: {
+        engines: {
+          "passwords": 1,
+        },
+      },
+    };
+    engineSync._updateEnabledFromMeta(meta, 3, manager);
+    Assert.strictEqual(wipeCount, 2, "wipeServer should *not* have been called again");
+    Assert.ok(!manager.isDeclined("passwords"), "password engine is not declined");
+    // It should be enabled locally.
+    assertEnabled(true, "engine now enabled locally");
+    // Next time a sync starts it should magically re-appear in our meta/global
+    engine._syncStartup();
+    //engineSync._updateEnabledEngines();
+    engineMeta = Service.recordManager.get(engine.metaURL);
+    Assert.equal(engineMeta.payload.engines[engine.name].version, engine.version,
+                 "The engine should re-appear in the metadata");
+  } finally {
+    // restore the damage we did above...
+    engine.wipeServer = engineWipeServerF;
+    engine._store.wipe();
+    // Un-stub mpEnabled and fxAccountsEnabled
+    Utils.mpEnabled = mpEnabledF;
+    xpcs.__defineGetter__("fxAccountsEnabled", fxaEnabledGetter);
+    server.stop(run_next_test);
+  }
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -164,8 +164,10 @@ skip-if = debug
 [test_prefs_store.js]
 [test_prefs_tracker.js]
 [test_tab_engine.js]
 [test_tab_store.js]
 [test_tab_tracker.js]
 
 [test_healthreport.js]
 skip-if = ! healthreport
+
+[test_password_mpenabled.js]