summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutlogins/LoginBreaches.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutlogins/LoginBreaches.sys.mjs')
-rw-r--r--browser/components/aboutlogins/LoginBreaches.sys.mjs176
1 files changed, 176 insertions, 0 deletions
diff --git a/browser/components/aboutlogins/LoginBreaches.sys.mjs b/browser/components/aboutlogins/LoginBreaches.sys.mjs
new file mode 100644
index 0000000000..bd7a8cdf66
--- /dev/null
+++ b/browser/components/aboutlogins/LoginBreaches.sys.mjs
@@ -0,0 +1,176 @@
+/* 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.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ RemoteSettingsClient:
+ "resource://services-settings/RemoteSettingsClient.sys.mjs",
+});
+
+export 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");
+});