зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1569847 - Store breached passwords as potentially "vulnerable". r=MattN
MANUAL PUSH: Bug 1561376 - moz-phab needs to allow uploading a patch to a revision without commandeering Differential Revision: https://phabricator.services.mozilla.com//D40889 --HG-- rename : toolkit/components/passwordmgr/LoginHelper.jsm => browser/components/aboutlogins/LoginBreaches.jsm rename : browser/components/aboutlogins/tests/unit/test_getBreachesForLogins.js => browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js extra : rebase_source : c8f9ff4966e7395cc7a5ae90f8c1168a41af2002
This commit is contained in:
Родитель
dd46a7292d
Коммит
91411a62c2
|
@ -519,6 +519,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
HomePage: "resource:///modules/HomePage.jsm",
|
||||
HybridContentTelemetry: "resource://gre/modules/HybridContentTelemetry.jsm",
|
||||
Integration: "resource://gre/modules/Integration.jsm",
|
||||
LoginBreaches: "resource:///modules/LoginBreaches.jsm",
|
||||
LiveBookmarkMigrator: "resource:///modules/LiveBookmarkMigrator.jsm",
|
||||
NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
|
||||
Normandy: "resource://normandy/Normandy.jsm",
|
||||
|
@ -990,6 +991,8 @@ BrowserGlue.prototype = {
|
|||
"migrateMatchBucketsPrefForUI66-done"
|
||||
);
|
||||
});
|
||||
} else if (data == "add-breaches-sync-handler") {
|
||||
this._addBreachesSyncHandler();
|
||||
}
|
||||
break;
|
||||
case "initial-migration-will-import-default-bookmarks":
|
||||
|
@ -2203,6 +2206,7 @@ BrowserGlue.prototype = {
|
|||
|
||||
Services.tm.idleDispatchToMainThread(() => {
|
||||
RemoteSettings.init();
|
||||
this._addBreachesSyncHandler();
|
||||
});
|
||||
|
||||
Services.tm.idleDispatchToMainThread(() => {
|
||||
|
@ -2210,6 +2214,22 @@ BrowserGlue.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
_addBreachesSyncHandler() {
|
||||
if (
|
||||
Services.prefs.getBoolPref(
|
||||
"signon.management.page.breach-alerts.enabled",
|
||||
false
|
||||
)
|
||||
) {
|
||||
RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).on(
|
||||
"sync",
|
||||
async event => {
|
||||
await LoginBreaches.update(event.data.current);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_onQuitRequest: function BG__onQuitRequest(aCancelQuit, aQuitType) {
|
||||
// If user has already dismissed quit request, then do nothing
|
||||
if (aCancelQuit instanceof Ci.nsISupportsPRBool && aCancelQuit.data) {
|
||||
|
|
|
@ -15,20 +15,12 @@ const { RemotePages } = ChromeUtils.import(
|
|||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
fxAccounts: "resource://gre/modules/FxAccounts.jsm",
|
||||
FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js",
|
||||
FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js",
|
||||
LoginBreaches: "resource:///modules/LoginBreaches.jsm",
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
|
@ -213,7 +205,7 @@ var AboutProtectionsHandler = {
|
|||
// password is set.
|
||||
if (!LoginHelper.isMasterPasswordSet()) {
|
||||
const logins = await LoginHelper.getAllUserFacingLogins();
|
||||
potentiallyBreachedLogins = await LoginHelper.getBreachesForLogins(
|
||||
potentiallyBreachedLogins = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
logins
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,37 +9,16 @@ var EXPORTED_SYMBOLS = ["AboutLoginsParent"];
|
|||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"E10SUtils",
|
||||
"resource://gre/modules/E10SUtils.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"MigrationUtils",
|
||||
"resource:///modules/MigrationUtils.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"Services",
|
||||
"resource://gre/modules/Services.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"UIState",
|
||||
"resource://services-sync/UIState.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm"
|
||||
);
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
|
||||
LoginBreaches: "resource:////modules/LoginBreaches.jsm",
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
MigrationUtils: "resource:///modules/MigrationUtils.jsm",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
UIState: "resource://services-sync/UIState.jsm",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
return LoginHelper.createLogger("AboutLoginsParent");
|
||||
|
@ -121,9 +100,9 @@ var AboutLoginsParent = {
|
|||
case "AboutLogins:DismissBreachAlert": {
|
||||
const login = message.data.login;
|
||||
|
||||
await LoginHelper.recordBreachAlertDismissal(login.guid);
|
||||
await LoginBreaches.recordDismissal(login.guid);
|
||||
const logins = await this.getAllLogins();
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
logins
|
||||
);
|
||||
const messageManager = message.target.messageManager;
|
||||
|
@ -428,7 +407,7 @@ var AboutLoginsParent = {
|
|||
);
|
||||
|
||||
if (BREACH_ALERTS_ENABLED) {
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
logins
|
||||
);
|
||||
messageManager.sendAsyncMessage(
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
RemoteSettings: "resource://services-settings/remote-settings.js",
|
||||
RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
|
||||
});
|
||||
|
||||
this.LoginBreaches = {
|
||||
REMOTE_SETTINGS_COLLECTION: "fxmonitor-breaches",
|
||||
|
||||
async recordDismissal(loginGuid) {
|
||||
await Services.logins.initializationPromise;
|
||||
const storageJSON =
|
||||
Services.logins.wrappedJSObject._storage.wrappedJSObject;
|
||||
|
||||
return storageJSON.recordBreachAlertDismissal(loginGuid);
|
||||
},
|
||||
|
||||
async update(breaches = null) {
|
||||
const logins = await 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 RemoteSettings(this.REMOTE_SETTINGS_COLLECTION).get();
|
||||
} catch (ex) {
|
||||
if (ex instanceof RemoteSettingsClient.UnknownCollectionError) {
|
||||
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) {
|
||||
const loginURI = Services.io.newURI(login.origin);
|
||||
let loginHost;
|
||||
try {
|
||||
// nsIURI.host can throw if the URI scheme doesn't have a host.
|
||||
loginHost = loginURI.host;
|
||||
} catch (ex) {
|
||||
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);
|
||||
breach.breachAlertURL = breachAlertURL.href;
|
||||
breachesByLoginGUID.set(login.guid, breach);
|
||||
}
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
_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(this, "log", () => {
|
||||
return LoginHelper.createLogger("LoginBreaches");
|
||||
});
|
|
@ -54,6 +54,7 @@ input[type="url"][readOnly] {
|
|||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
|
|
|
@ -117,6 +117,7 @@ ol {
|
|||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.favicon-wrapper {
|
||||
|
|
|
@ -11,6 +11,7 @@ with Files('**'):
|
|||
|
||||
EXTRA_JS_MODULES += [
|
||||
'AboutLoginsParent.jsm',
|
||||
'LoginBreaches.jsm',
|
||||
]
|
||||
|
||||
FINAL_TARGET_FILES.actors += [
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
let { LoginBreaches } = ChromeUtils.import(
|
||||
"resource:///modules/LoginBreaches.jsm"
|
||||
);
|
||||
|
||||
const TEST_BREACHES = [
|
||||
{
|
||||
AddedDate: "2019-12-20T23:56:26Z",
|
||||
|
@ -31,7 +35,7 @@ add_task(async function setup() {
|
|||
add_task(async function test_show_login() {
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
TEST_LOGIN3.timePasswordChanged = 12345;
|
||||
let testBreaches = await LoginHelper.getBreachesForLogins(
|
||||
let testBreaches = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[TEST_LOGIN3],
|
||||
TEST_BREACHES
|
||||
);
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
/**
|
||||
* Test LoginHelper.getBreachesForLogins
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { AboutLoginsParent } = ChromeUtils.import(
|
||||
"resource:///modules/AboutLoginsParent.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm"
|
||||
);
|
||||
|
||||
const TEST_BREACHES = [
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "breached.com",
|
||||
Name: "Breached",
|
||||
PwnCount: 1643100,
|
||||
DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0043",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "breached-subdomain.host.com",
|
||||
Name: "Only a Sub-Domain was Breached",
|
||||
PwnCount: 2754200,
|
||||
DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0044",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "breached-site-without-passwords.com",
|
||||
Name: "Breached Site without passwords",
|
||||
PwnCount: 987654,
|
||||
DataClasses: ["Email addresses", "Usernames", "IP addresses"],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0045",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
];
|
||||
|
||||
const NOT_BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://www.example.com",
|
||||
formActionOrigin: "https://www.example.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: Date.now(),
|
||||
});
|
||||
const BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://www.breached.com",
|
||||
formActionOrigin: "https://www.breached.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
const NOT_BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://not-breached-subdomain.host.com",
|
||||
formActionOrigin: "https://not-breached-subdomain.host.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
});
|
||||
const BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://breached-subdomain.host.com",
|
||||
formActionOrigin: "https://breached-subdomain.host.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
const LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS = LoginTestUtils.testData.formLogin(
|
||||
{
|
||||
origin: "https://breached-site-without-passwords.com",
|
||||
formActionOrigin: "https://breached-site-without-passwords.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
}
|
||||
);
|
||||
const LOGIN_WITH_NON_STANDARD_URI = LoginTestUtils.testData.formLogin({
|
||||
origin: "someApp://random/path/to/login",
|
||||
formActionOrigin: "someApp://random/path/to/login",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
|
||||
add_task(async function test_getBreachesForLogins_notBreachedLogin() {
|
||||
Services.logins.addLogin(NOT_BREACHED_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached logins."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getBreachesForLogins_breachedLogin() {
|
||||
Services.logins.addLogin(BREACHED_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[NOT_BREACHED_LOGIN, BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login: " + BREACHED_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getBreachesForLogins_notBreachedSubdomain() {
|
||||
Services.logins.addLogin(NOT_BREACHED_SUBDOMAIN_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[NOT_BREACHED_LOGIN, NOT_BREACHED_SUBDOMAIN_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached logins."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getBreachesForLogins_breachedSubdomain() {
|
||||
Services.logins.addLogin(BREACHED_SUBDOMAIN_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[NOT_BREACHED_SUBDOMAIN_LOGIN, BREACHED_SUBDOMAIN_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login: " + BREACHED_SUBDOMAIN_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_getBreachesForLogins_breachedSiteWithoutPasswords() {
|
||||
Services.logins.addLogin(LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached login: " +
|
||||
LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_getBreachesForLogins_breachAlertHiddenAfterDismissal() {
|
||||
BREACHED_LOGIN.guid = "{d2de5ac1-4de6-e544-a7af-1f75abcba92b}";
|
||||
|
||||
await Services.logins.initializationPromise;
|
||||
const storageJSON =
|
||||
Services.logins.wrappedJSObject._storage.wrappedJSObject;
|
||||
|
||||
storageJSON.recordBreachAlertDismissal(BREACHED_LOGIN.guid);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[BREACHED_LOGIN, NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached logins after dismissal: " + BREACHED_LOGIN.origin
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_getBreachesForLogins_newBreachAfterDismissal() {
|
||||
TEST_BREACHES[0].AddedDate = new Date().toISOString();
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[BREACHED_LOGIN, NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login after new breach following the dismissal of a previous breach: " +
|
||||
BREACHED_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_getBreachesForLogins_ExceptionsThrownByNonStandardURIsAreCaught() {
|
||||
Services.logins.addLogin(LOGIN_WITH_NON_STANDARD_URI);
|
||||
|
||||
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
|
||||
[LOGIN_WITH_NON_STANDARD_URI, BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Exceptions thrown by logins with non-standard URIs should be caught."
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,293 @@
|
|||
/**
|
||||
* Test LoginBreaches.getPotentialBreachesByLoginGUID
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { RemoteSettings } = ChromeUtils.import(
|
||||
"resource://services-settings/remote-settings.js"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
|
||||
Ci.nsIObserver
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"LoginBreaches",
|
||||
"resource:///modules/LoginBreaches.jsm"
|
||||
);
|
||||
|
||||
const TEST_BREACHES = [
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "breached.com",
|
||||
Name: "Breached",
|
||||
PwnCount: 1643100,
|
||||
DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0043",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "breached-subdomain.host.com",
|
||||
Name: "Only a Sub-Domain was Breached",
|
||||
PwnCount: 2754200,
|
||||
DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0044",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "breached-site-without-passwords.com",
|
||||
Name: "Breached Site without passwords",
|
||||
PwnCount: 987654,
|
||||
DataClasses: ["Email addresses", "Usernames", "IP addresses"],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0045",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
];
|
||||
|
||||
const NOT_BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://www.example.com",
|
||||
formActionOrigin: "https://www.example.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
const BREACHED_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://www.breached.com",
|
||||
formActionOrigin: "https://www.breached.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
const NOT_BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://not-breached-subdomain.host.com",
|
||||
formActionOrigin: "https://not-breached-subdomain.host.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
});
|
||||
const BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({
|
||||
origin: "https://breached-subdomain.host.com",
|
||||
formActionOrigin: "https://breached-subdomain.host.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
const LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS = LoginTestUtils.testData.formLogin(
|
||||
{
|
||||
origin: "https://breached-site-without-passwords.com",
|
||||
formActionOrigin: "https://breached-site-without-passwords.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
}
|
||||
);
|
||||
const LOGIN_WITH_NON_STANDARD_URI = LoginTestUtils.testData.formLogin({
|
||||
origin: "someApp://random/path/to/login",
|
||||
formActionOrigin: "someApp://random/path/to/login",
|
||||
username: "username",
|
||||
password: "password",
|
||||
timePasswordChanged: new Date("2018-12-15").getTime(),
|
||||
});
|
||||
|
||||
add_task(async function test_notBreachedLogin() {
|
||||
Services.logins.addLogin(NOT_BREACHED_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached logins."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_breachedLogin() {
|
||||
Services.logins.addLogin(BREACHED_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[NOT_BREACHED_LOGIN, BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login: " + BREACHED_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_notBreachedSubdomain() {
|
||||
Services.logins.addLogin(NOT_BREACHED_SUBDOMAIN_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[NOT_BREACHED_LOGIN, NOT_BREACHED_SUBDOMAIN_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached logins."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_breachedSubdomain() {
|
||||
Services.logins.addLogin(BREACHED_SUBDOMAIN_LOGIN);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[NOT_BREACHED_SUBDOMAIN_LOGIN, BREACHED_SUBDOMAIN_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login: " + BREACHED_SUBDOMAIN_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_breachedSiteWithoutPasswords() {
|
||||
Services.logins.addLogin(LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached login: " +
|
||||
LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_breachAlertHiddenAfterDismissal() {
|
||||
BREACHED_LOGIN.guid = "{d2de5ac1-4de6-e544-a7af-1f75abcba92b}";
|
||||
|
||||
await Services.logins.initializationPromise;
|
||||
const storageJSON = Services.logins.wrappedJSObject._storage.wrappedJSObject;
|
||||
|
||||
storageJSON.recordBreachAlertDismissal(BREACHED_LOGIN.guid);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[BREACHED_LOGIN, NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached logins after dismissal: " + BREACHED_LOGIN.origin
|
||||
);
|
||||
|
||||
info("Clear login storage");
|
||||
Services.logins.removeAllLogins();
|
||||
|
||||
const breachesByLoginGUID2 = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[BREACHED_LOGIN, NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID2.size,
|
||||
1,
|
||||
"Breached login should re-appear after clearing storage: " +
|
||||
BREACHED_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_newBreachAfterDismissal() {
|
||||
TEST_BREACHES[0].AddedDate = new Date().toISOString();
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[BREACHED_LOGIN, NOT_BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login after new breach following the dismissal of a previous breach: " +
|
||||
BREACHED_LOGIN.origin
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_ExceptionsThrownByNonStandardURIsAreCaught() {
|
||||
Services.logins.addLogin(LOGIN_WITH_NON_STANDARD_URI);
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[LOGIN_WITH_NON_STANDARD_URI, BREACHED_LOGIN],
|
||||
TEST_BREACHES
|
||||
);
|
||||
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Exceptions thrown by logins with non-standard URIs should be caught."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_updateBreachesFromRemoteSettingsSync() {
|
||||
const login = NOT_BREACHED_SUBDOMAIN_LOGIN;
|
||||
const nowExampleIsInBreachedRecords = [
|
||||
{
|
||||
AddedDate: "2018-12-20T23:56:26Z",
|
||||
BreachDate: "2018-12-16",
|
||||
Domain: "not-breached-subdomain.host.com",
|
||||
Name: "not-breached-subdomain.host.com is now breached!",
|
||||
PwnCount: 1643100,
|
||||
DataClasses: [
|
||||
"Email addresses",
|
||||
"Usernames",
|
||||
"Passwords",
|
||||
"IP addresses",
|
||||
],
|
||||
_status: "synced",
|
||||
id: "047940fe-d2fd-4314-b636-b4a952ee0044",
|
||||
last_modified: "1541615610052",
|
||||
schema: "1541615609018",
|
||||
},
|
||||
];
|
||||
async function emitSync() {
|
||||
await RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).emit(
|
||||
"sync",
|
||||
{ data: { current: nowExampleIsInBreachedRecords } }
|
||||
);
|
||||
}
|
||||
|
||||
const beforeSyncBreachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[login]
|
||||
);
|
||||
Assert.strictEqual(
|
||||
beforeSyncBreachesByLoginGUID.size,
|
||||
0,
|
||||
"Should be 0 breached login before not-breached-subdomain.host.com is added to fxmonitor-breaches collection and synced: "
|
||||
);
|
||||
gBrowserGlue.observe(null, "browser-glue-test", "add-breaches-sync-handler");
|
||||
const collection = await RemoteSettings(
|
||||
LoginBreaches.REMOTE_SETTINGS_COLLECTION
|
||||
).openCollection();
|
||||
await collection.create(nowExampleIsInBreachedRecords[0], {
|
||||
useRecordId: true,
|
||||
});
|
||||
await collection.db.saveLastModified(42);
|
||||
await emitSync();
|
||||
|
||||
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
|
||||
[login]
|
||||
);
|
||||
Assert.strictEqual(
|
||||
breachesByLoginGUID.size,
|
||||
1,
|
||||
"Should be 1 breached login after not-breached-subdomain.host.com is added to fxmonitor-breaches collection and synced: "
|
||||
);
|
||||
});
|
|
@ -2,4 +2,4 @@
|
|||
head = head.js
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_getBreachesForLogins.js]
|
||||
[test_getPotentialBreachesByLoginGUID.js]
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["LoginHelper"];
|
||||
const REMOTE_SETTINGS_BREACHES_COLLECTION = "fxmonitor-breaches";
|
||||
|
||||
// Globals
|
||||
|
||||
|
@ -22,18 +21,6 @@ const { XPCOMUtils } = ChromeUtils.import(
|
|||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"RemoteSettings",
|
||||
"resource://services-settings/remote-settings.js"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"RemoteSettingsClient",
|
||||
"resource://services-settings/RemoteSettingsClient.jsm"
|
||||
);
|
||||
|
||||
/**
|
||||
* Contains functions shared by different Login Manager components.
|
||||
*/
|
||||
|
@ -1112,79 +1099,6 @@ this.LoginHelper = {
|
|||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async recordBreachAlertDismissal(loginGuid) {
|
||||
await Services.logins.initializationPromise;
|
||||
const storageJSON =
|
||||
Services.logins.wrappedJSObject._storage.wrappedJSObject;
|
||||
|
||||
return storageJSON.recordBreachAlertDismissal(loginGuid);
|
||||
},
|
||||
|
||||
async getBreachesForLogins(logins, breaches = null) {
|
||||
const breachesByLoginGUID = new Map();
|
||||
if (!breaches) {
|
||||
try {
|
||||
breaches = await RemoteSettings(
|
||||
REMOTE_SETTINGS_BREACHES_COLLECTION
|
||||
).get();
|
||||
} catch (ex) {
|
||||
if (ex instanceof RemoteSettingsClient.UnknownCollectionError) {
|
||||
log.warn(
|
||||
"Could not get Remote Settings collection.",
|
||||
REMOTE_SETTINGS_BREACHES_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) {
|
||||
const loginURI = Services.io.newURI(login.origin);
|
||||
let loginHost;
|
||||
try {
|
||||
// nsIURI.host can throw if the URI scheme doesn't have a host.
|
||||
loginHost = loginURI.host;
|
||||
} catch (ex) {
|
||||
continue;
|
||||
}
|
||||
for (const breach of breaches) {
|
||||
if (!breach.Domain) {
|
||||
continue;
|
||||
}
|
||||
const breachDate = new Date(breach.BreachDate).getTime();
|
||||
const breachAddedDate = new Date(breach.AddedDate).getTime();
|
||||
if (
|
||||
Services.eTLD.hasRootDomain(loginHost, breach.Domain) &&
|
||||
breach.hasOwnProperty("DataClasses") &&
|
||||
breach.DataClasses.includes("Passwords") &&
|
||||
login.timePasswordChanged < breachDate &&
|
||||
(!dismissedBreachAlertsByLoginGUID[login.guid] ||
|
||||
dismissedBreachAlertsByLoginGUID[login.guid]
|
||||
.timeBreachAlertDismissed < breachAddedDate)
|
||||
) {
|
||||
let breachAlertURL = new URL(breach.Name, baseBreachAlertURL);
|
||||
breach.breachAlertURL = breachAlertURL.href;
|
||||
breachesByLoginGUID.set(login.guid, breach);
|
||||
}
|
||||
}
|
||||
}
|
||||
return breachesByLoginGUID;
|
||||
},
|
||||
};
|
||||
|
||||
LoginHelper.init();
|
||||
|
@ -1196,6 +1110,5 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let logger = LoginHelper.createLogger("LoginHelper");
|
||||
return logger;
|
||||
return LoginHelper.createLogger("LoginHelper");
|
||||
});
|
||||
|
|
|
@ -98,6 +98,10 @@ LoginStore.prototype._dataPostProcessor = function(data) {
|
|||
data.logins = [];
|
||||
}
|
||||
|
||||
if (!data.potentiallyVulnerablePasswords) {
|
||||
data.potentiallyVulnerablePasswords = [];
|
||||
}
|
||||
|
||||
if (!data.dismissedBreachAlertsByLoginGUID) {
|
||||
data.dismissedBreachAlertsByLoginGUID = {};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* 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/. */
|
||||
|
||||
/*
|
||||
/**
|
||||
* nsILoginManagerStorage implementation for the JSON back-end.
|
||||
*/
|
||||
|
||||
|
@ -57,6 +57,24 @@ this.LoginManagerStorage_json.prototype = {
|
|||
return this.__crypto;
|
||||
},
|
||||
|
||||
__decryptedPotentiallyVulnerablePasswords: null,
|
||||
get _decryptedPotentiallyVulnerablePasswords() {
|
||||
if (!this.__decryptedPotentiallyVulnerablePasswords) {
|
||||
this._store.ensureDataReady();
|
||||
this.__decryptedPotentiallyVulnerablePasswords = [];
|
||||
for (const potentiallyVulnerablePassword of this._store.data
|
||||
.potentiallyVulnerablePasswords) {
|
||||
const decryptedPotentiallyVulnerablePassword = this._crypto.decrypt(
|
||||
potentiallyVulnerablePassword.encryptedPassword
|
||||
);
|
||||
this.__decryptedPotentiallyVulnerablePasswords.push(
|
||||
decryptedPotentiallyVulnerablePassword
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.__decryptedPotentiallyVulnerablePasswords;
|
||||
},
|
||||
|
||||
initialize() {
|
||||
try {
|
||||
// Force initialization of the crypto module.
|
||||
|
@ -553,6 +571,9 @@ this.LoginManagerStorage_json.prototype = {
|
|||
|
||||
this.log("Removing all logins");
|
||||
this._store.data.logins = [];
|
||||
this._store.data.potentiallyVulnerablePasswords = [];
|
||||
this.__decryptedPotentiallyVulnerablePasswords = null;
|
||||
this._store.data.dismissedBreachAlertsByLoginGUID = {};
|
||||
this._store.saveSoon();
|
||||
|
||||
LoginHelper.notifyStorageChanged("removeAllLogins", null);
|
||||
|
@ -597,6 +618,26 @@ this.LoginManagerStorage_json.prototype = {
|
|||
return logins.length;
|
||||
},
|
||||
|
||||
addPotentiallyVulnerablePassword(login) {
|
||||
this._store.ensureDataReady();
|
||||
// this breached password is already stored
|
||||
if (this.isPotentiallyVulnerablePassword(login)) {
|
||||
return;
|
||||
}
|
||||
this.__decryptedPotentiallyVulnerablePasswords.push(login.password);
|
||||
|
||||
this._store.data.potentiallyVulnerablePasswords.push({
|
||||
encryptedPassword: this._crypto.encrypt(login.password),
|
||||
});
|
||||
this._store.saveSoon();
|
||||
},
|
||||
|
||||
isPotentiallyVulnerablePassword(login) {
|
||||
return this._decryptedPotentiallyVulnerablePasswords.includes(
|
||||
login.password
|
||||
);
|
||||
},
|
||||
|
||||
get uiBusy() {
|
||||
return this._crypto.uiBusy;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function setup() {
|
||||
await Services.logins.initializationPromise;
|
||||
});
|
||||
|
||||
add_task(async function test_vulnerable_password_methods() {
|
||||
const storageJSON = Services.logins.wrappedJSObject._storage.wrappedJSObject;
|
||||
|
||||
let logins = TestData.loginList();
|
||||
Assert.greater(logins.length, 0, "Initial logins length should be > 0.");
|
||||
|
||||
for (let loginInfo of logins) {
|
||||
Services.logins.addLogin(loginInfo);
|
||||
Assert.ok(
|
||||
!storageJSON.isPotentiallyVulnerablePassword(loginInfo),
|
||||
"No logins should be vulnerable until addVulnerablePasswords is called."
|
||||
);
|
||||
}
|
||||
|
||||
const vulnerableLogin = logins.shift();
|
||||
storageJSON.addPotentiallyVulnerablePassword(vulnerableLogin);
|
||||
|
||||
Assert.ok(
|
||||
storageJSON.isPotentiallyVulnerablePassword(vulnerableLogin),
|
||||
"Login should be vulnerable after calling addVulnerablePassword."
|
||||
);
|
||||
for (let loginInfo of logins) {
|
||||
Assert.ok(
|
||||
!storageJSON.isPotentiallyVulnerablePassword(loginInfo),
|
||||
"No other logins should be vulnerable when addVulnerablePassword is called" +
|
||||
" with a single argument"
|
||||
);
|
||||
}
|
||||
});
|
|
@ -57,3 +57,5 @@ skip-if = os == "android" # Not packaged/used on Android
|
|||
[test_shadowHTTPLogins.js]
|
||||
[test_storage.js]
|
||||
[test_telemetry.js]
|
||||
[test_vulnerable_passwords.js]
|
||||
skip-if = os == "android" # Not implemented for storage-mozStorage
|
||||
|
|
Загрузка…
Ссылка в новой задаче