Bug 1575597: Add pref UI for login breach alerts. r=fluent-reviewers,MattN,flod
authorLuke Crouch <lcrouch@mozilla.com>
Fri, 30 Aug 2019 21:53:08 +0000
changeset 554726 bf6c6df8d211178292928a2194c560b22b168ba3
parent 554725 98059acdf5be3f6b91a14e5700403432dd998ab8
child 554727 204d32b36d7e2e2e06126b3b08015e4cca2134d8
push id2165
push userffxbld-merge
push dateMon, 14 Oct 2019 16:30:58 +0000
treeherdermozilla-release@0eae18af659f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfluent-reviewers, MattN, flod
bugs1575597
milestone70.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 1575597: Add pref UI for login breach alerts. r=fluent-reviewers,MattN,flod Differential Revision: https://phabricator.services.mozilla.com/D43831
browser/components/BrowserGlue.jsm
browser/components/aboutlogins/LoginBreaches.jsm
browser/components/preferences/in-content/privacy.inc.xul
browser/components/preferences/in-content/privacy.js
browser/locales/en-US/browser/preferences/preferences.ftl
toolkit/components/passwordmgr/storage-json.js
toolkit/components/passwordmgr/test/unit/test_vulnerable_passwords.js
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -2030,16 +2030,21 @@ BrowserGlue.prototype = {
     Services.tm.idleDispatchToMainThread(() => {
       try {
         Services.logins;
       } catch (ex) {
         Cu.reportError(ex);
       }
     }, 3000);
 
+    // Add breach alerts pref observer reasonably early so the pref flip works
+    Services.tm.idleDispatchToMainThread(() => {
+      this._addBreachAlertsPrefObserver();
+    });
+
     // It's important that SafeBrowsing is initialized reasonably
     // early, so we use a maximum timeout for it.
     Services.tm.idleDispatchToMainThread(() => {
       SafeBrowsing.init();
     }, 5000);
 
     if (AppConstants.MOZ_CRASHREPORTER) {
       UnsubmittedCrashHandler.scheduleCheckForUnsubmittedCrashReports();
@@ -2195,16 +2200,30 @@ BrowserGlue.prototype = {
         "sync",
         async event => {
           await LoginBreaches.update(event.data.current);
         }
       );
     }
   },
 
+  _addBreachAlertsPrefObserver() {
+    const BREACH_ALERTS_PREF = "signon.management.page.breach-alerts.enabled";
+    const clearVulnerablePasswordsIfBreachAlertsDisabled = async function() {
+      if (!Services.prefs.getBoolPref(BREACH_ALERTS_PREF)) {
+        await LoginBreaches.clearAllPotentiallyVulnerablePasswords();
+      }
+    };
+    clearVulnerablePasswordsIfBreachAlertsDisabled();
+    Services.prefs.addObserver(
+      BREACH_ALERTS_PREF,
+      clearVulnerablePasswordsIfBreachAlertsDisabled
+    );
+  },
+
   _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/aboutlogins/LoginBreaches.jsm
+++ b/browser/components/aboutlogins/LoginBreaches.jsm
@@ -139,16 +139,23 @@ this.LoginBreaches = {
     for (const login of logins) {
       if (storageJSON.isPotentiallyVulnerablePassword(login)) {
         vulnerablePasswordsByLoginGUID.set(login.guid, true);
       }
     }
     return vulnerablePasswordsByLoginGUID;
   },
 
+  async clearAllPotentiallyVulnerablePasswords() {
+    await Services.logins.initializationPromise;
+    const storageJSON =
+      Services.logins.wrappedJSObject._storage.wrappedJSObject;
+    storageJSON.clearAllPotentiallyVulnerablePasswords();
+  },
+
   _breachAlertIsDismissed(login, breach, dismissedBreachAlerts) {
     const breachAddedDate = new Date(breach.AddedDate).getTime();
     const breachAlertIsDismissed =
       dismissedBreachAlerts[login.guid] &&
       dismissedBreachAlerts[login.guid].timeBreachAlertDismissed >
         breachAddedDate;
     return breachAlertIsDismissed;
   },
--- a/browser/components/preferences/in-content/privacy.inc.xul
+++ b/browser/components/preferences/in-content/privacy.inc.xul
@@ -447,16 +447,25 @@
         </hbox>
         <hbox class="indent" id="generatePasswordsBox" flex="1">
           <checkbox id="generatePasswords"
                     data-l10n-id="forms-generate-passwords"
                     search-l10n-ids="forms-generate-passwords.label"
                     preference="signon.generation.enabled"
                     flex="1" />
         </hbox>
+        <hbox class="indent" id="breachAlertsBox" flex="1" align="center">
+          <checkbox id="breachAlerts"
+                    class="tail-with-learn-more"
+                    data-l10n-id="forms-breach-alerts"
+                    search-l10n-ids="breach-alerts.label"
+                    preference="signon.management.page.breach-alerts.enabled"/>
+          <label id="breachAlertsLearnMoreLink" class="learnMore" is="text-link"
+              data-l10n-id="forms-breach-alerts-learn-more-link"/>
+        </hbox>
       </vbox>
       <vbox>
         <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
         <hbox>
           <button id="passwordExceptions"
                   is="highlightable-button"
                   class="accessory-button"
                   data-l10n-id="forms-exceptions"
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -121,16 +121,17 @@ Preferences.addAll([
   { id: "media.autoplay.default", type: "int" },
 
   // Popups
   { id: "dom.disable_open_during_load", type: "bool" },
   // Passwords
   { id: "signon.rememberSignons", type: "bool" },
   { id: "signon.generation.enabled", type: "bool" },
   { id: "signon.autofillForms", type: "bool" },
+  { id: "signon.management.page.breach-alerts.enabled", type: "bool" },
 
   // Buttons
   { id: "pref.privacy.disable_button.view_passwords", type: "bool" },
   { id: "pref.privacy.disable_button.view_passwords_exceptions", type: "bool" },
 
   /* Certificates tab
    * security.default_personal_cert
    *   - a string:
@@ -496,16 +497,24 @@ var gPrivacyPane = {
       "command",
       gPrivacyPane.showSecurityDevices
     );
 
     this._pane = document.getElementById("panePrivacy");
 
     this._initPasswordGenerationUI();
     this._initMasterPasswordUI();
+    // set up the breach alerts Learn More link with the correct URL
+    const breachAlertsLearnMoreLink = document.getElementById(
+      "breachAlertsLearnMoreLink"
+    );
+    const breachAlertsLearnMoreUrl =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") +
+      "lockwise-alerts";
+    breachAlertsLearnMoreLink.setAttribute("href", breachAlertsLearnMoreUrl);
 
     this._initSafeBrowsing();
 
     setEventListener(
       "autoplaySettingsButton",
       "command",
       gPrivacyPane.showAutoplayMediaExceptions
     );
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -749,16 +749,20 @@ forms-ask-to-save-logins =
     .label = Ask to save logins and passwords for websites
     .accesskey = r
 forms-exceptions =
     .label = Exceptions…
     .accesskey = x
 forms-generate-passwords =
     .label = Suggest and generate strong passwords
     .accesskey = u
+forms-breach-alerts =
+    .label = Show alerts about passwords for breached websites
+    .accesskey = b
+forms-breach-alerts-learn-more-link = Learn more
 forms-fill-logins-and-passwords =
     .label = Autofill logins and passwords
     .accesskey = i
 forms-saved-logins =
     .label = Saved Logins…
     .accesskey = L
 forms-master-pw-use =
     .label = Use a master password
--- a/toolkit/components/passwordmgr/storage-json.js
+++ b/toolkit/components/passwordmgr/storage-json.js
@@ -633,16 +633,27 @@ this.LoginManagerStorage_json.prototype 
   },
 
   isPotentiallyVulnerablePassword(login) {
     return this._decryptedPotentiallyVulnerablePasswords.includes(
       login.password
     );
   },
 
+  clearAllPotentiallyVulnerablePasswords() {
+    this._store.ensureDataReady();
+    if (!this._store.data.potentiallyVulnerablePasswords.length) {
+      // No need to write to disk
+      return;
+    }
+    this._store.data.potentiallyVulnerablePasswords = [];
+    this._store.saveSoon();
+    this.__decryptedPotentiallyVulnerablePasswords = null;
+  },
+
   get uiBusy() {
     return this._crypto.uiBusy;
   },
 
   get isLoggedIn() {
     return this._crypto.isLoggedIn;
   },
 
--- a/toolkit/components/passwordmgr/test/unit/test_vulnerable_passwords.js
+++ b/toolkit/components/passwordmgr/test/unit/test_vulnerable_passwords.js
@@ -30,9 +30,18 @@ add_task(async function test_vulnerable_
   );
   for (let loginInfo of logins) {
     Assert.ok(
       !storageJSON.isPotentiallyVulnerablePassword(loginInfo),
       "No other logins should be vulnerable when addVulnerablePassword is called" +
         " with a single argument"
     );
   }
+
+  storageJSON.clearAllPotentiallyVulnerablePasswords();
+
+  for (let loginInfo of logins) {
+    Assert.ok(
+      !storageJSON.isPotentiallyVulnerablePassword(loginInfo),
+      "No logins should be vulnerable when clearAllPotentiallyVulnerablePasswords is called."
+    );
+  }
 });