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 });