summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
commita90a5cba08fdf6c0ceb95101c275108a152a3aed (patch)
tree532507288f3defd7f4dcf1af49698bcb76034855 /toolkit/components/passwordmgr
parentAdding debian version 126.0.1-1. (diff)
downloadfirefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.tar.xz
firefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/passwordmgr')
-rw-r--r--toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs254
-rw-r--r--toolkit/components/passwordmgr/LoginHelper.sys.mjs105
-rw-r--r--toolkit/components/passwordmgr/LoginManagerChild.sys.mjs338
-rw-r--r--toolkit/components/passwordmgr/LoginManagerParent.sys.mjs50
-rw-r--r--toolkit/components/passwordmgr/moz.build1
-rw-r--r--toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl31
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html26
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html6
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_isProbablyANewPasswordField.js13
9 files changed, 431 insertions, 393 deletions
diff --git a/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs b/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs
index 187caf9d62..a7ea1bfa2c 100644
--- a/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs
+++ b/toolkit/components/passwordmgr/LoginAutoComplete.sys.mjs
@@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
- * nsIAutoCompleteResult and nsILoginAutoCompleteSearch implementations for saved logins.
+ * nsIAutoCompleteResult implementations for saved logins.
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
@@ -15,11 +15,7 @@ import {
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
- LoginFormFactory: "resource://gre/modules/LoginFormFactory.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
- LoginManagerChild: "resource://gre/modules/LoginManagerChild.sys.mjs",
- NewPasswordModel: "resource://gre/modules/NewPasswordModel.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
@@ -335,6 +331,8 @@ export class LoginAutoCompleteResult {
// The footer comes last if it's enabled
if (isFooterEnabled()) {
+ // TODO: This would be removed once autofill is triggered from the parent.
+ gAutoCompleteListener.init();
if (autocompleteItems) {
this.#rows.push(
...autocompleteItems.map(
@@ -472,252 +470,6 @@ export class LoginAutoCompleteResult {
}
}
-export class LoginAutoComplete {
- // HTMLInputElement to number, the element's new-password heuristic confidence score
- #cachedNewPasswordScore = new WeakMap();
- #autoCompleteLookupPromise = null;
- classID = Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}");
- QueryInterface = ChromeUtils.generateQI(["nsILoginAutoCompleteSearch"]);
-
- /**
- * Yuck. This is called directly by satchel:
- * nsFormFillController::StartSearch()
- * [toolkit/components/satchel/nsFormFillController.cpp]
- *
- * We really ought to have a simple way for code to register an
- * auto-complete provider, and not have satchel calling pwmgr directly.
- *
- * @param {string} aSearchString The value typed in the field.
- * @param {nsIAutoCompleteResult} aPreviousResult
- * @param {HTMLInputElement} aElement
- * @param {nsIFormFillCompleteObserver} aCallback
- */
- startSearch(aSearchString, aPreviousResult, aElement, aCallback) {
- let { isNullPrincipal } = aElement.nodePrincipal;
- if (
- aElement.nodePrincipal.schemeIs("about") ||
- aElement.nodePrincipal.isSystemPrincipal
- ) {
- // Don't show autocomplete results for about: pages.
- // XXX: Don't we need to call the callback here?
- return;
- }
-
- let searchStartTimeMS = Services.telemetry.msSystemNow();
-
- // Show the insecure login warning in the passwords field on null principal documents.
- // Avoid loading InsecurePasswordUtils.sys.mjs in a sandboxed document (e.g. an ad. frame) if we
- // already know it has a null principal and will therefore get the insecure autocomplete
- // treatment.
- // InsecurePasswordUtils doesn't handle the null principal case as not secure because we don't
- // want the same treatment:
- // * The web console warnings will be confusing (as they're primarily about http:) and not very
- // useful if the developer intentionally sandboxed the document.
- // * The site identity insecure field warning would require LoginManagerChild being loaded and
- // listening to some of the DOM events we're ignoring in null principal documents. For memory
- // reasons it's better to not load LMC at all for these sandboxed frames. Also, if the top-
- // document is sandboxing a document, it probably doesn't want that sandboxed document to be
- // able to affect the identity icon in the address bar by adding a password field.
- let form = lazy.LoginFormFactory.createFromField(aElement);
- let isSecure =
- !isNullPrincipal && lazy.InsecurePasswordUtils.isFormSecure(form);
- let { hasBeenTypePassword } = aElement;
- let hostname = aElement.ownerDocument.documentURIObject.host;
- let formOrigin = lazy.LoginHelper.getLoginOrigin(
- aElement.ownerDocument.documentURI
- );
- let loginManagerActor = lazy.LoginManagerChild.forWindow(
- aElement.ownerGlobal
- );
- let completeSearch = async autoCompleteLookupPromise => {
- // Assign to the member synchronously before awaiting the Promise.
- this.#autoCompleteLookupPromise = autoCompleteLookupPromise;
-
- let {
- generatedPassword,
- importable,
- logins,
- autocompleteItems,
- willAutoSaveGeneratedPassword,
- } = await autoCompleteLookupPromise;
-
- // If the search was canceled before we got our
- // results, don't bother reporting them.
- // N.B. This check must occur after the `await` above for it to be
- // effective.
- if (this.#autoCompleteLookupPromise !== autoCompleteLookupPromise) {
- lazy.log.debug("Ignoring result from previous search.");
- return;
- }
-
- let telemetryEventData = {
- acFieldName: aElement.getAutocompleteInfo().fieldName,
- hadPrevious: !!aPreviousResult,
- typeWasPassword: aElement.hasBeenTypePassword,
- fieldType: aElement.type,
- searchStartTimeMS,
- stringLength: aSearchString.length,
- };
-
- this.#autoCompleteLookupPromise = null;
- let results = new LoginAutoCompleteResult(
- aSearchString,
- logins,
- autocompleteItems,
- formOrigin,
- {
- generatedPassword,
- willAutoSaveGeneratedPassword,
- importable,
- actor: loginManagerActor,
- isSecure,
- hasBeenTypePassword,
- hostname,
- telemetryEventData,
- }
- );
- aCallback.onSearchCompletion(results);
- };
-
- if (isNullPrincipal) {
- // Don't search login storage when the field has a null principal as we don't want to fill
- // logins for the `location` in this case.
- completeSearch(Promise.resolve({ logins: [] }));
- return;
- }
-
- if (
- hasBeenTypePassword &&
- aSearchString &&
- !loginManagerActor.isPasswordGenerationForcedOn(aElement)
- ) {
- // Return empty result on password fields with password already filled,
- // unless password generation was forced.
- completeSearch(Promise.resolve({ logins: [] }));
- return;
- }
-
- if (!lazy.LoginHelper.enabled) {
- completeSearch(Promise.resolve({ logins: [] }));
- return;
- }
-
- let previousResult;
- if (aPreviousResult) {
- previousResult = {
- searchString: aPreviousResult.searchString,
- logins: lazy.LoginHelper.loginsToVanillaObjects(
- aPreviousResult.wrappedJSObject.logins
- ),
- };
- } else {
- previousResult = null;
- }
-
- let acLookupPromise = this.#requestAutoCompleteResultsFromParent({
- searchString: aSearchString,
- previousResult,
- inputElement: aElement,
- form,
- hasBeenTypePassword,
- });
- completeSearch(acLookupPromise).catch(lazy.log.error.bind(lazy.log));
- }
-
- stopSearch() {
- this.#autoCompleteLookupPromise = null;
- }
-
- async #requestAutoCompleteResultsFromParent({
- searchString,
- previousResult,
- inputElement,
- form,
- hasBeenTypePassword,
- }) {
- let actionOrigin = lazy.LoginHelper.getFormActionOrigin(form);
- let autocompleteInfo = inputElement.getAutocompleteInfo();
-
- let loginManagerActor = lazy.LoginManagerChild.forWindow(
- inputElement.ownerGlobal
- );
- let forcePasswordGeneration = false;
- let isProbablyANewPasswordField = false;
- if (hasBeenTypePassword) {
- forcePasswordGeneration =
- loginManagerActor.isPasswordGenerationForcedOn(inputElement);
- // Run the Fathom model only if the password field does not have the
- // autocomplete="new-password" attribute.
- isProbablyANewPasswordField =
- autocompleteInfo.fieldName == "new-password" ||
- this.isProbablyANewPasswordField(inputElement);
- }
- const scenario = loginManagerActor.getScenario(inputElement);
-
- if (lazy.LoginHelper.showAutoCompleteFooter) {
- gAutoCompleteListener.init();
- }
-
- lazy.log.debug("LoginAutoComplete search:", {
- forcePasswordGeneration,
- hasBeenTypePassword,
- isProbablyANewPasswordField,
- searchStringLength: searchString.length,
- });
-
- const result = await loginManagerActor.sendQuery(
- "PasswordManager:autoCompleteLogins",
- {
- actionOrigin,
- searchString,
- previousResult,
- forcePasswordGeneration,
- hasBeenTypePassword,
- isProbablyANewPasswordField,
- scenarioName: scenario?.constructor.name,
- inputMaxLength: inputElement.maxLength,
- isWebAuthn: this.#isWebAuthnCredentials(autocompleteInfo),
- }
- );
-
- return {
- generatedPassword: result.generatedPassword,
- importable: result.importable,
- autocompleteItems: result.autocompleteItems,
- logins: lazy.LoginHelper.vanillaObjectsToLogins(result.logins),
- willAutoSaveGeneratedPassword: result.willAutoSaveGeneratedPassword,
- };
- }
-
- isProbablyANewPasswordField(inputElement) {
- const threshold = lazy.LoginHelper.generationConfidenceThreshold;
- if (threshold == -1) {
- // Fathom is disabled
- return false;
- }
-
- let score = this.#cachedNewPasswordScore.get(inputElement);
- if (score) {
- return score >= threshold;
- }
-
- const { rules, type } = lazy.NewPasswordModel;
- const results = rules.against(inputElement);
- score = results.get(inputElement).scoreFor(type);
- this.#cachedNewPasswordScore.set(inputElement, score);
- return score >= threshold;
- }
-
- /**
- * @param {string} autocompleteInfo
- * @returns whether the non-autofill credential type (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#non-autofill-credential-type)
- * of the input field is "webauthn"
- */
- #isWebAuthnCredentials(autocompleteInfo) {
- return autocompleteInfo.credentialType == "webauthn";
- }
-}
-
let gAutoCompleteListener = {
added: false,
diff --git a/toolkit/components/passwordmgr/LoginHelper.sys.mjs b/toolkit/components/passwordmgr/LoginHelper.sys.mjs
index 5626312f17..615ac79449 100644
--- a/toolkit/components/passwordmgr/LoginHelper.sys.mjs
+++ b/toolkit/components/passwordmgr/LoginHelper.sys.mjs
@@ -18,6 +18,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
});
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "Crypto",
+ "@mozilla.org/login-manager/crypto/SDR;1",
+ "nsILoginManagerCrypto"
+);
+
export class ParentAutocompleteOption {
image;
title;
@@ -366,7 +373,7 @@ class ImportRowProcessor {
return this.summary;
}
}
-
+const OS_AUTH_FOR_PASSWORDS_PREF = "signon.management.page.os-auth.optout";
/**
* Contains functions shared by different Login Manager components.
*/
@@ -394,6 +401,7 @@ export const LoginHelper = {
testOnlyUserHasInteractedWithDocument: null,
userInputRequiredToCapture: null,
captureInputChanges: null,
+ OS_AUTH_FOR_PASSWORDS_PREF,
init() {
// Watch for pref changes to update cached pref values.
@@ -1583,6 +1591,90 @@ export const LoginHelper = {
},
/**
+ * Get the decrypted value for a string pref.
+ *
+ * @param {string} prefName -> The pref whose value is needed.
+ * @param {string} safeDefaultValue -> Value to be returned incase the pref is not yet set.
+ * @returns {string}
+ */
+ getSecurePref(prefName, safeDefaultValue) {
+ try {
+ const encryptedValue = Services.prefs.getStringPref(prefName, "");
+ return encryptedValue === ""
+ ? safeDefaultValue
+ : lazy.Crypto.decrypt(encryptedValue);
+ } catch {
+ return safeDefaultValue;
+ }
+ },
+
+ /**
+ * Set the pref to the encrypted form of the value.
+ *
+ * @param {string} prefName -> The pref whose value is to be set.
+ * @param {string} value -> The value to be set in its encrypted form.
+ */
+ setSecurePref(prefName, value) {
+ if (value) {
+ const encryptedValue = lazy.Crypto.encrypt(value);
+ Services.prefs.setStringPref(prefName, encryptedValue);
+ } else {
+ Services.prefs.clearUserPref(prefName);
+ }
+ },
+
+ /**
+ * Get whether the OSAuth is enabled or not.
+ *
+ * @param {string} prefName -> The name of the pref (creditcards or addresses)
+ * @returns {boolean}
+ */
+ getOSAuthEnabled(prefName) {
+ return (
+ lazy.OSKeyStore.canReauth() &&
+ this.getSecurePref(prefName, "") !== "opt out"
+ );
+ },
+
+ /**
+ * Set whether the OSAuth is enabled or not.
+ *
+ * @param {string} prefName -> The pref to encrypt.
+ * @param {boolean} enable -> Whether the pref is to be enabled.
+ */
+ setOSAuthEnabled(prefName, enable) {
+ this.setSecurePref(prefName, enable ? null : "opt out");
+ },
+
+ async verifyUserOSAuth(
+ prefName,
+ promptMessage,
+ captionDialog = "",
+ parentWindow = null,
+ generateKeyIfNotAvailable = true
+ ) {
+ if (!this.getOSAuthEnabled(prefName)) {
+ promptMessage = false;
+ }
+ try {
+ return (
+ await lazy.OSKeyStore.ensureLoggedIn(
+ promptMessage,
+ captionDialog,
+ parentWindow,
+ generateKeyIfNotAvailable
+ )
+ ).authenticated;
+ } catch (ex) {
+ // Since Win throws an exception whereas Mac resolves to false upon cancelling.
+ if (ex.result !== Cr.NS_ERROR_FAILURE) {
+ throw ex;
+ }
+ }
+ return false;
+ },
+
+ /**
* Shows the Primary Password prompt if enabled, or the
* OS auth dialog otherwise.
* @param {Element} browser
@@ -1637,18 +1729,21 @@ export const LoginHelper = {
}
// Use the OS auth dialog if there is no primary password
if (!token.hasPassword && OSReauthEnabled) {
- let result = await lazy.OSKeyStore.ensureLoggedIn(
+ let isAuthorized = await this.verifyUserOSAuth(
+ OS_AUTH_FOR_PASSWORDS_PREF,
messageText,
captionText,
browser.ownerGlobal,
false
);
- isAuthorized = result.authenticated;
+ let value = lazy.OSKeyStore.canReauth()
+ ? "success"
+ : "success_unsupported_platform";
+
telemetryEvent = {
object: "os_auth",
method: "reauthenticate",
- value: result.auth_details,
- extra: result.auth_details_extra,
+ value: isAuthorized ? value : "fail",
};
return {
isAuthorized,
diff --git a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
index 66f45416f8..5433083ede 100644
--- a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
+++ b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
@@ -51,10 +51,12 @@ ChromeUtils.defineESModuleGetters(lazy, {
FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
+ LoginAutoCompleteResult: "resource://gre/modules/LoginAutoComplete.sys.mjs",
LoginFormFactory: "resource://gre/modules/LoginFormFactory.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
LoginRecipesContent: "resource://gre/modules/LoginRecipes.sys.mjs",
LoginManagerTelemetry: "resource://gre/modules/LoginManagerTelemetry.sys.mjs",
+ NewPasswordModel: "resource://gre/modules/NewPasswordModel.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
@@ -157,40 +159,6 @@ const observer = {
loginManagerChild()._onNavigation(window.document);
},
- // nsIObserver
- observe(subject, topic, _data) {
- switch (topic) {
- case "autocomplete-did-enter-text": {
- let input = subject.QueryInterface(Ci.nsIAutoCompleteInput);
- let { selectedIndex } = input.popup;
- if (selectedIndex < 0 || selectedIndex >= input.controller.matchCount) {
- break;
- }
-
- let { focusedInput } = lazy.gFormFillService;
- if (focusedInput.nodePrincipal.isNullPrincipal) {
- // If we have a null principal then prevent any more password manager code from running and
- // incorrectly using the document `location`.
- return;
- }
-
- let window = focusedInput.ownerGlobal;
- let loginManagerChild = LoginManagerChild.forWindow(window);
-
- let style = input.controller.getStyleAt(selectedIndex);
- if (style == "login" || style == "loginWithOrigin") {
- let details = JSON.parse(
- input.controller.getCommentAt(selectedIndex)
- );
- loginManagerChild.onFieldAutoComplete(focusedInput, details.guid);
- } else if (style == "generatedPassword") {
- loginManagerChild._filledWithGeneratedPassword(focusedInput);
- }
- break;
- }
- }
- },
-
// nsIDOMEventListener
handleEvent(aEvent) {
if (!aEvent.isTrusted) {
@@ -435,9 +403,6 @@ const observer = {
},
};
-// Add this observer once for the process.
-Services.obs.addObserver(observer, "autocomplete-did-enter-text");
-
/**
* Form scenario defines what can be done with form.
*/
@@ -812,7 +777,9 @@ export class LoginFormState {
// The login manager is responsible for fields with the "webauthn" credential type.
let acCredentialType = focusedField.getAutocompleteInfo()?.credentialType;
if (acCredentialType == "webauthn") {
- lazy.gFormFillService.markAsLoginManagerField(focusedField);
+ const actor =
+ focusedField.ownerGlobal.windowGlobalChild.getActor("LoginManager");
+ actor.markAsAutoCompletableField(focusedField);
}
lazy.log("Opening the autocomplete popup.");
@@ -1525,6 +1492,16 @@ export class LoginManagerChild extends JSWindowActorChild {
this.notifyObserversOfFormProcessed(msg.data.formid);
break;
}
+ case "PasswordManager:fillFields": {
+ const login = lazy.LoginHelper.vanillaObjectToLogin(msg.data);
+ this.fillFields(login);
+ break;
+ }
+ case "PasswordManager:fillGeneratedPassword": {
+ const { focusedInput } = lazy.gFormFillService;
+ this.filledWithGeneratedPassword(focusedInput);
+ break;
+ }
}
return undefined;
@@ -2174,7 +2151,7 @@ export class LoginManagerChild extends JSWindowActorChild {
/**
* A username or password was autocompleted into a field.
*/
- onFieldAutoComplete(acInputField, loginGUID) {
+ onFieldAutoComplete(acInputField, login) {
if (!lazy.LoginHelper.enabled) {
return;
}
@@ -2189,7 +2166,7 @@ export class LoginManagerChild extends JSWindowActorChild {
}
if (lazy.LoginHelper.isUsernameFieldType(acInputField)) {
- this.onUsernameAutocompleted(acInputField, loginGUID);
+ this.onUsernameAutocompleted(acInputField, [login]);
} else if (acInputField.hasBeenTypePassword) {
// Ensure the field gets re-masked and edits don't overwrite the generated
// password in case a generated password was filled into it previously.
@@ -2203,7 +2180,7 @@ export class LoginManagerChild extends JSWindowActorChild {
* A username field was filled or tabbed away from so try fill in the
* associated password in the password field.
*/
- onUsernameAutocompleted(acInputField, loginGUID = null) {
+ async onUsernameAutocompleted(acInputField, loginsFound = null) {
lazy.log(`Autocompleting input field with name: ${acInputField.name}`);
let acForm = lazy.LoginFormFactory.createFromField(acInputField);
@@ -2219,43 +2196,49 @@ export class LoginManagerChild extends JSWindowActorChild {
const docState = this.stateForDocument(acInputField.ownerDocument);
let { usernameField, newPasswordField: passwordField } =
docState._getFormFields(acForm, false, recipes);
- if (usernameField == acInputField) {
- // Fill the form when a password field is present.
- if (passwordField) {
- this._getLoginDataFromParent(acForm, {
- guid: loginGUID,
- showPrimaryPassword: false,
- })
- .then(({ form, loginsFound, recipes }) => {
- if (!loginGUID) {
- // not an explicit autocomplete menu selection, filter for exact matches only
- loginsFound = this._filterForExactFormOriginLogins(
- loginsFound,
- acForm
- );
- // filter the list for exact matches with the username
- // NOTE: this could be an empty string which is a valid username
- let searchString = usernameField.value.toLowerCase();
- loginsFound = loginsFound.filter(
- l => l.username.toLowerCase() == searchString
- );
- }
-
- this._fillForm(form, loginsFound, recipes, {
- autofillForm: true,
- clobberPassword: true,
- userTriggered: true,
- });
- })
- .catch(console.error);
- // Use `loginGUID !== null` to distinguish whether this is called when the
- // field is filled or tabbed away from. For the latter, don't highlight the field.
- } else if (loginGUID !== null) {
+ // Ignore the event, it's for some input we don't care about.
+ if (usernameField != acInputField) {
+ return;
+ }
+
+ if (!passwordField) {
+ // Use `loginsFound !== null` to distinguish whether this is called when the
+ // field is filled or tabbed away from. For the latter, don't highlight the field.
+ if (loginsFound !== null) {
LoginFormState._highlightFilledField(usernameField);
}
- } else {
- // Ignore the event, it's for some input we don't care about.
+ return;
+ }
+
+ // Fill the form when a password field is present.
+ if (!loginsFound) {
+ const loginData = await this._getLoginDataFromParent(acForm, {
+ showPrimaryPassword: false,
+ }).catch(console.error);
+
+ if (!loginData?.loginsFound.length) {
+ return;
+ }
+
+ // not an explicit autocomplete menu selection, filter for exact matches only
+ loginsFound = this._filterForExactFormOriginLogins(
+ loginData.loginsFound,
+ acForm
+ );
+ // filter the list for exact matches with the username
+ // NOTE: this could be an empty string which is a valid username
+ const searchString = usernameField.value.toLowerCase();
+ loginsFound = loginsFound.filter(
+ l => l.username.toLowerCase() == searchString
+ );
+ recipes = loginData.recipes;
}
+
+ this._fillForm(acForm, loginsFound, recipes, {
+ autofillForm: true,
+ clobberPassword: true,
+ userTriggered: true,
+ });
}
/**
@@ -2399,7 +2382,7 @@ export class LoginManagerChild extends JSWindowActorChild {
};
if (fields.usernameField) {
- lazy.gFormFillService.markAsLoginManagerField(fields.usernameField);
+ this.markAsAutoCompletableField(fields.usernameField);
}
// It's possible the field triggering this message isn't one of those found by _getFormFields' heuristics
@@ -2639,7 +2622,7 @@ export class LoginManagerChild extends JSWindowActorChild {
* field is handled accordingly.
* @param {HTMLInputElement} passwordField
*/
- _filledWithGeneratedPassword(passwordField) {
+ filledWithGeneratedPassword(passwordField) {
LoginFormState._highlightFilledField(passwordField);
this._passwordEditedOrGenerated(passwordField, {
triggeredByFillingGenerated: true,
@@ -2821,7 +2804,7 @@ export class LoginManagerChild extends JSWindowActorChild {
if (scenario) {
docState.setScenario(form.rootElement, scenario);
- lazy.gFormFillService.markAsLoginManagerField(usernameField);
+ this.markAsAutoCompletableField(usernameField);
}
}
@@ -2868,7 +2851,7 @@ export class LoginManagerChild extends JSWindowActorChild {
// We would also need this attached to show the insecure login
// warning, regardless of saved login.
if (usernameField) {
- lazy.gFormFillService.markAsLoginManagerField(usernameField);
+ this.markAsAutoCompletableField(usernameField);
usernameField.addEventListener("keydown", observer);
}
@@ -3116,7 +3099,7 @@ export class LoginManagerChild extends JSWindowActorChild {
}
if (style === "generatedPassword") {
- this._filledWithGeneratedPassword(passwordField);
+ this.filledWithGeneratedPassword(passwordField);
}
lazy.log("_fillForm succeeded");
@@ -3173,4 +3156,199 @@ export class LoginManagerChild extends JSWindowActorChild {
const docState = this.stateForDocument(inputElement.ownerDocument);
return docState.getScenario(inputElement);
}
+
+ #interestedInputs = [];
+
+ markAsAutoCompletableField(input) {
+ this.#interestedInputs.push(input);
+ this.manager
+ .getActor("AutoComplete")
+ ?.markAsAutoCompletableField(input, this);
+ }
+
+ get actorName() {
+ return "LoginManager";
+ }
+
+ /**
+ * Get the search options when searching for autocomplete entries in the parent
+ *
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @returns {object} the search options for the input
+ */
+ getAutoCompleteSearchOption(input, searchString) {
+ const form = lazy.LoginFormFactory.createFromField(input);
+ const formOrigin = lazy.LoginHelper.getLoginOrigin(
+ input.ownerDocument.documentURI
+ );
+ const actionOrigin = lazy.LoginHelper.getFormActionOrigin(form);
+ const autocompleteInfo = input.getAutocompleteInfo();
+ const hasBeenTypePassword = input.hasBeenTypePassword;
+
+ let forcePasswordGeneration = false;
+ let isProbablyANewPasswordField = false;
+ if (hasBeenTypePassword) {
+ forcePasswordGeneration = this.isPasswordGenerationForcedOn(input);
+ // Run the Fathom model only if the password field does not have the
+ // autocomplete="new-password" attribute.
+ isProbablyANewPasswordField =
+ autocompleteInfo.fieldName == "new-password" ||
+ this.isProbablyANewPasswordField(input);
+ }
+
+ const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
+ ? "SignUpFormScenario"
+ : "";
+
+ const r = {
+ formOrigin,
+ actionOrigin,
+ searchString,
+ forcePasswordGeneration,
+ hasBeenTypePassword,
+ isProbablyANewPasswordField,
+ scenarioName,
+ inputMaxLength: input.maxLength,
+ isWebAuthn: this.#isWebAuthnCredentials(autocompleteInfo),
+ };
+ return r;
+ }
+
+ #searchStartTimeMS = null;
+
+ /**
+ * Ask the provider whether it might have autocomplete entry to show
+ * for the given input.
+ *
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @returns {boolean} true if we shold search for autocomplete entries
+ */
+ shouldSearchForAutoComplete(input, searchString) {
+ this.#searchStartTimeMS = Services.telemetry.msSystemNow();
+
+ // Don't search login storage when the field has a null principal as we don't want to fill
+ // logins for the `location` in this case.
+ if (input.nodePrincipal.isNullPrincipal) {
+ return false;
+ }
+
+ // Return empty result on password fields with password already filled,
+ // unless password generation was forced.
+ if (
+ input.hasBeenTypePassword &&
+ searchString &&
+ !this.isPasswordGenerationForcedOn(input)
+ ) {
+ return false;
+ }
+
+ if (!lazy.LoginHelper.enabled) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Convert the search result to autocomplete results
+ *
+ * @param {string} searchString - The string to search for
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @param {Array<object>} records - autocomplete records
+ * @returns {AutocompleteResult}
+ */
+ searchResultToAutoCompleteResult(searchString, input, records) {
+ if (
+ input.nodePrincipal.schemeIs("about") ||
+ input.nodePrincipal.isSystemPrincipal
+ ) {
+ // Don't show autocomplete results for about: pages.
+ return null;
+ }
+
+ let {
+ generatedPassword,
+ autocompleteItems,
+ importable,
+ logins,
+ willAutoSaveGeneratedPassword,
+ } = records ?? {};
+ logins ||= [];
+
+ const formOrigin = lazy.LoginHelper.getLoginOrigin(
+ input.ownerDocument.documentURI
+ );
+
+ const isNullPrincipal = input.nodePrincipal.isNullPrincipal;
+ const form = lazy.LoginFormFactory.createFromField(input);
+ const isSecure =
+ !isNullPrincipal && lazy.InsecurePasswordUtils.isFormSecure(form);
+
+ const telemetryEventData = {
+ acFieldName: input.getAutocompleteInfo().fieldName,
+ //hadPrevious: !!aPreviousResult,
+ hadPrevious: false,
+ typeWasPassword: input.hasBeenTypePassword,
+ fieldType: input.type,
+ searchStartTimeMS: this.#searchStartTimeMS,
+ stringLength: searchString.length,
+ };
+
+ const acResult = new lazy.LoginAutoCompleteResult(
+ searchString,
+ lazy.LoginHelper.vanillaObjectsToLogins(logins),
+ autocompleteItems,
+ formOrigin,
+ {
+ generatedPassword,
+ willAutoSaveGeneratedPassword,
+ importable,
+ actor: this,
+ isSecure,
+ hasBeenTypePassword: input.hasBeenTypePassword,
+ hostname: input.ownerDocument.documentURIObject.host,
+ telemetryEventData,
+ }
+ );
+ return acResult;
+ }
+
+ isLoginManagerField(input) {
+ return input.hasBeenTypePassword || this.#interestedInputs.includes(input);
+ }
+
+ #cachedNewPasswordScore = new WeakMap();
+
+ isProbablyANewPasswordField(inputElement) {
+ const threshold = lazy.LoginHelper.generationConfidenceThreshold;
+ if (threshold == -1) {
+ // Fathom is disabled
+ return false;
+ }
+
+ let score = this.#cachedNewPasswordScore.get(inputElement);
+ if (score) {
+ return score >= threshold;
+ }
+
+ const { rules, type } = lazy.NewPasswordModel;
+ const results = rules.against(inputElement);
+ score = results.get(inputElement).scoreFor(type);
+ this.#cachedNewPasswordScore.set(inputElement, score);
+ return score >= threshold;
+ }
+
+ /**
+ * @param {string} autocompleteInfo
+ * @returns whether the non-autofill credential type (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#non-autofill-credential-type)
+ * of the input field is "webauthn"
+ */
+ #isWebAuthnCredentials(autocompleteInfo) {
+ return autocompleteInfo.credentialType == "webauthn";
+ }
+
+ fillFields(login) {
+ let { focusedInput } = lazy.gFormFillService;
+ this.onFieldAutoComplete(focusedInput, login);
+ }
}
diff --git a/toolkit/components/passwordmgr/LoginManagerParent.sys.mjs b/toolkit/components/passwordmgr/LoginManagerParent.sys.mjs
index 862ab6b846..cb3ea2bd7c 100644
--- a/toolkit/components/passwordmgr/LoginManagerParent.sys.mjs
+++ b/toolkit/components/passwordmgr/LoginManagerParent.sys.mjs
@@ -829,15 +829,19 @@ export class LoginManagerParent extends JSWindowActorParent {
}))
);
}
- autocompleteItems.push(
- ...(await lazy.WebAuthnFeature.autocompleteItemsAsync(
- this._overrideBrowsingContextId ??
- this.getRootBrowser().browsingContext.id,
- formOrigin,
- scenarioName,
- isWebAuthn
- ))
- );
+ // This check is only used to init webauthn in tests, which causes
+ // intermittent like Bug 1890419.
+ if (LoginManagerParent._webAuthnAutoComplete) {
+ autocompleteItems.push(
+ ...(await lazy.WebAuthnFeature.autocompleteItemsAsync(
+ this._overrideBrowsingContextId ??
+ this.getRootBrowser().browsingContext.id,
+ formOrigin,
+ scenarioName,
+ isWebAuthn
+ ))
+ );
+ }
return {
generatedPassword,
@@ -1541,6 +1545,27 @@ export class LoginManagerParent extends JSWindowActorParent {
return gRecipeManager.initializationPromise;
}
+
+ async searchAutoCompleteEntries(searchString, data) {
+ return this.doAutocompleteSearch(data.formOrigin, data);
+ }
+
+ previewFields(_result) {
+ // Logins do not show previews
+ }
+
+ autofillFields(result) {
+ if (result.style == "login" || result.style == "loginWithOrigin") {
+ try {
+ const profile = JSON.parse(result.comment);
+ this.sendAsyncMessage("PasswordManager:fillFields", profile.login);
+ } catch (e) {
+ lazy.log("Fail to get autofill profile: ", e.message);
+ }
+ } else if (result.style == "generatedPassword") {
+ this.sendAsyncMessage("PasswordManager:fillGeneratedPassword");
+ }
+ }
}
LoginManagerParent.SUGGEST_IMPORT_DEBOUNCE_MS = 10000;
@@ -1551,3 +1576,10 @@ XPCOMUtils.defineLazyPreferenceGetter(
"signon.masterPasswordReprompt.timeout_ms",
900000
); // 15 Minutes
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ LoginManagerParent,
+ "_webAuthnAutoComplete",
+ "signon.webauthn.autocomplete",
+ true
+);
diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build
index 4fbb9dcbf4..16e4303702 100644
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -16,7 +16,6 @@ TESTING_JS_MODULES += [
]
XPIDL_SOURCES += [
- "nsILoginAutoCompleteSearch.idl",
"nsILoginInfo.idl",
"nsILoginManager.idl",
"nsILoginManagerAuthPrompter.idl",
diff --git a/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl b/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl
deleted file mode 100644
index 586292d04d..0000000000
--- a/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl
+++ /dev/null
@@ -1,31 +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/. */
-
-#include "nsISupports.idl"
-
-interface nsIAutoCompleteResult;
-interface nsIFormFillCompleteObserver;
-
-webidl HTMLInputElement;
-
-[scriptable, uuid(2bdac17c-53f1-4896-a521-682ccdeef3a8)]
-interface nsILoginAutoCompleteSearch : nsISupports {
- /**
- * Generate results for a login field autocomplete menu.
- *
- * NOTE: This interface is provided for use only by the FormFillController,
- * which calls it directly. This isn't really ideal, it should
- * probably be callback registered through the FFC.
- * NOTE: This API is different than nsIAutoCompleteSearch.
- */
- void startSearch(in AString aSearchString,
- in nsIAutoCompleteResult aPreviousResult,
- in HTMLInputElement aElement,
- in nsIFormFillCompleteObserver aListener);
-
- /**
- * Stop a previously-started search.
- */
- void stopSearch();
-};
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
index 54cb450815..b5db098174 100644
--- a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
@@ -146,21 +146,29 @@ Login Manager test: filling generated passwords into confirm password fields
"resetLoginsAndGeneratedPasswords", () => {
LoginTestUtils.clearData();
LoginTestUtils.resetGeneratedPasswordsCache();
+ return Promise.resolve();
}
);
});
function resetLoginsAndGeneratedPasswords() {
- return setupScript.sendAsyncMessage("resetLoginsAndGeneratedPasswords");
+ return setupScript.sendQuery("resetLoginsAndGeneratedPasswords");
}
async function triggerPasswordGeneration(form) {
await openPopupOn(form.pword);
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
+
+ const storageAddPromise = promiseStorageChanged(["addLogin"]);
await SimpleTest.promiseWaitForCondition(() => !!form.pword.value, "Wait for generated password to get filled");
+ await storageAddPromise;
}
+ add_setup(async () => {
+ SpecialPowers.pushPrefEnv({"set": [["signon.webauthn.autocomplete", false]]});
+ })
+
add_named_task("autocomplete menu contains option to generate password", async () => {
await resetLoginsAndGeneratedPasswords();
const form = setContentForTask(formTemplates.form1);
@@ -206,7 +214,7 @@ Login Manager test: filling generated passwords into confirm password fields
await resetLoginsAndGeneratedPasswords();
const form = setContentForTask(formTemplates.form1);
await triggerPasswordGeneration(form);
- is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pword.value == form.pwordNext.value, "Value of the confirm field has been filled with generated password");
});
add_named_task("password field is not masked initially after password generation", async () => {
@@ -270,7 +278,7 @@ Login Manager test: filling generated passwords into confirm password fields
form.pword.blur();
await messageSentPromise;
- is(form.pwordNext.value, generatedPassword, "Value of the confirm field still holds the original generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == generatedPassword, "Value of the confirm field still holds the original generated password");
ok(form.pwordNext.matches(":autofill"), "Highlight is still applied to password confirmation field");
});
@@ -314,21 +322,21 @@ Login Manager test: filling generated passwords into confirm password fields
form.pwordNext.focus()
sendString("edited value");
await triggerPasswordGeneration(form);
- is(form.pwordNext.value, "edited value", "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == "edited value", "Value of the confirm field has been filled with generated password");
});
add_named_task("password confirmation does not get filled with the generated password if its readonly", async () => {
await resetLoginsAndGeneratedPasswords();
const form = setContentForTask(formTemplates.form3);
await triggerPasswordGeneration(form);
- is(form.pwordNext.value, "", "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == "", "Value of the confirm field has been filled with generated password");
});
add_named_task("password confirmation does not get filled with the generated password if its disabled", async () => {
await resetLoginsAndGeneratedPasswords();
const form = setContentForTask(formTemplates.form4);
await triggerPasswordGeneration(form);
- is(form.pwordNext.value, "", "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == "", "Value of the confirm field has been filled with generated password");
});
add_named_task("password confirmation matching autocomplete info gets filled with the generated password", async () => {
@@ -336,14 +344,14 @@ Login Manager test: filling generated passwords into confirm password fields
const form = setContentForTask(formTemplates.form5);
await triggerPasswordGeneration(form);
is(form.pwordBetween.value, "", "Value of the between field has not been filled");
- is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == form.pword.value, "Value of the confirm field has been filled with generated password");
});
add_named_task("password confirmation matching autocomplete info gets ignored if its disabled, even if has autocomplete info", async () => {
await resetLoginsAndGeneratedPasswords();
const form = setContentForTask(formTemplates.form6);
await triggerPasswordGeneration(form);
- is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == form.pword.value, "Value of the confirm field has been filled with generated password");
is(form.pwordAfter.value, "", "Value of the disabled confirmation field has not been filled");
});
@@ -365,7 +373,7 @@ Login Manager test: filling generated passwords into confirm password fields
await resetLoginsAndGeneratedPasswords();
const form = setContentForTask(formTemplates.form9);
await triggerPasswordGeneration(form);
- is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
+ await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == form.pword.value, "Value of the confirm field has been filled with generated password");
});
add_named_task("do not fill third password field after the confirm-password field", async () => {
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
index 803197c2a2..ae4586f1bb 100644
--- a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
@@ -22,6 +22,8 @@ const iframe = document.getElementsByTagName("iframe")[0];
let iframeDoc, hostname;
add_setup(async () => {
+ SpecialPowers.pushPrefEnv({"set": [["signon.webauthn.autocomplete", false]]});
+
const origin = window.location.origin;
await setStoredLoginsAsync(
[origin, origin, null, "name", "pass"],
@@ -96,9 +98,9 @@ add_task(async function test_not_reopened_after_selecting() {
listenForUnexpectedPopupShown();
await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
- let formFillController = SpecialPowers.getFormFillController();
+ const actor = content.windowGlobalChild.getActor("AutoComplete");
let usernameField = this.content.document.getElementById("form-basic-username");
- formFillController.markAsLoginManagerField(usernameField);
+ actor.markAsAutoCompletableField(usernameField);
});
info("Waiting to see if a popupshown occurs");
diff --git a/toolkit/components/passwordmgr/test/unit/test_isProbablyANewPasswordField.js b/toolkit/components/passwordmgr/test/unit/test_isProbablyANewPasswordField.js
index 167c160da2..0bc88734d2 100644
--- a/toolkit/components/passwordmgr/test/unit/test_isProbablyANewPasswordField.js
+++ b/toolkit/components/passwordmgr/test/unit/test_isProbablyANewPasswordField.js
@@ -4,9 +4,10 @@
"use strict";
-const LoginAutoComplete = Cc[
- "@mozilla.org/login-manager/autocompletesearch;1"
-].getService(Ci.nsILoginAutoCompleteSearch).wrappedJSObject;
+const { LoginManagerChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerChild.sys.mjs"
+);
+
// TODO: create a fake window for the test document to pass fathom.isVisible check.
// We should consider moving these tests to mochitest because many fathom
// signals rely on visibility, position, etc., of the test element (See Bug 1712699),
@@ -143,9 +144,10 @@ add_task(async function test_returns_false_when_pref_disabled() {
"http://localhost:8080/test/",
testcase.document
);
+ const lmc = new LoginManagerChild();
for (let [i, input] of testcase.inputs ||
document.querySelectorAll(`input[type="password"]`).entries()) {
- const result = LoginAutoComplete.isProbablyANewPasswordField(input);
+ const result = lmc.isProbablyANewPasswordField(input);
Assert.strictEqual(
result,
false,
@@ -172,10 +174,11 @@ for (let testcase of TESTCASES) {
document = makeDocumentVisibleToFathom(document);
+ const lmc = new LoginManagerChild();
const results = [];
for (let input of testcase.inputs ||
document.querySelectorAll(`input[type="password"]`)) {
- const result = LoginAutoComplete.isProbablyANewPasswordField(input);
+ const result = lmc.isProbablyANewPasswordField(input);
results.push(result);
}