Merge inbound to mozilla-central. a=merge

This commit is contained in:
Bogdan Tara 2019-08-28 12:41:46 +03:00
Родитель 786d0c66bc 41f1c050cb
Коммит 8ea946dcf5
17 изменённых файлов: 597 добавлений и 367 удалений

Просмотреть файл

@ -507,6 +507,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",
@ -979,6 +980,8 @@ BrowserGlue.prototype = {
"migrateMatchBucketsPrefForUI66-done"
);
});
} else if (data == "add-breaches-sync-handler") {
this._addBreachesSyncHandler();
}
break;
case "initial-migration-will-import-default-bookmarks":
@ -2192,6 +2195,7 @@ BrowserGlue.prototype = {
Services.tm.idleDispatchToMainThread(() => {
RemoteSettings.init();
this._addBreachesSyncHandler();
});
Services.tm.idleDispatchToMainThread(() => {
@ -2203,6 +2207,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");
@ -132,9 +111,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;
@ -441,7 +420,7 @@ var AboutLoginsParent = {
);
if (BREACH_ALERTS_ENABLED) {
const breachesByLoginGUID = await LoginHelper.getBreachesForLogins(
const breachesByLoginGUID = await LoginBreaches.getPotentialBreachesByLoginGUID(
logins
);
messageManager.sendAsyncMessage(
@ -559,7 +538,7 @@ var AboutLoginsParent = {
async getFavicon(login) {
try {
const faviconData = await PlacesUtils.promiseFaviconData(login.hostname);
const faviconData = await PlacesUtils.promiseFaviconData(login.origin);
return {
faviconData,
guid: login.guid,

Просмотреть файл

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

Просмотреть файл

@ -60,6 +60,7 @@ input[type="url"][readOnly] {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.delete-button,

Просмотреть файл

@ -120,6 +120,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]

Просмотреть файл

@ -1,3 +0,0 @@
[fetch-sw.https.html]
expected:
if (os == "android") and debug: CRASH

Просмотреть файл

@ -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