Backed out changeset b2e87e49589e (bug 1569847) for causing failures in browser_breachAlertDismissals.js
authorMihai Alexandru Michis <malexandru@mozilla.com>
Tue, 27 Aug 2019 04:49:39 +0300
changeset 489996 7e382148d47f8f09f5395c014bb621e8f55cc0a1
parent 489995 7fa89d5cdb35de67da5cc94d15fbf0c184b32d8b
child 489997 7ce6426b7653e89258f3bfd74903e9b97390399c
push id93693
push usermalexandru@mozilla.com
push dateTue, 27 Aug 2019 01:50:17 +0000
treeherderautoland@7e382148d47f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1569847
milestone70.0a1
backs outb2e87e49589e59f0cd41fa8984b2381b32ec4956
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
Backed out changeset b2e87e49589e (bug 1569847) for causing failures in browser_breachAlertDismissals.js
browser/components/BrowserGlue.jsm
browser/components/about/AboutProtectionsHandler.jsm
browser/components/aboutlogins/AboutLoginsParent.jsm
browser/components/aboutlogins/LoginBreaches.jsm
browser/components/aboutlogins/content/components/login-item.css
browser/components/aboutlogins/content/components/login-list.css
browser/components/aboutlogins/moz.build
browser/components/aboutlogins/tests/browser/browser_breachAlertDismissals.js
browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js
browser/components/aboutlogins/tests/unit/xpcshell.ini
toolkit/components/passwordmgr/LoginHelper.jsm
toolkit/components/passwordmgr/LoginStore.jsm
toolkit/components/passwordmgr/storage-json.js
toolkit/components/passwordmgr/test/unit/test_vulnerable_passwords.js
toolkit/components/passwordmgr/test/unit/xpcshell.ini
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -502,17 +502,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Corroborate: "resource://gre/modules/Corroborate.jsm",
   Discovery: "resource:///modules/Discovery.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   FirefoxMonitor: "resource:///modules/FirefoxMonitor.jsm",
   FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   HomePage: "resource:///modules/HomePage.jsm",
   HybridContentTelemetry: "resource://gre/modules/HybridContentTelemetry.jsm",
   Integration: "resource://gre/modules/Integration.jsm",
-  LoginBreaches: "resource:///modules/LoginBreaches.jsm",
   LiveBookmarkMigrator: "resource:///modules/LiveBookmarkMigrator.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   Normandy: "resource://normandy/Normandy.jsm",
   ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
   PageThumbs: "resource://gre/modules/PageThumbs.jsm",
   PdfJs: "resource://pdf.js/PdfJs.jsm",
@@ -975,18 +974,16 @@ BrowserGlue.prototype = {
         } else if (data == "migrateMatchBucketsPrefForUI66") {
           this._migrateMatchBucketsPrefForUI66().then(() => {
             Services.obs.notifyObservers(
               null,
               "browser-glue-test",
               "migrateMatchBucketsPrefForUI66-done"
             );
           });
-        } else if (data == "add-breaches-sync-handler") {
-          this._addBreachesSyncHandler();
         }
         break;
       case "initial-migration-will-import-default-bookmarks":
         this._migrationImportsDefaultBookmarks = true;
         break;
       case "initial-migration-did-import-default-bookmarks":
         this._initPlaces(true);
         break;
@@ -2190,44 +2187,27 @@ BrowserGlue.prototype = {
       this._gmpInstallManager = new obj.GMPInstallManager();
       // We don't really care about the results, if someone is interested they
       // can check the log.
       this._gmpInstallManager.simpleCheckAndInstall().catch(() => {});
     });
 
     Services.tm.idleDispatchToMainThread(() => {
       RemoteSettings.init();
-      this._addBreachesSyncHandler();
     });
 
     Services.tm.idleDispatchToMainThread(() => {
       PublicSuffixList.init();
     });
 
     Services.tm.idleDispatchToMainThread(() => {
       RemoteSecuritySettings.init();
     });
   },
 
-  _addBreachesSyncHandler() {
-    if (
-      Services.prefs.getBoolPref(
-        "signon.management.page.breach-alerts.enabled",
-        false
-      )
-    ) {
-      RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).on(
-        "sync",
-        async event => {
-          await LoginBreaches.update(event.data.current);
-        }
-      );
-    }
-  },
-
   _onQuitRequest: function BG__onQuitRequest(aCancelQuit, aQuitType) {
     // If user has already dismissed quit request, then do nothing
     if (aCancelQuit instanceof Ci.nsISupportsPRBool && aCancelQuit.data) {
       return;
     }
 
     // There are several cases where we won't show a dialog here:
     // 1. There is only 1 tab open in 1 window
--- a/browser/components/about/AboutProtectionsHandler.jsm
+++ b/browser/components/about/AboutProtectionsHandler.jsm
@@ -10,22 +10,30 @@ var EXPORTED_SYMBOLS = ["AboutProtection
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 const { RemotePages } = ChromeUtils.import(
   "resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(
+  this,
+  "fxAccounts",
+  "resource://gre/modules/FxAccounts.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "LoginHelper",
+  "resource://gre/modules/LoginHelper.jsm"
+);
+
 XPCOMUtils.defineLazyModuleGetters(this, {
-  fxAccounts: "resource://gre/modules/FxAccounts.jsm",
   FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js",
   FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js",
-  LoginBreaches: "resource:///modules/LoginBreaches.jsm",
-  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "TrackingDBService",
   "@mozilla.org/tracking-db-service;1",
   "nsITrackingDBService"
 );
@@ -200,17 +208,17 @@ var AboutProtectionsHandler = {
     try {
       if ((await fxAccounts.accountStatus()) && token) {
         monitorData = await this.fetchUserBreachStats(token);
 
         // Get the stats for number of potentially breached Lockwise passwords if no master
         // password is set.
         if (!LoginHelper.isMasterPasswordSet()) {
           const logins = await LoginHelper.getAllUserFacingLogins();
-          potentiallyBreachedLogins = await LoginBreaches.getPotentialBreachesByLoginGUID(
+          potentiallyBreachedLogins = await LoginHelper.getBreachesForLogins(
             logins
           );
         }
       } else {
         // If no account exists, then the user is not logged in with an fxAccount.
         monitorData = {
           errorMessage: "No account",
         };
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -4,26 +4,47 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["AboutLoginsParent"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "E10SUtils",
+  "resource://gre/modules/E10SUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "LoginHelper",
+  "resource://gre/modules/LoginHelper.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "MigrationUtils",
+  "resource:///modules/MigrationUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "Services",
+  "resource://gre/modules/Services.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "UIState",
+  "resource://services-sync/UIState.jsm"
+);
 
-XPCOMUtils.defineLazyModuleGetters(this, {
-  E10SUtils: "resource://gre/modules/E10SUtils.jsm",
-  LoginBreaches: "resource:////modules/LoginBreaches.jsm",
-  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
-  MigrationUtils: "resource:///modules/MigrationUtils.jsm",
-  Services: "resource://gre/modules/Services.jsm",
-  UIState: "resource://services-sync/UIState.jsm",
-  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
-});
+ChromeUtils.defineModuleGetter(
+  this,
+  "PlacesUtils",
+  "resource://gre/modules/PlacesUtils.jsm"
+);
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   return LoginHelper.createLogger("AboutLoginsParent");
 });
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
   "BREACH_ALERTS_ENABLED",
   "signon.management.page.breach-alerts.enabled",
@@ -100,19 +121,19 @@ var AboutLoginsParent = {
       case "AboutLogins:DeleteLogin": {
         let login = LoginHelper.vanillaObjectToLogin(message.data.login);
         Services.logins.removeLogin(login);
         break;
       }
       case "AboutLogins:DismissBreachAlert": {
         const login = message.data.login;
 
-        await LoginBreaches.recordDismissal(login.guid);
+        await LoginHelper.recordBreachAlertDismissal(login.guid);
         const logins = await this.getAllLogins();
-        const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+        const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
           logins
         );
         const messageManager = message.target.messageManager;
         messageManager.sendAsyncMessage(
           "AboutLogins:UpdateBreaches",
           breachesByLoginGUID
         );
         break;
@@ -407,17 +428,17 @@ var AboutLoginsParent = {
           };
 
           messageManager.sendAsyncMessage(
             "AboutLogins:LocalizeBadges",
             selectedBadgeLanguages
           );
 
           if (BREACH_ALERTS_ENABLED) {
-            const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+            const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
               logins
             );
             messageManager.sendAsyncMessage(
               "AboutLogins:UpdateBreaches",
               breachesByLoginGUID
             );
           }
 
deleted file mode 100644
--- a/browser/components/aboutlogins/LoginBreaches.jsm
+++ /dev/null
@@ -1,171 +0,0 @@
-/* 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/. */
-
-/**
- * Manages breach alerts for saved logins using data from Firefox Monitor via
- * RemoteSettings.
- */
-
-"use strict";
-
-const EXPORTED_SYMBOLS = ["LoginBreaches"];
-
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
-
-XPCOMUtils.defineLazyModuleGetters(this, {
-  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
-  RemoteSettings: "resource://services-settings/remote-settings.js",
-  RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
-});
-
-this.LoginBreaches = {
-  REMOTE_SETTINGS_COLLECTION: "fxmonitor-breaches",
-
-  async recordDismissal(loginGuid) {
-    await Services.logins.initializationPromise;
-    const storageJSON =
-      Services.logins.wrappedJSObject._storage.wrappedJSObject;
-
-    return storageJSON.recordBreachAlertDismissal(loginGuid);
-  },
-
-  async update(breaches = null) {
-    const logins = await LoginHelper.getAllUserFacingLogins();
-    await this.getPotentialBreachesByLoginGUID(logins, breaches);
-  },
-
-  /**
-   * Return a Map of login GUIDs to a potential breach affecting that login
-   * by considering only breaches affecting passwords.
-   *
-   * This only uses the breach `Domain` and `timePasswordChanged` to determine
-   * if a login may be breached which means it may contain false-positives if
-   * login timestamps are incorrect, the user didn't save their password change
-   * in Firefox, or the breach didn't contain all accounts, etc. As a result,
-   * consumers should avoid making stronger claims than the data supports.
-   *
-   * @param {nsILoginInfo[]} logins Saved logins to check for potential breaches.
-   * @param {object[]} [breaches = null] Only ones involving passwords will be used.
-   * @returns {Map} with a key for each login GUID potentially in a breach.
-   */
-  async getPotentialBreachesByLoginGUID(logins, breaches = null) {
-    const breachesByLoginGUID = new Map();
-    if (!breaches) {
-      try {
-        breaches = await RemoteSettings(this.REMOTE_SETTINGS_COLLECTION).get();
-      } catch (ex) {
-        if (ex instanceof RemoteSettingsClient.UnknownCollectionError) {
-          log.warn(
-            "Could not get Remote Settings collection.",
-            this.REMOTE_SETTINGS_COLLECTION,
-            ex
-          );
-          return breachesByLoginGUID;
-        }
-        throw ex;
-      }
-    }
-    const BREACH_ALERT_URL = Services.prefs.getStringPref(
-      "signon.management.page.breachAlertUrl"
-    );
-    const baseBreachAlertURL = new URL(BREACH_ALERT_URL);
-
-    await Services.logins.initializationPromise;
-    const storageJSON =
-      Services.logins.wrappedJSObject._storage.wrappedJSObject;
-    const dismissedBreachAlertsByLoginGUID = storageJSON.getBreachAlertDismissalsByLoginGUID();
-
-    // Determine potentially breached logins by checking their origin and the last time
-    // they were changed. It's important to note here that we are NOT considering the
-    // username and password of that login.
-    for (const login of logins) {
-      const loginURI = Services.io.newURI(login.origin);
-      let loginHost;
-      try {
-        // nsIURI.host can throw if the URI scheme doesn't have a host.
-        loginHost = loginURI.host;
-      } catch (ex) {
-        continue;
-      }
-      for (const breach of breaches) {
-        if (
-          !breach.Domain ||
-          !Services.eTLD.hasRootDomain(loginHost, breach.Domain) ||
-          !this._breachInvolvedPasswords(breach) ||
-          !this._breachWasAfterPasswordLastChanged(breach, login)
-        ) {
-          continue;
-        }
-
-        if (!storageJSON.isPotentiallyVulnerablePassword(login)) {
-          storageJSON.addPotentiallyVulnerablePassword(login);
-        }
-
-        if (
-          this._breachAlertIsDismissed(
-            login,
-            breach,
-            dismissedBreachAlertsByLoginGUID
-          )
-        ) {
-          continue;
-        }
-
-        let breachAlertURL = new URL(breach.Name, baseBreachAlertURL);
-        breach.breachAlertURL = breachAlertURL.href;
-        breachesByLoginGUID.set(login.guid, breach);
-      }
-    }
-    return breachesByLoginGUID;
-  },
-
-  /**
-   * Return information about logins using passwords that were potentially in a
-   * breach.
-   * @see the caveats in the documentation for `getPotentialBreachesByLoginGUID`.
-   *
-   * @param {nsILoginInfo[]} logins to check the passwords of.
-   * @returns {Map} from login GUID to `true` for logins that have a password
-   *                that may be vulnerable.
-   */
-  getPotentiallyVulnerablePasswordsByLoginGUID(logins) {
-    const vulnerablePasswordsByLoginGUID = new Map();
-    const storageJSON =
-      Services.logins.wrappedJSObject._storage.wrappedJSObject;
-    for (const login of logins) {
-      if (storageJSON.isPotentiallyVulnerablePassword(login)) {
-        vulnerablePasswordsByLoginGUID.set(login.guid, true);
-      }
-    }
-    return vulnerablePasswordsByLoginGUID;
-  },
-
-  _breachAlertIsDismissed(login, breach, dismissedBreachAlerts) {
-    const breachAddedDate = new Date(breach.AddedDate).getTime();
-    const breachAlertIsDismissed =
-      dismissedBreachAlerts[login.guid] &&
-      dismissedBreachAlerts[login.guid].timeBreachAlertDismissed >
-        breachAddedDate;
-    return breachAlertIsDismissed;
-  },
-
-  _breachInvolvedPasswords(breach) {
-    return (
-      breach.hasOwnProperty("DataClasses") &&
-      breach.DataClasses.includes("Passwords")
-    );
-  },
-
-  _breachWasAfterPasswordLastChanged(breach, login) {
-    const breachDate = new Date(breach.BreachDate).getTime();
-    return login.timePasswordChanged < breachDate;
-  },
-};
-
-XPCOMUtils.defineLazyGetter(this, "log", () => {
-  return LoginHelper.createLogger("LoginBreaches");
-});
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -55,17 +55,16 @@ input[type="url"][readOnly] {
 }
 
 .title {
   margin-top: 0;
   margin-bottom: 0;
   flex-grow: 1;
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: nowrap;
 }
 
 .delete-button,
 .edit-button {
   background-repeat: no-repeat;
   background-position: 8px;
   -moz-context-properties: fill;
   fill: currentColor;
--- a/browser/components/aboutlogins/content/components/login-list.css
+++ b/browser/components/aboutlogins/content/components/login-list.css
@@ -115,17 +115,16 @@ ol {
   overflow: hidden;
 }
 
 .title,
 .username {
   display: block;
   text-overflow: ellipsis;
   overflow: hidden;
-  white-space: nowrap;
 }
 
 .favicon-wrapper {
   height: 16px;
   width: 16px;
   background-image: url("chrome://mozapps/skin/places/defaultFavicon.svg");
   background-repeat: no-repeat;
   margin-inline-end: 12px;
--- a/browser/components/aboutlogins/moz.build
+++ b/browser/components/aboutlogins/moz.build
@@ -6,17 +6,16 @@
 
 JAR_MANIFESTS += ['jar.mn']
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Password Manager')
 
 EXTRA_JS_MODULES += [
     'AboutLoginsParent.jsm',
-    'LoginBreaches.jsm',
 ]
 
 FINAL_TARGET_FILES.actors += [
     'AboutLoginsChild.jsm',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
--- a/browser/components/aboutlogins/tests/browser/browser_breachAlertDismissals.js
+++ b/browser/components/aboutlogins/tests/browser/browser_breachAlertDismissals.js
@@ -1,15 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let { LoginBreaches } = ChromeUtils.import(
-  "resource:///modules/LoginBreaches.jsm"
-);
-
 const TEST_BREACHES = [
   {
     AddedDate: "2019-12-20T23:56:26Z",
     BreachDate: "2018-12-16",
     Domain: "breached.com",
     Name: "Breached",
     PwnCount: 1643100,
     DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
@@ -30,17 +26,17 @@ add_task(async function setup() {
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
     Services.logins.removeAllLogins();
   });
 });
 
 add_task(async function test_show_login() {
   let browser = gBrowser.selectedBrowser;
   TEST_LOGIN3.timePasswordChanged = 12345;
-  let testBreaches = await LoginBreaches.getPotentialBreachesByLoginGUID(
+  let testBreaches = await LoginHelper.getBreachesForLogins(
     [TEST_LOGIN3],
     TEST_BREACHES
   );
   browser.messageManager.sendAsyncMessage(
     "AboutLogins:UpdateBreaches",
     testBreaches
   );
   await ContentTask.spawn(browser, TEST_LOGIN3, async () => {
rename from browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js
rename to browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
--- a/browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js
+++ b/browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js
@@ -1,26 +1,23 @@
 /**
- * Test LoginBreaches.getPotentialBreachesByLoginGUID
+ * Test LoginHelper.getBreachesForLogins
  */
 
 "use strict";
 
-const { RemoteSettings } = ChromeUtils.import(
-  "resource://services-settings/remote-settings.js"
-);
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
-  Ci.nsIObserver
+const { AboutLoginsParent } = ChromeUtils.import(
+  "resource:///modules/AboutLoginsParent.jsm"
 );
 
 ChromeUtils.defineModuleGetter(
   this,
-  "LoginBreaches",
-  "resource:///modules/LoginBreaches.jsm"
+  "LoginHelper",
+  "resource://gre/modules/LoginHelper.jsm"
 );
 
 const TEST_BREACHES = [
   {
     AddedDate: "2018-12-20T23:56:26Z",
     BreachDate: "2018-12-16",
     Domain: "breached.com",
     Name: "Breached",
@@ -57,17 +54,17 @@ const TEST_BREACHES = [
   },
 ];
 
 const NOT_BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
   origin: "https://www.example.com",
   formActionOrigin: "https://www.example.com",
   username: "username",
   password: "password",
-  timePasswordChanged: new Date("2018-12-15").getTime(),
+  timePasswordChanged: Date.now(),
 });
 const BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
   origin: "https://www.breached.com",
   formActionOrigin: "https://www.breached.com",
   username: "username",
   password: "password",
   timePasswordChanged: new Date("2018-12-15").getTime(),
 });
@@ -96,198 +93,135 @@ const LOGIN_FOR_BREACHED_SITE_WITHOUT_PA
 const LOGIN_WITH_NON_STANDARD_URI = LoginTestUtils.testData.formLogin({
   origin: "someApp://random/path/to/login",
   formActionOrigin: "someApp://random/path/to/login",
   username: "username",
   password: "password",
   timePasswordChanged: new Date("2018-12-15").getTime(),
 });
 
-add_task(async function test_notBreachedLogin() {
+add_task(async function test_getBreachesForLogins_notBreachedLogin() {
   Services.logins.addLogin(NOT_BREACHED_LOGIN);
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+  const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
     [NOT_BREACHED_LOGIN],
     TEST_BREACHES
   );
   Assert.strictEqual(
     breachesByLoginGUID.size,
     0,
     "Should be 0 breached logins."
   );
 });
 
-add_task(async function test_breachedLogin() {
+add_task(async function test_getBreachesForLogins_breachedLogin() {
   Services.logins.addLogin(BREACHED_LOGIN);
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+  const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
     [NOT_BREACHED_LOGIN, BREACHED_LOGIN],
     TEST_BREACHES
   );
   Assert.strictEqual(
     breachesByLoginGUID.size,
     1,
     "Should be 1 breached login: " + BREACHED_LOGIN.origin
   );
 });
 
-add_task(async function test_notBreachedSubdomain() {
+add_task(async function test_getBreachesForLogins_notBreachedSubdomain() {
   Services.logins.addLogin(NOT_BREACHED_SUBDOMAIN_LOGIN);
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+  const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
     [NOT_BREACHED_LOGIN, NOT_BREACHED_SUBDOMAIN_LOGIN],
     TEST_BREACHES
   );
   Assert.strictEqual(
     breachesByLoginGUID.size,
     0,
     "Should be 0 breached logins."
   );
 });
 
-add_task(async function test_breachedSubdomain() {
+add_task(async function test_getBreachesForLogins_breachedSubdomain() {
   Services.logins.addLogin(BREACHED_SUBDOMAIN_LOGIN);
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+  const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
     [NOT_BREACHED_SUBDOMAIN_LOGIN, BREACHED_SUBDOMAIN_LOGIN],
     TEST_BREACHES
   );
   Assert.strictEqual(
     breachesByLoginGUID.size,
     1,
     "Should be 1 breached login: " + BREACHED_SUBDOMAIN_LOGIN.origin
   );
 });
 
-add_task(async function test_breachedSiteWithoutPasswords() {
-  Services.logins.addLogin(LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS);
+add_task(
+  async function test_getBreachesForLogins_breachedSiteWithoutPasswords() {
+    Services.logins.addLogin(LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS);
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
-    [LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS],
-    TEST_BREACHES
-  );
-  Assert.strictEqual(
-    breachesByLoginGUID.size,
-    0,
-    "Should be 0 breached login: " +
-      LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin
-  );
-});
-
-add_task(async function test_breachAlertHiddenAfterDismissal() {
-  BREACHED_LOGIN.guid = "{d2de5ac1-4de6-e544-a7af-1f75abcba92b}";
-
-  await Services.logins.initializationPromise;
-  const storageJSON = Services.logins.wrappedJSObject._storage.wrappedJSObject;
-
-  storageJSON.recordBreachAlertDismissal(BREACHED_LOGIN.guid);
+    const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+      [LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS],
+      TEST_BREACHES
+    );
+    Assert.strictEqual(
+      breachesByLoginGUID.size,
+      0,
+      "Should be 0 breached login: " +
+        LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin
+    );
+  }
+);
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
-    [BREACHED_LOGIN, NOT_BREACHED_LOGIN],
-    TEST_BREACHES
-  );
-  Assert.strictEqual(
-    breachesByLoginGUID.size,
-    0,
-    "Should be 0 breached logins after dismissal: " + BREACHED_LOGIN.origin
-  );
+add_task(
+  async function test_getBreachesForLogins_breachAlertHiddenAfterDismissal() {
+    BREACHED_LOGIN.guid = "{d2de5ac1-4de6-e544-a7af-1f75abcba92b}";
 
-  info("Clear login storage");
-  Services.logins.removeAllLogins();
+    await Services.logins.initializationPromise;
+    const storageJSON =
+      Services.logins.wrappedJSObject._storage.wrappedJSObject;
+
+    storageJSON.recordBreachAlertDismissal(BREACHED_LOGIN.guid);
 
-  const breachesByLoginGUID2 = await LoginBreaches.getPotentialBreachesByLoginGUID(
-    [BREACHED_LOGIN, NOT_BREACHED_LOGIN],
-    TEST_BREACHES
-  );
-  Assert.strictEqual(
-    breachesByLoginGUID2.size,
-    1,
-    "Breached login should re-appear after clearing storage: " +
-      BREACHED_LOGIN.origin
-  );
-});
+    const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+      [BREACHED_LOGIN, NOT_BREACHED_LOGIN],
+      TEST_BREACHES
+    );
+    Assert.strictEqual(
+      breachesByLoginGUID.size,
+      0,
+      "Should be 0 breached logins after dismissal: " + BREACHED_LOGIN.origin
+    );
+  }
+);
 
-add_task(async function test_newBreachAfterDismissal() {
+add_task(async function test_getBreachesForLogins_newBreachAfterDismissal() {
   TEST_BREACHES[0].AddedDate = new Date().toISOString();
 
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
+  const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
     [BREACHED_LOGIN, NOT_BREACHED_LOGIN],
     TEST_BREACHES
   );
 
   Assert.strictEqual(
     breachesByLoginGUID.size,
     1,
     "Should be 1 breached login after new breach following the dismissal of a previous breach: " +
       BREACHED_LOGIN.origin
   );
 });
 
-add_task(async function test_ExceptionsThrownByNonStandardURIsAreCaught() {
-  Services.logins.addLogin(LOGIN_WITH_NON_STANDARD_URI);
-
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
-    [LOGIN_WITH_NON_STANDARD_URI, BREACHED_LOGIN],
-    TEST_BREACHES
-  );
-
-  Assert.strictEqual(
-    breachesByLoginGUID.size,
-    1,
-    "Exceptions thrown by logins with non-standard URIs should be caught."
-  );
-});
+add_task(
+  async function test_getBreachesForLogins_ExceptionsThrownByNonStandardURIsAreCaught() {
+    Services.logins.addLogin(LOGIN_WITH_NON_STANDARD_URI);
 
-add_task(async function test_updateBreachesFromRemoteSettingsSync() {
-  const login = NOT_BREACHED_SUBDOMAIN_LOGIN;
-  const nowExampleIsInBreachedRecords = [
-    {
-      AddedDate: "2018-12-20T23:56:26Z",
-      BreachDate: "2018-12-16",
-      Domain: "not-breached-subdomain.host.com",
-      Name: "not-breached-subdomain.host.com is now breached!",
-      PwnCount: 1643100,
-      DataClasses: [
-        "Email addresses",
-        "Usernames",
-        "Passwords",
-        "IP addresses",
-      ],
-      _status: "synced",
-      id: "047940fe-d2fd-4314-b636-b4a952ee0044",
-      last_modified: "1541615610052",
-      schema: "1541615609018",
-    },
-  ];
-  async function emitSync() {
-    await RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).emit(
-      "sync",
-      { data: { current: nowExampleIsInBreachedRecords } }
+    const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
+      [LOGIN_WITH_NON_STANDARD_URI, BREACHED_LOGIN],
+      TEST_BREACHES
+    );
+
+    Assert.strictEqual(
+      breachesByLoginGUID.size,
+      1,
+      "Exceptions thrown by logins with non-standard URIs should be caught."
     );
   }
-
-  const beforeSyncBreachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
-    [login]
-  );
-  Assert.strictEqual(
-    beforeSyncBreachesByLoginGUID.size,
-    0,
-    "Should be 0 breached login before not-breached-subdomain.host.com is added to fxmonitor-breaches collection and synced: "
-  );
-  gBrowserGlue.observe(null, "browser-glue-test", "add-breaches-sync-handler");
-  const collection = await RemoteSettings(
-    LoginBreaches.REMOTE_SETTINGS_COLLECTION
-  ).openCollection();
-  await collection.create(nowExampleIsInBreachedRecords[0], {
-    useRecordId: true,
-  });
-  await collection.db.saveLastModified(42);
-  await emitSync();
-
-  const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
-    [login]
-  );
-  Assert.strictEqual(
-    breachesByLoginGUID.size,
-    1,
-    "Should be 1 breached login after not-breached-subdomain.host.com is added to fxmonitor-breaches collection and synced: "
-  );
-});
+);
--- a/browser/components/aboutlogins/tests/unit/xpcshell.ini
+++ b/browser/components/aboutlogins/tests/unit/xpcshell.ini
@@ -1,5 +1,5 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
-[test_getPotentialBreachesByLoginGUID.js]
+[test_getBreachesForLogins.js]
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -8,24 +8,37 @@
  * This JavaScript module exists in order to share code between the different
  * XPCOM components that constitute the Login Manager, including implementations
  * of nsILoginManager and nsILoginManagerStorage.
  */
 
 "use strict";
 
 const EXPORTED_SYMBOLS = ["LoginHelper"];
+const REMOTE_SETTINGS_BREACHES_COLLECTION = "fxmonitor-breaches";
 
 // Globals
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
+ChromeUtils.defineModuleGetter(
+  this,
+  "RemoteSettings",
+  "resource://services-settings/remote-settings.js"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "RemoteSettingsClient",
+  "resource://services-settings/RemoteSettingsClient.jsm"
+);
+
 /**
  * Contains functions shared by different Login Manager components.
  */
 this.LoginHelper = {
   debug: null,
   enabled: null,
   formlessCaptureEnabled: null,
   generationAvailable: null,
@@ -1094,21 +1107,95 @@ this.LoginHelper = {
     } catch (e) {
       if (e.result == Cr.NS_ERROR_ABORT) {
         // If the user cancels the MP prompt then return no logins.
         return [];
       }
       throw e;
     }
   },
+
+  async recordBreachAlertDismissal(loginGuid) {
+    await Services.logins.initializationPromise;
+    const storageJSON =
+      Services.logins.wrappedJSObject._storage.wrappedJSObject;
+
+    return storageJSON.recordBreachAlertDismissal(loginGuid);
+  },
+
+  async getBreachesForLogins(logins, breaches = null) {
+    const breachesByLoginGUID = new Map();
+    if (!breaches) {
+      try {
+        breaches = await RemoteSettings(
+          REMOTE_SETTINGS_BREACHES_COLLECTION
+        ).get();
+      } catch (ex) {
+        if (ex instanceof RemoteSettingsClient.UnknownCollectionError) {
+          log.warn(
+            "Could not get Remote Settings collection.",
+            REMOTE_SETTINGS_BREACHES_COLLECTION,
+            ex
+          );
+          return breachesByLoginGUID;
+        }
+        throw ex;
+      }
+    }
+    const BREACH_ALERT_URL = Services.prefs.getStringPref(
+      "signon.management.page.breachAlertUrl"
+    );
+    const baseBreachAlertURL = new URL(BREACH_ALERT_URL);
+
+    await Services.logins.initializationPromise;
+    const storageJSON =
+      Services.logins.wrappedJSObject._storage.wrappedJSObject;
+    const dismissedBreachAlertsByLoginGUID = storageJSON.getBreachAlertDismissalsByLoginGUID();
+
+    // Determine potentially breached logins by checking their origin and the last time
+    // they were changed. It's important to note here that we are NOT considering the
+    // username and password of that login.
+    for (const login of logins) {
+      const loginURI = Services.io.newURI(login.origin);
+      let loginHost;
+      try {
+        // nsIURI.host can throw if the URI scheme doesn't have a host.
+        loginHost = loginURI.host;
+      } catch (ex) {
+        continue;
+      }
+      for (const breach of breaches) {
+        if (!breach.Domain) {
+          continue;
+        }
+        const breachDate = new Date(breach.BreachDate).getTime();
+        const breachAddedDate = new Date(breach.AddedDate).getTime();
+        if (
+          Services.eTLD.hasRootDomain(loginHost, breach.Domain) &&
+          breach.hasOwnProperty("DataClasses") &&
+          breach.DataClasses.includes("Passwords") &&
+          login.timePasswordChanged < breachDate &&
+          (!dismissedBreachAlertsByLoginGUID[login.guid] ||
+            dismissedBreachAlertsByLoginGUID[login.guid]
+              .timeBreachAlertDismissed < breachAddedDate)
+        ) {
+          let breachAlertURL = new URL(breach.Name, baseBreachAlertURL);
+          breach.breachAlertURL = breachAlertURL.href;
+          breachesByLoginGUID.set(login.guid, breach);
+        }
+      }
+    }
+    return breachesByLoginGUID;
+  },
 };
 
 LoginHelper.init();
 
 XPCOMUtils.defineLazyPreferenceGetter(
   LoginHelper,
   "showInsecureFieldWarning",
   "security.insecure_field_warning.contextual.enabled"
 );
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
-  return LoginHelper.createLogger("LoginHelper");
+  let logger = LoginHelper.createLogger("LoginHelper");
+  return logger;
 });
--- a/toolkit/components/passwordmgr/LoginStore.jsm
+++ b/toolkit/components/passwordmgr/LoginStore.jsm
@@ -93,20 +93,16 @@ LoginStore.prototype._dataPostProcessor 
     data.nextId = 1;
   }
 
   // Create any arrays that are not present in the saved file.
   if (!data.logins) {
     data.logins = [];
   }
 
-  if (!data.potentiallyVulnerablePasswords) {
-    data.potentiallyVulnerablePasswords = [];
-  }
-
   if (!data.dismissedBreachAlertsByLoginGUID) {
     data.dismissedBreachAlertsByLoginGUID = {};
   }
 
   // Indicate that the current version of the code has touched the file.
   data.version = kDataVersion;
 
   return data;
--- a/toolkit/components/passwordmgr/storage-json.js
+++ b/toolkit/components/passwordmgr/storage-json.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
-/**
+/*
  * nsILoginManagerStorage implementation for the JSON back-end.
  */
 
 "use strict";
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
@@ -52,34 +52,16 @@ this.LoginManagerStorage_json.prototype 
     if (!this.__crypto) {
       this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
         Ci.nsILoginManagerCrypto
       );
     }
     return this.__crypto;
   },
 
-  __decryptedPotentiallyVulnerablePasswords: null,
-  get _decryptedPotentiallyVulnerablePasswords() {
-    if (!this.__decryptedPotentiallyVulnerablePasswords) {
-      this._store.ensureDataReady();
-      this.__decryptedPotentiallyVulnerablePasswords = [];
-      for (const potentiallyVulnerablePassword of this._store.data
-        .potentiallyVulnerablePasswords) {
-        const decryptedPotentiallyVulnerablePassword = this._crypto.decrypt(
-          potentiallyVulnerablePassword.encryptedPassword
-        );
-        this.__decryptedPotentiallyVulnerablePasswords.push(
-          decryptedPotentiallyVulnerablePassword
-        );
-      }
-    }
-    return this.__decryptedPotentiallyVulnerablePasswords;
-  },
-
   initialize() {
     try {
       // Force initialization of the crypto module.
       // See bug 717490 comment 17.
       this._crypto;
 
       // Set the reference to LoginStore synchronously.
       let jsonPath = OS.Path.join(OS.Constants.Path.profileDir, "logins.json");
@@ -566,19 +548,16 @@ this.LoginManagerStorage_json.prototype 
   /**
    * Removes all logins from storage.
    */
   removeAllLogins() {
     this._store.ensureDataReady();
 
     this.log("Removing all logins");
     this._store.data.logins = [];
-    this._store.data.potentiallyVulnerablePasswords = [];
-    this.__decryptedPotentiallyVulnerablePasswords = null;
-    this._store.data.dismissedBreachAlertsByLoginGUID = {};
     this._store.saveSoon();
 
     LoginHelper.notifyStorageChanged("removeAllLogins", null);
   },
 
   findLogins(origin, formActionOrigin, httpRealm) {
     let loginData = {
       origin,
@@ -613,36 +592,16 @@ this.LoginManagerStorage_json.prototype 
       }
     }
     let [logins, ids] = this._searchLogins(matchData);
 
     this.log("_countLogins: counted logins:", logins.length);
     return logins.length;
   },
 
-  addPotentiallyVulnerablePassword(login) {
-    this._store.ensureDataReady();
-    // this breached password is already stored
-    if (this.isPotentiallyVulnerablePassword(login)) {
-      return;
-    }
-    this.__decryptedPotentiallyVulnerablePasswords.push(login.password);
-
-    this._store.data.potentiallyVulnerablePasswords.push({
-      encryptedPassword: this._crypto.encrypt(login.password),
-    });
-    this._store.saveSoon();
-  },
-
-  isPotentiallyVulnerablePassword(login) {
-    return this._decryptedPotentiallyVulnerablePasswords.includes(
-      login.password
-    );
-  },
-
   get uiBusy() {
     return this._crypto.uiBusy;
   },
 
   get isLoggedIn() {
     return this._crypto.isLoggedIn;
   },
 
deleted file mode 100644
--- a/toolkit/components/passwordmgr/test/unit/test_vulnerable_passwords.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-add_task(async function setup() {
-  await Services.logins.initializationPromise;
-});
-
-add_task(async function test_vulnerable_password_methods() {
-  const storageJSON = Services.logins.wrappedJSObject._storage.wrappedJSObject;
-
-  let logins = TestData.loginList();
-  Assert.greater(logins.length, 0, "Initial logins length should be > 0.");
-
-  for (let loginInfo of logins) {
-    Services.logins.addLogin(loginInfo);
-    Assert.ok(
-      !storageJSON.isPotentiallyVulnerablePassword(loginInfo),
-      "No logins should be vulnerable until addVulnerablePasswords is called."
-    );
-  }
-
-  const vulnerableLogin = logins.shift();
-  storageJSON.addPotentiallyVulnerablePassword(vulnerableLogin);
-
-  Assert.ok(
-    storageJSON.isPotentiallyVulnerablePassword(vulnerableLogin),
-    "Login should be vulnerable after calling addVulnerablePassword."
-  );
-  for (let loginInfo of logins) {
-    Assert.ok(
-      !storageJSON.isPotentiallyVulnerablePassword(loginInfo),
-      "No other logins should be vulnerable when addVulnerablePassword is called" +
-        " with a single argument"
-    );
-  }
-});
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -52,10 +52,8 @@ skip-if = os != "win"
 [test_PasswordGenerator.js]
 skip-if = os == "android" # Not packaged/used on Android
 [test_recipes_add.js]
 [test_recipes_content.js]
 [test_search_schemeUpgrades.js]
 [test_shadowHTTPLogins.js]
 [test_storage.js]
 [test_telemetry.js]
-[test_vulnerable_passwords.js]
-skip-if = os == "android" # Not implemented for storage-mozStorage