Bug 1548381 - Generate and cache a password for autocomplete="new-password" password fields. r=sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 21 May 2019 00:24:30 +0000
changeset 474663 c1beb9ce876d02c14c4277adabd67bfd00df502b
parent 474662 7b82d533e37e432561ac9dedc7598ea9780d40dd
child 474664 e79711ad9ef56e0ede123fd455ed444980d173bc
push id36043
push userrmaries@mozilla.com
push dateTue, 21 May 2019 09:44:47 +0000
treeherdermozilla-central@b74e5737da64 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs1548381
milestone69.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 1548381 - Generate and cache a password for autocomplete="new-password" password fields. r=sfoster Differential Revision: https://phabricator.services.mozilla.com/D31206
browser/base/content/test/static/browser_all_files_referenced.js
toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -142,18 +142,16 @@ var whitelist = [
   {file: "chrome://mozapps/skin/downloads/downloadButtons.png", platforms: ["linux", "win"]},
   // Bug 1348558
   {file: "chrome://mozapps/skin/update/downloadButtons.png",
    platforms: ["linux"]},
   // Bug 1348559
   {file: "chrome://pippki/content/resetpassword.xul"},
   // Bug 1337345
   {file: "resource://gre/modules/Manifest.jsm"},
-  // Bug 1548381
-  {file: "resource://gre/modules/PasswordGenerator.jsm"},
   // Bug 1351097
   {file: "resource://gre/modules/accessibility/AccessFu.jsm"},
   // Bug 1356043
   {file: "resource://gre/modules/PerfMeasurement.jsm"},
   // Bug 1356045
   {file: "chrome://global/content/test-ipc.xul"},
   // Bug 1378173 (warning: still used by devtools)
   {file: "resource://gre/modules/Promise.jsm"},
--- a/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
+++ b/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
@@ -389,19 +389,17 @@ LoginAutoComplete.prototype = {
       previousResult = {
         searchString: aPreviousResult.searchString,
         logins: aPreviousResult.wrappedJSObject.logins,
       };
     } else {
       previousResult = null;
     }
 
-    let rect = BrowserUtils.getElementBoundingScreenRect(aElement);
     let acLookupPromise = this._autoCompleteLookupPromise =
-      LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult,
-                                                   aElement, rect);
+      LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult, aElement);
     acLookupPromise.then(completeSearch.bind(this, acLookupPromise)).catch(log.error);
   },
 
   stopSearch() {
     this._autoCompleteLookupPromise = null;
   },
 };
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -340,39 +340,42 @@ var LoginManagerContent = {
                         options };
 
     return this._sendRequest(messageManager, requestData,
                              "PasswordManager:findLogins",
                              messageData);
   },
 
   _autoCompleteSearchAsync(aSearchString, aPreviousResult,
-                           aElement, aRect) {
+                           aElement) {
     let doc = aElement.ownerDocument;
     let form = LoginFormFactory.createFromField(aElement);
     let win = doc.defaultView;
 
     let formOrigin = LoginHelper.getLoginOrigin(doc.documentURI);
     let actionOrigin = LoginHelper.getFormActionOrigin(form);
+    let autocompleteInfo = aElement.getAutocompleteInfo();
 
     let messageManager = win.docShell.messageManager;
 
     let previousResult = aPreviousResult ?
                            { searchString: aPreviousResult.searchString,
                              logins: LoginHelper.loginsToVanillaObjects(aPreviousResult.logins) } :
                            null;
 
     let requestData = {};
-    let messageData = { formOrigin,
-                        actionOrigin,
-                        searchString: aSearchString,
-                        previousResult,
-                        rect: aRect,
-                        isSecure: InsecurePasswordUtils.isFormSecure(form),
-                        isPasswordField: aElement.type == "password",
+    let messageData = {
+      autocompleteInfo,
+      browsingContextId: win.docShell.browsingContext.id,
+      formOrigin,
+      actionOrigin,
+      searchString: aSearchString,
+      previousResult,
+      isSecure: InsecurePasswordUtils.isFormSecure(form),
+      isPasswordField: aElement.type == "password",
     };
 
     if (LoginHelper.showAutoCompleteFooter) {
       messageManager.addMessageListener("FormAutoComplete:PopupOpened", this);
       messageManager.addMessageListener("FormAutoComplete:PopupClosed", this);
     }
 
     return this._sendRequest(messageManager, requestData,
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -10,28 +10,39 @@ const {Services} = ChromeUtils.import("r
 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 ChromeUtils.defineModuleGetter(this, "AutoCompletePopup",
                                "resource://gre/modules/AutoCompletePopup.jsm");
 ChromeUtils.defineModuleGetter(this, "DeferredTask",
                                "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginHelper",
                                "resource://gre/modules/LoginHelper.jsm");
+ChromeUtils.defineModuleGetter(this, "PasswordGenerator",
+                               "resource://gre/modules/PasswordGenerator.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let logger = LoginHelper.createLogger("LoginManagerParent");
   return logger.log.bind(logger);
 });
 
 var EXPORTED_SYMBOLS = [ "LoginManagerParent" ];
 
 var LoginManagerParent = {
   /**
+   * A map of a principal's origin (including suffixes) to a generated password string so that we
+   * can offer the same password later (e.g. in a confirmation field).
+   *
+   * We don't currently evict from this cache so entries should last until the end of the browser
+   * session. That may change later but for now a typical session would max out at a few entries.
+   */
+  _generatedPasswordsByPrincipalOrigin: new Map(),
+
+  /**
    * Reference to the default LoginRecipesParent (instead of the initialization promise) for
    * synchronous access. This is a temporary hack and new consumers should yield on
    * recipeParentPromise instead.
    *
    * @type LoginRecipesParent
    * @deprecated
    */
   _recipeManager: null,
@@ -239,19 +250,26 @@ var LoginManagerParent = {
     var jsLogins = LoginHelper.loginsToVanillaObjects(logins);
     target.sendAsyncMessage("PasswordManager:loginsFound", {
       requestId,
       logins: jsLogins,
       recipes,
     });
   },
 
-  doAutocompleteSearch({ formOrigin, actionOrigin,
-                         searchString, previousResult,
-                         rect, requestId, isSecure, isPasswordField,
+  doAutocompleteSearch({
+    autocompleteInfo,
+    browsingContextId,
+    formOrigin,
+    actionOrigin,
+    searchString,
+    previousResult,
+    requestId,
+    isSecure,
+    isPasswordField,
   }, target) {
     // Note: previousResult is a regular object, not an
     // nsIAutoCompleteResult.
 
     // Cancel if we unsuccessfully prompted for the master password too recently.
     if (!Services.logins.isLoggedIn) {
       let timeDiff = Date.now() - this._lastMPLoginCancelled;
       if (timeDiff < this._repromptTimeout) {
@@ -283,32 +301,59 @@ var LoginManagerParent = {
       logins = this._searchAndDedupeLogins(formOrigin, actionOrigin, {looseActionOriginMatch: true});
     }
 
     let matchingLogins = logins.filter(function(fullMatch) {
       let match = fullMatch.username;
 
       // Remove results that are too short, or have different prefix.
       // Also don't offer empty usernames as possible results except
-      // for password field.
+      // for on password fields.
       if (isPasswordField) {
         return true;
       }
       return match && match.toLowerCase().startsWith(searchStringLower);
     });
 
+    let generatedPassword = null;
+    if (isPasswordField && autocompleteInfo.fieldName == "new-password") {
+      generatedPassword = this.getGeneratedPassword(browsingContextId);
+    }
+
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
     var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
     target.messageManager.sendAsyncMessage("PasswordManager:loginsAutoCompleted", {
       requestId,
+      generatedPassword,
       logins: jsLogins,
     });
   },
 
+  getGeneratedPassword(browsingContextId) {
+    if (!LoginHelper.enabled || !LoginHelper.generationAvailable || !LoginHelper.generationEnabled) {
+      return null;
+    }
+
+    let browsingContext = BrowsingContext.get(browsingContextId);
+    if (!browsingContext) {
+      return null;
+    }
+    let framePrincipalOrigin = browsingContext.currentWindowGlobal.documentPrincipal.origin;
+    // Use the same password if we already generated one for this origin so that it doesn't change
+    // with each search/keystroke and the user can easily re-enter a password in a confirmation field.
+    let generatedPW = this._generatedPasswordsByPrincipalOrigin.get(framePrincipalOrigin);
+    if (generatedPW) {
+      return generatedPW;
+    }
+    generatedPW = PasswordGenerator.generatePassword();
+    this._generatedPasswordsByPrincipalOrigin.set(framePrincipalOrigin, generatedPW);
+    return generatedPW;
+  },
+
   onFormSubmit({hostname, formSubmitURL, autoFilledLoginGuid,
                 usernameField, newPasswordField,
                 oldPasswordField, openerTopWindowID,
                 dismissedPrompt, target}) {
     function getPrompter() {
       var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
                         createInstance(Ci.nsILoginManagerPrompter);
       prompterSvc.init(target.ownerGlobal);