зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f35178a9bc
Коммит
a0ab1f9aba
|
@ -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]
|
||||
|
|
Загрузка…
Ссылка в новой задаче