Bug 1134852 - Update password manager recipes from Remote Settings. r=sfoster,leplatrem,geckoview-reviewers,esawin

Differential Revision: https://phabricator.services.mozilla.com/D89821
This commit is contained in:
Tim Giles 2020-10-02 19:45:03 +00:00
Родитель f35178a9bc
Коммит a0ab1f9aba
10 изменённых файлов: 234 добавлений и 95 удалений

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

@ -88,6 +88,7 @@
@BINPATH@/defaults/settings/blocklists/addons.json
; TODO bug 1639050: addons-bloomfilters should be used instead of addons.json
@BINPATH@/defaults/settings/security-state/onecrl.json
@BINPATH@/defaults/settings/main/password-recipes.json
; [Components]
@BINPATH@/components/components.manifest

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

@ -3750,7 +3750,9 @@ pref("signon.privateBrowsingCapture.enabled", true);
pref("signon.storeWhenAutocompleteOff", true);
pref("signon.userInputRequiredToCapture.enabled", true);
pref("signon.debug", false);
pref("signon.recipes.path", "chrome://passwordmgr/content/recipes.json");
pref("signon.recipes.path", "resource://app/defaults/settings/main/password-recipes.json");
pref("signon.recipes.remoteRecipesEnabled", true);
pref("signon.schemeUpgrades", true);
pref("signon.includeOtherSubdomainsInLookup", true);
// This temporarily prevents the master password to reprompt for autocomplete.

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

@ -7,6 +7,7 @@ FINAL_TARGET_FILES.defaults.settings.main += [
'example.json',
'hijack-blocklists.json',
'language-dictionaries.json',
'password-recipes.json',
'search-config.json',
'search-default-override-allowlist.json',
'sites-classification.json',

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

@ -0,0 +1 @@
{"data":[{"hosts":["www.dmv.ca.gov"],"schema":1600889016495,"description":"www.dmv.ca.gov: Checking account form field has type=password fields and gets login suggestions (Bug 1595339)","notPasswordSelector":"#bankRoutingNo, #validateBankRoutingNo, #bankAcctNo, #validateBankAcctNo","id":"85113859-1767-4fe4-bec3-826028b7cc91","last_modified":1600889167888},{"hosts":["mozilla.okta.com"],"schema":1599766903292,"description":"okta uses a hidden password field to disable filling","passwordSelector":"#pass-signin","id":"99bed888-5309-468f-9924-2a57bd5289ee","last_modified":1600889016443},{"hosts":["www.anthem.com"],"schema":1599839796360,"description":"anthem uses a hidden password and username field to disable filling","passwordSelector":"#LoginContent_txtLoginPass","id":"ef7f2f3f-04b9-49d0-a2b5-3e3766b41d19","last_modified":1600889016440},{"hosts":["www.discover.com"],"schema":1599839796979,"description":"An ephemeral password-shim field is incorrectly selected as the username field.","usernameSelector":"#login-account","id":"dc49fe14-1969-48ab-b98b-2055480e55a4","last_modified":1600889016436},{"hosts":["secure.tibia.com"],"schema":1599839797632,"pathRegex":"^\\/account\\/","description":"Tibia uses type=password for its username field and puts the email address before the password field during registration","passwordSelector":"#password1, input[name='loginpassword']","usernameSelector":"#accountname, input[name='loginname']","id":"8fdbd3ae-4f74-4ff6-9399-b1c4cb2a6421","last_modified":1600889016433},{"hosts":["www.facebook.com"],"schema":1599839798260,"description":"Username field will be incorrectly captured in the change password form (bug 1243722)","notUsernameSelector":"#password_strength","id":"1f76dae4-c8ad-4633-b9fa-3a2420cf7e5c","last_modified":1600889016430},{"hosts":["www.united.com"],"schema":1599839798880,"pathRegex":"^\\/travel\\/checkin\\/changefqtv.aspx","description":"United uses a useless password field plus one per frequent flyer number during checkin. Don't save any of them (Bug 1330810)","notPasswordSelector":"input[type='password']","id":"ed57feb9-a928-4f61-8947-c676bd95a7b0","last_modified":1600889016426},{"hosts":["buy.gogoinflight.com"],"schema":1599839799488,"description":"Gogo In-Flight uses a password field for credit card numbers on the same page as login","notPasswordSelector":"#cardNumber","id":"2de99a9f-9430-411a-94d0-dbb31910742e","last_modified":1600889016423},{"hosts":["mabanque.fortuneo.fr"],"schema":1599839800167,"pathRegex":"\\\\/identification\\\\.jsp$","description":"The Fortuneo bank uses a different form for each input, defeating the password manager (Bug 1433754)","usernameSelector":"input[name='LOGIN']","id":"aebc1ea5-ca41-4258-9594-9255bbcadf1e","last_modified":1600889016419},{"hosts":["www.benefits.ml.com"],"schema":1599839800780,"pathRegex":"^\\/login\\/login$","description":"Merrill's benefits website has six type=password fields which is over our threshold for filling (Bug 1538026)","passwordSelector":"#SFText_Password","usernameSelector":"#SFText_UserName","id":"00814138-7ede-4f56-8953-b6d1c99d5f26","last_modified":1600889016416}]}

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

@ -38,6 +38,8 @@ this.LoginHelper = {
includeOtherSubdomainsInLookup: null,
insecureAutofill: null,
privateBrowsingCaptureEnabled: null,
remoteRecipesEnabled: null,
remoteRecipesCollection: "password-recipes",
schemeUpgrades: null,
showAutoCompleteFooter: null,
showAutoCompleteImport: null,
@ -152,6 +154,9 @@ this.LoginHelper = {
this.userInputRequiredToCapture = Services.prefs.getBoolPref(
"signon.userInputRequiredToCapture.enabled"
);
this.remoteRecipesEnabled = Services.prefs.getBoolPref(
"signon.recipes.remoteRecipesEnabled"
);
},
createLogger(aLogPrefix) {

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

@ -14,6 +14,9 @@ const OPTIONAL_KEYS = [
"passwordSelector",
"pathRegex",
"usernameSelector",
"schema",
"id",
"last_modified",
];
const SUPPORTED_KEYS = REQUIRED_KEYS.concat(OPTIONAL_KEYS);
@ -23,7 +26,7 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "fetch"]);
ChromeUtils.defineModuleGetter(
this,
@ -35,6 +38,10 @@ XPCOMUtils.defineLazyGetter(this, "log", () =>
LoginHelper.createLogger("LoginRecipes")
);
XPCOMUtils.defineLazyModuleGetters(this, {
RemoteSettings: "resource://services-settings/remote-settings.js",
});
/**
* Create an instance of the object to manage recipes in the parent process.
* Consumers should wait until {@link initializationPromise} resolves before
@ -74,6 +81,12 @@ LoginRecipesParent.prototype = {
*/
_recipesByHost: null,
/**
* @type {Object} Instance of Remote Settings client that has access to the
* "password-recipes" collection
*/
_rsClient: null,
/**
* @param {Object} aRecipes an object containing recipes to load for use. The object
* should be compatible with JSON (e.g. no RegExp).
@ -92,11 +105,9 @@ LoginRecipesParent.prototype = {
log.error("Error loading recipe", rawRecipe, ex);
}
}
if (recipeErrors) {
return Promise.reject(`There were ${recipeErrors} recipe error(s)`);
}
return Promise.resolve();
},
@ -106,37 +117,34 @@ LoginRecipesParent.prototype = {
reset() {
log.debug("Resetting recipes with defaults:", this._defaults);
this._recipesByHost = new Map();
if (this._defaults) {
let channel = NetUtil.newChannel({
uri: NetUtil.newURI(this._defaults, "UTF-8"),
loadUsingSystemPrincipal: true,
});
channel.contentType = "application/json";
try {
this.initializationPromise = new Promise(function(resolve) {
NetUtil.asyncFetch(channel, function(stream, result) {
if (!Components.isSuccessCode(result)) {
throw new Error("Error fetching recipe file:" + result);
let initPromise;
/**
* Both branches rely on a JSON dump of the Remote Settings collection, packaged both in Desktop and Android.
* The «legacy» mode will read the dump directly from the packaged resources.
* With Remote Settings, the dump is used to initialize the local database without network,
* and the list of password recipes can be refreshed without restarting and without software update.
*/
if (LoginHelper.remoteRecipesEnabled) {
if (!this._rsClient) {
this._rsClient = RemoteSettings(LoginHelper.remoteRecipesCollection);
// Set up sync observer to update local recipes from Remote Settings recipes
this._rsClient.on("sync", event => this.onRemoteSettingsSync(event));
}
let count = stream.available();
let data = NetUtil.readInputStreamToString(stream, count, {
charset: "UTF-8",
});
resolve(JSON.parse(data));
});
})
.then(recipes => {
initPromise = this._rsClient.get();
} else if (this._defaults.startsWith("resource://")) {
initPromise = fetch(this._defaults)
.then(resp => resp.json())
.then(({ data }) => data);
} else {
log.error("Invalid recipe path found, setting empty recipes list!");
initPromise = new Promise(() => []);
}
this.initializationPromise = initPromise.then(async siteRecipes => {
Services.ppmm.broadcastAsyncMessage("clearRecipeCache");
return this.load(recipes);
})
.then(resolve => {
await this.load({ siteRecipes });
return this;
});
} catch (e) {
throw new Error("Error reading recipe file:" + e);
}
} else {
this.initializationPromise = Promise.resolve(this);
}
@ -213,6 +221,27 @@ LoginRecipesParent.prototype = {
return hostRecipes;
},
/**
* Handles the Remote Settings sync event for the "password-recipes" collection.
*
* @param {Object} aEvent
* @param {Array} event.current Records in the "password-recipes" collection after the sync event
* @param {Array} event.created Records that were created with this particular sync
* @param {Array} event.updated Records that were updated with this particular sync
* @param {Array} event.deleted Records that were deleted with this particular sync
*/
onRemoteSettingsSync(aEvent) {
this._recipesByHost = new Map();
let {
data: { current },
} = aEvent;
let recipes = {
siteRecipes: current,
};
Services.ppmm.broadcastAsyncMessage("clearRecipeCache");
this.load(recipes);
},
};
this.LoginRecipesContent = {

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

@ -1,60 +0,0 @@
{
"siteRecipes": [
{
"description": "okta uses a hidden password field to disable filling",
"hosts": ["mozilla.okta.com"],
"passwordSelector": "#pass-signin"
},
{
"description": "anthem uses a hidden password and username field to disable filling",
"hosts": ["www.anthem.com"],
"passwordSelector": "#LoginContent_txtLoginPass"
},
{
"description": "An ephemeral password-shim field is incorrectly selected as the username field.",
"hosts": ["www.discover.com"],
"usernameSelector": "#login-account"
},
{
"description": "Tibia uses type=password for its username field and puts the email address before the password field during registration",
"hosts": ["secure.tibia.com"],
"usernameSelector": "#accountname, input[name='loginname']",
"passwordSelector": "#password1, input[name='loginpassword']",
"pathRegex": "^\/account\/"
},
{
"description": "Username field will be incorrectly captured in the change password form (bug 1243722)",
"hosts": ["www.facebook.com"],
"notUsernameSelector": "#password_strength"
},
{
"description": "United uses a useless password field plus one per frequent flyer number during checkin. Don't save any of them (Bug 1330810)",
"hosts": ["www.united.com"],
"notPasswordSelector": "input[type='password']",
"pathRegex": "^\/travel\/checkin\/changefqtv.aspx"
},
{
"description": "Gogo In-Flight uses a password field for credit card numbers on the same page as login",
"hosts": ["buy.gogoinflight.com"],
"notPasswordSelector": "#cardNumber"
},
{
"description": "The Fortuneo bank uses a different form for each input, defeating the password manager (Bug 1433754)",
"hosts": ["mabanque.fortuneo.fr"],
"pathRegex": "\\/identification\\.jsp$",
"usernameSelector": "input[name='LOGIN']"
},
{
"description": "Merrill's benefits website has six type=password fields which is over our threshold for filling (Bug 1538026)",
"hosts": ["www.benefits.ml.com"],
"usernameSelector": "#SFText_UserName",
"passwordSelector": "#SFText_Password",
"pathRegex": "^\/login\/login$"
},
{
"description": "Bug 1595339: Checking account type=password fields get login suggestions",
"hosts": ["www.dmv.ca.gov"],
"notPasswordSelector": "#bankRoutingNo, #validateBankRoutingNo, #bankAcctNo, #validateBankAcctNo"
}
]
}

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

@ -4,4 +4,3 @@
toolkit.jar:
% content passwordmgr %content/passwordmgr/
content/passwordmgr/recipes.json (content/recipes.json)

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

@ -0,0 +1,159 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests retrieving remote LoginRecipes in the parent process.
*/
"use strict";
const { RemoteSettings } = ChromeUtils.import(
"resource://services-settings/remote-settings.js",
{}
);
const REMOTE_SETTINGS_COLLECTION = "password-recipes";
add_task(async function test_init_remote_recipe() {
const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db;
const record1 = {
id: "some-fake-ID",
hosts: ["www.testDomain.com"],
description: "Some description here",
usernameSelector: "#username",
};
await db.create(record1);
let parent = new LoginRecipesParent({ defaults: true });
let recipesParent = await parent.initializationPromise;
Assert.ok(
recipesParent instanceof LoginRecipesParent,
"Check initialization promise value which should be an instance of LoginRecipesParent"
);
Assert.strictEqual(
recipesParent._recipesByHost.size,
1,
"Initially 1 recipe based on our test record"
);
let rsClient = recipesParent._rsClient;
recipesParent.reset();
await recipesParent.initializationPromise;
Assert.ok(
recipesParent instanceof LoginRecipesParent,
"Ensure that the instance of LoginRecipesParent has not changed after resetting"
);
Assert.strictEqual(
rsClient,
recipesParent._rsClient,
"Resetting recipes should not modify the rs client"
);
Assert.strictEqual(
recipesParent._recipesByHost.size,
1,
"Initially 1 recipe based on our test record"
);
await db.clear();
});
add_task(async function test_add_recipe_sync() {
const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db;
const record1 = {
id: "some-fake-ID",
hosts: ["www.testDomain.com"],
description: "Some description here",
usernameSelector: "#username",
};
await db.create(record1);
let parent = new LoginRecipesParent({ defaults: true });
let recipesParent = await parent.initializationPromise;
const record2 = {
id: "some-fake-ID-2",
hosts: ["www.testDomain2.com"],
description: "Some description here. Wow it changed!",
usernameSelector: "#username",
};
const payload = {
current: [record1, record2],
created: [record2],
updated: [],
deleted: [],
};
await RemoteSettings(REMOTE_SETTINGS_COLLECTION).emit("sync", {
data: payload,
});
Assert.strictEqual(
recipesParent._recipesByHost.size,
2,
"New recipe from sync event added successfully"
);
await db.clear();
});
add_task(async function test_remove_recipe_sync() {
const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db;
const record1 = {
id: "some-fake-ID",
hosts: ["www.testDomain.com"],
description: "Some description here",
usernameSelector: "#username",
};
await db.create(record1);
let parent = new LoginRecipesParent({ defaults: true });
let recipesParent = await parent.initializationPromise;
const deletePayload = {
current: [],
created: [],
updated: [],
deleted: [record1],
};
await RemoteSettings(REMOTE_SETTINGS_COLLECTION).emit("sync", {
data: deletePayload,
});
Assert.strictEqual(
recipesParent._recipesByHost.size,
0,
"Recipes successfully deleted on sync event"
);
await db.clear();
});
add_task(async function test_malformed_recipes_in_db() {
const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db;
const malformedRecord = {
id: "some-ID",
hosts: ["www.testDomain.com"],
description: "Some description here",
usernameSelector: "#username",
fieldThatDoesNotExist: "value",
};
await db.create(malformedRecord);
let parent = new LoginRecipesParent({ defaults: true });
try {
await parent.initializationPromise;
} catch (e) {
Assert.ok(
e == "There were 1 recipe error(s)",
"It should throw an error because of field that does not match the schema"
);
}
await db.clear();
const missingHostsRecord = {
id: "some-ID",
description: "Some description here",
usernameSelector: "#username",
};
await db.create(missingHostsRecord);
parent = new LoginRecipesParent({ defaults: true });
try {
await parent.initializationPromise;
} catch (e) {
Assert.ok(
e == "There were 1 recipe error(s)",
"It should throw an error because of missing hosts field"
);
}
});

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

@ -1,6 +1,6 @@
[DEFAULT]
head = head.js
skip-if = os == "android" # Not supported on GV because we can't add/remove from storage.
skip-if = os == "android" || toolkit == "android" # Not supported on GV because we can't add/remove from storage.
support-files = data/**
# Test logins.json file access, not applicable to Android.
@ -56,6 +56,8 @@ skip-if = os != "win"
skip-if = os == "android" # Not packaged/used on Android
[test_recipes_add.js]
[test_recipes_content.js]
[test_remote_recipes.js]
skip-if = os == "android"
[test_search_schemeUpgrades.js]
[test_shadowHTTPLogins.js]
[test_storage.js]