tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

LoginBreaches.sys.mjs (6746B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 /**
      8 * Manages breach alerts for saved logins using data from Firefox Monitor via
      9 * RemoteSettings.
     10 */
     11 
     12 const lazy = {};
     13 
     14 ChromeUtils.defineESModuleGetters(lazy, {
     15  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     16  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
     17  RemoteSettingsClient:
     18    "resource://services-settings/RemoteSettingsClient.sys.mjs",
     19 });
     20 
     21 XPCOMUtils.defineLazyPreferenceGetter(
     22  lazy,
     23  "VULNERABLE_PASSWORDS_ENABLED",
     24  "signon.management.page.vulnerable-passwords.enabled",
     25  false
     26 );
     27 
     28 export const LoginBreaches = {
     29  REMOTE_SETTINGS_COLLECTION: "fxmonitor-breaches",
     30 
     31  async update(breaches = null) {
     32    const logins = await lazy.LoginHelper.getAllUserFacingLogins();
     33    await this.getPotentialBreachesByLoginGUID(logins, breaches);
     34  },
     35 
     36  /**
     37   * Return a Map of login GUIDs to a potential breach affecting that login
     38   * by considering only breaches affecting passwords.
     39   *
     40   * This only uses the breach `Domain` and `timePasswordChanged` to determine
     41   * if a login may be breached which means it may contain false-positives if
     42   * login timestamps are incorrect, the user didn't save their password change
     43   * in Firefox, or the breach didn't contain all accounts, etc. As a result,
     44   * consumers should avoid making stronger claims than the data supports.
     45   *
     46   * @param {nsILoginInfo[]} logins Saved logins to check for potential breaches.
     47   * @param {object[]} [breaches = null] Only ones involving passwords will be used.
     48   * @returns {Map} with a key for each login GUID potentially in a breach.
     49   */
     50  async getPotentialBreachesByLoginGUID(logins, breaches = null) {
     51    const breachesByLoginGUID = new Map();
     52    if (!breaches) {
     53      try {
     54        breaches = await lazy
     55          .RemoteSettings(this.REMOTE_SETTINGS_COLLECTION)
     56          .get();
     57      } catch (ex) {
     58        if (ex instanceof lazy.RemoteSettingsClient.UnknownCollectionError) {
     59          lazy.log.warn(
     60            "Could not get Remote Settings collection.",
     61            this.REMOTE_SETTINGS_COLLECTION,
     62            ex
     63          );
     64          return breachesByLoginGUID;
     65        }
     66        throw ex;
     67      }
     68    }
     69    const BREACH_ALERT_URL = Services.prefs.getStringPref(
     70      "signon.management.page.breachAlertUrl"
     71    );
     72    const baseBreachAlertURL = new URL(BREACH_ALERT_URL);
     73 
     74    await Services.logins.initializationPromise;
     75    const storageJSON = Services.logins.wrappedJSObject._storage;
     76    const dismissedBreachAlertsByLoginGUID =
     77      storageJSON.getBreachAlertDismissalsByLoginGUID();
     78 
     79    // Determine potentially breached logins by checking their origin and the last time
     80    // they were changed. It's important to note here that we are NOT considering the
     81    // username and password of that login.
     82    for (const login of logins) {
     83      let loginHost;
     84      try {
     85        // nsIURI.host can throw if the URI scheme doesn't have a host.
     86        loginHost = Services.io.newURI(login.origin).host;
     87      } catch {
     88        continue;
     89      }
     90      for (const breach of breaches) {
     91        if (
     92          !breach.Domain ||
     93          !Services.eTLD.hasRootDomain(loginHost, breach.Domain) ||
     94          !this._breachInvolvedPasswords(breach) ||
     95          !this._breachWasAfterPasswordLastChanged(breach, login)
     96        ) {
     97          continue;
     98        }
     99 
    100        if (!storageJSON.isPotentiallyVulnerablePassword(login)) {
    101          storageJSON.addPotentiallyVulnerablePassword(login);
    102        }
    103 
    104        if (
    105          this._breachAlertIsDismissed(
    106            login,
    107            breach,
    108            dismissedBreachAlertsByLoginGUID
    109          )
    110        ) {
    111          continue;
    112        }
    113 
    114        let breachAlertURL = new URL(breach.Name, baseBreachAlertURL);
    115        breachAlertURL.searchParams.set("utm_source", "firefox-desktop");
    116        breachAlertURL.searchParams.set("utm_medium", "referral");
    117        breachAlertURL.searchParams.set("utm_campaign", "about-logins");
    118        breachAlertURL.searchParams.set("utm_content", "about-logins");
    119        breach.breachAlertURL = breachAlertURL.href;
    120        breachesByLoginGUID.set(login.guid, breach);
    121      }
    122    }
    123    Glean.pwmgr.potentiallyBreachedPasswords.set(breachesByLoginGUID.size);
    124    return breachesByLoginGUID;
    125  },
    126 
    127  /**
    128   * Return information about logins using passwords that were potentially in a
    129   * breach.
    130   *
    131   * @see the caveats in the documentation for `getPotentialBreachesByLoginGUID`.
    132   *
    133   * @param {nsILoginInfo[]} logins to check the passwords of.
    134   * @returns {Map} from login GUID to `true` for logins that have a password
    135   *                that may be vulnerable.
    136   */
    137  getPotentiallyVulnerablePasswordsByLoginGUID(logins) {
    138    const vulnerablePasswordsByLoginGUID = new Map();
    139    const storageJSON = Services.logins.wrappedJSObject._storage;
    140    for (const login of logins) {
    141      if (storageJSON.isPotentiallyVulnerablePassword(login)) {
    142        vulnerablePasswordsByLoginGUID.set(login.guid, true);
    143      }
    144    }
    145    return vulnerablePasswordsByLoginGUID;
    146  },
    147 
    148  recordBreachAlertDismissal(loginGuid) {
    149    const storageJSON = Services.logins.wrappedJSObject._storage;
    150    return storageJSON.recordBreachAlertDismissal(loginGuid);
    151  },
    152 
    153  isVulnerablePassword(login) {
    154    if (!lazy.VULNERABLE_PASSWORDS_ENABLED) {
    155      return false;
    156    }
    157 
    158    const storageJSON = Services.logins.wrappedJSObject._storage;
    159    return storageJSON.isPotentiallyVulnerablePassword(login);
    160  },
    161 
    162  async clearAllPotentiallyVulnerablePasswords() {
    163    await Services.logins.initializationPromise;
    164    const storageJSON = Services.logins.wrappedJSObject._storage;
    165    storageJSON.clearAllPotentiallyVulnerablePasswords();
    166  },
    167 
    168  _breachAlertIsDismissed(login, breach, dismissedBreachAlerts) {
    169    const breachAddedDate = new Date(breach.AddedDate).getTime();
    170    const breachAlertIsDismissed =
    171      dismissedBreachAlerts[login.guid] &&
    172      dismissedBreachAlerts[login.guid].timeBreachAlertDismissed >
    173        breachAddedDate;
    174    return breachAlertIsDismissed;
    175  },
    176 
    177  _breachInvolvedPasswords(breach) {
    178    return (
    179      breach.hasOwnProperty("DataClasses") &&
    180      breach.DataClasses.includes("Passwords")
    181    );
    182  },
    183 
    184  _breachWasAfterPasswordLastChanged(breach, login) {
    185    const breachDate = new Date(breach.BreachDate).getTime();
    186    return login.timePasswordChanged < breachDate;
    187  },
    188 };
    189 
    190 ChromeUtils.defineLazyGetter(lazy, "log", () => {
    191  return lazy.LoginHelper.createLogger("LoginBreaches");
    192 });