Bug 1559365: UI indication of breached logins r=jaws,MattN
authorLuke Crouch <lcrouch@mozilla.com>
Wed, 10 Jul 2019 19:57:25 +0000
changeset 482272 6484c07ff83649914781ddc9cfb70e98466cdd7a
parent 482271 27679131d486cb10ecb6337688db6e84cc8b48e4
child 482273 7018488ca120836111db492baf8fc852d0b699e3
push id89667
push usermozilla@noorenberghe.ca
push dateWed, 10 Jul 2019 20:29:25 +0000
treeherderautoland@6484c07ff836 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, MattN
bugs1559365
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 1559365: UI indication of breached logins r=jaws,MattN Differential Revision: https://phabricator.services.mozilla.com/D35105
browser/components/BrowserGlue.jsm
browser/components/aboutlogins/AboutLoginsChild.jsm
browser/components/aboutlogins/AboutLoginsParent.jsm
browser/components/aboutlogins/content/aboutLogins.js
browser/components/aboutlogins/content/components/login-list.css
browser/components/aboutlogins/content/components/login-list.js
browser/components/contextualidentity/test/browser/browser_aboutURLs.js
browser/components/originattributes/test/browser/browser_firstPartyIsolation_aboutPages.js
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -98,16 +98,17 @@ let LEGACY_ACTORS = {
         AboutLoginsOpenFeedback: { wantUntrusted: true },
         AboutLoginsOpenPreferences: { wantUntrusted: true },
         AboutLoginsOpenSite: { wantUntrusted: true },
         AboutLoginsRecordTelemetryEvent: { wantUntrusted: true },
         AboutLoginsUpdateLogin: { wantUntrusted: true },
       },
       messages: [
         "AboutLogins:AllLogins",
+        "AboutLogins:UpdateBreaches",
         "AboutLogins:LoginAdded",
         "AboutLogins:LoginModified",
         "AboutLogins:LoginRemoved",
       ],
     },
   },
 
   AboutReader: {
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -106,16 +106,19 @@ class AboutLoginsChild extends ActorChil
     }
   }
 
   receiveMessage(message) {
     switch (message.name) {
       case "AboutLogins:AllLogins":
         this.sendToContent("AllLogins", message.data);
         break;
+      case "AboutLogins:UpdateBreaches":
+        this.sendToContent("UpdateBreaches", message.data);
+        break;
       case "AboutLogins:LoginAdded":
         this.sendToContent("LoginAdded", message.data);
         break;
       case "AboutLogins:LoginModified":
         this.sendToContent("LoginModified", message.data);
         break;
       case "AboutLogins:LoginRemoved":
         this.sendToContent("LoginRemoved", message.data);
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -21,16 +21,21 @@ ChromeUtils.defineModuleGetter(
 );
 ChromeUtils.defineModuleGetter(
   this,
   "MigrationUtils",
   "resource:///modules/MigrationUtils.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
+  "RemoteSettings",
+  "resource://services-settings/remote-settings.js"
+);
+ChromeUtils.defineModuleGetter(
+  this,
   "Services",
   "resource://gre/modules/Services.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   return LoginHelper.createLogger("AboutLoginsParent");
 });
 
@@ -172,19 +177,22 @@ var AboutLoginsParent = {
         ) {
           Services.obs.addObserver(this, "passwordmgr-crypto-login");
           Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled");
           Services.obs.addObserver(this, "passwordmgr-storage-changed");
         }
         this._subscribers.add(message.target);
 
         let messageManager = message.target.messageManager;
+        const logins = await this.getAllLogins();
+        messageManager.sendAsyncMessage("AboutLogins:AllLogins", logins);
+        const breachesByLoginGUID = await this.getBreachesForLogins(logins);
         messageManager.sendAsyncMessage(
-          "AboutLogins:AllLogins",
-          await this.getAllLogins()
+          "AboutLogins:UpdateBreaches",
+          breachesByLoginGUID
         );
         break;
       }
       case "AboutLogins:UpdateLogin": {
         let loginUpdates = message.data.login;
         let logins = LoginHelper.searchLoginsWithObject({
           guid: loginUpdates.guid,
         });
@@ -360,9 +368,29 @@ var AboutLoginsParent = {
     } catch (e) {
       if (e.result == Cr.NS_ERROR_ABORT) {
         // If the user cancels the MP prompt then return no logins.
         return [];
       }
       throw e;
     }
   },
+
+  async getBreachesForLogins(logins) {
+    const breaches = await RemoteSettings("fxmonitor-breaches").get();
+    const breachHostMap = new Map();
+    for (const breach of breaches) {
+      breachHostMap.set(breach.Domain, breach);
+    }
+    const breachesByLoginGUID = new Map();
+    for (const login of logins) {
+      const loginURI = Services.io.newURI(login.origin);
+      const breach = breachHostMap.get(loginURI.host) || false;
+      if (
+        breach &&
+        login.timePasswordChanged < new Date(breach.BreachDate).getTime()
+      ) {
+        breachesByLoginGUID.set(login.guid, breach);
+      }
+    }
+    return breachesByLoginGUID;
+  },
 };
--- a/browser/components/aboutlogins/content/aboutLogins.js
+++ b/browser/components/aboutlogins/content/aboutLogins.js
@@ -26,16 +26,20 @@ document.addEventListener(
 );
 
 window.addEventListener("AboutLoginsChromeToContent", event => {
   switch (event.detail.messageType) {
     case "AllLogins": {
       gElements.loginList.setLogins(event.detail.value);
       break;
     }
+    case "UpdateBreaches": {
+      gElements.loginList.updateBreaches(event.detail.value);
+      break;
+    }
     case "LoginAdded": {
       gElements.loginList.loginAdded(event.detail.value);
       gElements.loginItem.loginAdded(event.detail.value);
       break;
     }
     case "LoginModified": {
       gElements.loginList.loginModified(event.detail.value);
       gElements.loginItem.loginModified(event.detail.value);
--- a/browser/components/aboutlogins/content/components/login-list.css
+++ b/browser/components/aboutlogins/content/components/login-list.css
@@ -60,16 +60,20 @@ ol {
   background-color: var(--in-content-box-background-odd);
 }
 
 .login-list-item.selected {
   border-inline-start-color: var(--in-content-border-highlight);
   background-color: var(--in-content-box-background-hover);
 }
 
+.login-list-item.breached {
+  border-inline-start-color: var(--in-content-border-invalid);
+}
+
 .title {
   font-weight: bold;
 }
 
 .title,
 .username {
   display: block;
   max-width: 50ch;
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -85,17 +85,22 @@ export default class LoginList extends H
     let fragment = document.createDocumentFragment();
     let chunkSize = 5;
     for (let i = 0; i < this._logins.length; i++) {
       let login = this._logins[i];
       let listItem = LoginListItemFactory.create(login);
       if (login.guid == this._selectedGuid) {
         this._setListItemAsSelected(listItem);
       }
-
+      if (
+        this._breachesByLoginGUID &&
+        this._breachesByLoginGUID.has(login.guid)
+      ) {
+        listItem.classList.add("breached");
+      }
       if (!visibleLogins.includes(login.guid)) {
         listItem.hidden = true;
       }
 
       fragment.appendChild(listItem);
 
       // Display a first chunk of logins ASAP to improve perceived performance,
       // then append progressively larger chunks until complete.
@@ -208,16 +213,24 @@ export default class LoginList extends H
             detail: firstVisibleListItem._login,
           })
         );
       }
     }
   }
 
   /**
+   * @param {Map} breachesByLoginGUID A Map of breaches by login GUIDs used for displaying breached login indicators.
+   */
+  updateBreaches(breachesByLoginGUID) {
+    this._breachesByLoginGUID = breachesByLoginGUID;
+    this.render();
+  }
+
+  /**
    * @param {login} login A login that was added to storage.
    */
   loginAdded(login) {
     this._logins.push(login);
     this.render();
   }
 
   /**
--- a/browser/components/contextualidentity/test/browser/browser_aboutURLs.js
+++ b/browser/components/contextualidentity/test/browser/browser_aboutURLs.js
@@ -13,18 +13,19 @@ add_task(async function() {
 
   // List of about: URLs that may cause problem, so we skip them in this test.
   let skipURLs = [
     // about:addons triggers an assertion in NS_CompareLoadInfoAndLoadContext:
     // "The value of mUserContextId in the loadContext and in the loadInfo are not the same"
     // This is due to a fetch request that has the default user context. Since
     // the fetch request omits credentials, the user context doesn't matter.
     "addons",
-    // about:credits will initiate network request.
+    // about:credits and about:logins will initiate network request.
     "credits",
+    "logins",
     // about:telemetry will fetch Telemetry asynchronously and takes longer,
     // so we skip this for now.
     "telemetry",
     // about:downloads causes a shutdown leak with stylo-chrome. bug 1419943.
     "downloads",
     // about:debugging requires specific wait code for internal pending RDP requests.
     "debugging",
     "debugging-new",
--- a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_aboutPages.js
+++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_aboutPages.js
@@ -163,17 +163,17 @@ add_task(async function test_remote_wind
 
 /**
  * about: pages should have firstPartyDomain set when we enable first party isolation.
  */
 add_task(async function test_aboutURL() {
   let aboutURLs = [];
 
   // List of about: URLs that will initiate network requests.
-  let networkURLs = ["credits"];
+  let networkURLs = ["credits", "logins"];
 
   for (let cid in Cc) {
     let result = cid.match(
       /@mozilla.org\/network\/protocol\/about;1\?what\=(.*)$/
     );
     if (!result) {
       continue;
     }