diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/components/aboutlogins/LoginBreaches.jsm | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/aboutlogins/LoginBreaches.jsm')
-rw-r--r-- | browser/components/aboutlogins/LoginBreaches.jsm | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/browser/components/aboutlogins/LoginBreaches.jsm b/browser/components/aboutlogins/LoginBreaches.jsm new file mode 100644 index 0000000000..2f299574ea --- /dev/null +++ b/browser/components/aboutlogins/LoginBreaches.jsm @@ -0,0 +1,180 @@ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + LoginHelper: "resource://gre/modules/LoginHelper.jsm", + RemoteSettings: "resource://services-settings/remote-settings.js", + RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm", +}); + +const LoginBreaches = { + REMOTE_SETTINGS_COLLECTION: "fxmonitor-breaches", + + async update(breaches = null) { + const logins = await lazy.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 lazy + .RemoteSettings(this.REMOTE_SETTINGS_COLLECTION) + .get(); + } catch (ex) { + if (ex instanceof lazy.RemoteSettingsClient.UnknownCollectionError) { + lazy.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) { + let loginHost; + try { + // nsIURI.host can throw if the URI scheme doesn't have a host. + loginHost = Services.io.newURI(login.origin).host; + } catch { + 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); + breachAlertURL.searchParams.set("utm_source", "firefox-desktop"); + breachAlertURL.searchParams.set("utm_medium", "referral"); + breachAlertURL.searchParams.set("utm_campaign", "about-logins"); + breachAlertURL.searchParams.set("utm_content", "about-logins"); + breach.breachAlertURL = breachAlertURL.href; + breachesByLoginGUID.set(login.guid, breach); + } + } + Services.telemetry.scalarSet( + "pwmgr.potentially_breached_passwords", + breachesByLoginGUID.size + ); + 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; + }, + + 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; + }, + + _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(lazy, "log", () => { + return lazy.LoginHelper.createLogger("LoginBreaches"); +}); |