Bug 1752839, improve logins sync by not using the LegacyTracker, r=sgalich,skhamis,sync-reviewers,dimi,joschmidt,markh

Instead of recording a separate list of changes that could be incorrect, determine the list of modified logins when the sync occurs. Two flags are added to login info objects, the syncStatus that identifies that a login has been synced and a counter which is incremented during a sync and decremented by the same amount when the sync is complete. Deleted logins are flagged with a deleted marker rather than deleted directly.

This behaviour mirrors the form auto fill sync method.

Differential Revision: https://phabricator.services.mozilla.com/D170817
This commit is contained in:
Neil Deakin 2023-06-13 13:00:42 +00:00
parent 23e85a5101
commit bbc4d2f51e
8 changed files with 375 additions and 251 deletions

View File

@ -2,17 +2,15 @@
* 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/. */
import {
Collection,
CryptoWrapper,
} from "resource://services-sync/record.sys.mjs";
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
import { CollectionValidator } from "resource://services-sync/collection_validator.sys.mjs";
import {
Changeset,
Store,
SyncEngine,
LegacyTracker,
Tracker,
} from "resource://services-sync/engines.sys.mjs";
import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
@ -37,38 +35,17 @@ const VALID_LOGIN_FIELDS = [
"timesUsed",
"username",
"usernameField",
"everSynced",
"syncCounter",
"unknownFields",
];
const SYNCABLE_LOGIN_FIELDS = [
// `nsILoginInfo` fields.
"hostname",
"formSubmitURL",
"httpRealm",
"username",
"password",
"usernameField",
"passwordField",
import { LoginManagerStorage } from "resource://passwordmgr/passwordstorage.sys.mjs";
// `nsILoginMetaInfo` fields.
"timeCreated",
"timePasswordChanged",
];
// Compares two logins to determine if their syncable fields changed. The login
// manager fires `modifyLogin` for changes to all fields, including ones we
// don't sync. In particular, `timeLastUsed` changes shouldn't mark the login
// for upload; otherwise, we might overwrite changed passwords before they're
// downloaded (bug 973166).
function isSyncableChange(oldLogin, newLogin) {
oldLogin.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
newLogin.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
for (let property of SYNCABLE_LOGIN_FIELDS) {
if (oldLogin[property] != newLogin[property]) {
return true;
}
}
return false;
// Sync and many tests rely on having an time that is rounded to the nearest
// 100th of a second otherwise tests can fail intermittently.
function roundTimeForSync(time) {
return Math.round(time / 10) / 100;
}
export function LoginRec(collection, id) {
@ -111,7 +88,10 @@ PasswordEngine.prototype = {
syncPriority: 2,
// Metadata for syncing is stored in the login manager
emptyChangeset() {
return new PasswordsChangeset();
},
async ensureCurrentSyncID(newSyncID) {
return Services.logins.ensureCurrentSyncID(newSyncID);
},
@ -126,46 +106,54 @@ PasswordEngine.prototype = {
);
return legacyValue;
}
return Services.logins.getLastSync();
return this._store.storage.getLastSync();
},
async setLastSync(timestamp) {
await Services.logins.setLastSync(timestamp);
await this._store.storage.setLastSync(timestamp);
},
async _syncFinish() {
await SyncEngine.prototype._syncFinish.call(this);
// Testing function to emulate that a login has been synced.
async markSynced(guid) {
this._store.storage.resetSyncCounter(guid, 0);
},
// Delete the Weave credentials from the server once.
if (!Svc.Prefs.get("deletePwdFxA", false)) {
try {
let ids = [];
for (let host of Utils.getSyncCredentialsHosts()) {
for (let info of Services.logins.findLogins(host, "", "")) {
ids.push(info.QueryInterface(Ci.nsILoginMetaInfo).guid);
}
async pullAllChanges() {
return this._getChangedIDs(true);
},
async getChangedIDs() {
return this._getChangedIDs(false);
},
async _getChangedIDs(getAll) {
let changes = {};
let logins = await this._store.storage.getAllLoginsAsync(true);
for (let login of logins) {
if (getAll || login.syncCounter > 0) {
if (Utils.getSyncCredentialsHosts().has(login.origin)) {
continue;
}
if (ids.length) {
let coll = new Collection(this.engineURL, null, this.service);
coll.ids = ids;
let ret = await coll.delete();
this._log.debug("Delete result: " + ret);
if (!ret.success && ret.status != 400) {
// A non-400 failure means try again next time.
return;
}
} else {
this._log.debug("Didn't find any passwords to delete");
}
// If there were no ids to delete, or we succeeded, or got a 400,
// record success.
Svc.Prefs.set("deletePwdFxA", true);
Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
} catch (ex) {
if (Async.isShutdownException(ex)) {
throw ex;
}
this._log.debug("Password deletes failed", ex);
changes[login.guid] = {
counter: login.syncCounter, // record the initial counter value
modified: roundTimeForSync(login.timePasswordChanged),
deleted: this._store.storage.loginIsDeleted(login.guid),
};
}
}
return changes;
},
async trackRemainingChanges() {
// Reset the syncCounter on the items that were changed.
for (let [guid, { counter, synced }] of Object.entries(
this._modified.changes
)) {
if (synced) {
this._store.storage.resetSyncCounter(guid, counter);
}
}
},
@ -176,7 +164,7 @@ PasswordEngine.prototype = {
return null;
}
let logins = Services.logins.findLogins(
let logins = this._store.storage.findLogins(
login.origin,
login.formActionOrigin,
login.httpRealm
@ -194,13 +182,8 @@ PasswordEngine.prototype = {
return null;
},
async pullAllChanges() {
let changes = {};
let ids = await this._store.getAllIDs();
for (let [id, info] of Object.entries(ids)) {
changes[id] = info.timePasswordChanged / 1000;
}
return changes;
_deleteId(id) {
this._noteDeletedId(id);
},
getValidator() {
@ -216,6 +199,7 @@ function PasswordStore(name, engine) {
Ci.nsILoginInfo,
"init"
);
this.storage = LoginManagerStorage.create();
}
PasswordStore.prototype = {
_newPropertyBag() {
@ -299,25 +283,30 @@ PasswordStore.prototype = {
return info;
},
async _getLoginFromGUID(id) {
let prop = this._newPropertyBag();
prop.setPropertyAsAUTF8String("guid", id);
let logins = Services.logins.searchLogins(prop);
await Async.promiseYield(); // Yield back to main thread after synchronous operation.
async _getLoginFromGUID(guid) {
let logins = await this.storage.searchLoginsAsync({ guid }, true);
if (logins.length) {
this._log.trace(logins.length + " items matching " + id + " found.");
this._log.trace(logins.length + " items matching " + guid + " found.");
return logins[0];
}
this._log.trace("No items matching " + id + " found. Ignoring");
this._log.trace("No items matching " + guid + " found. Ignoring");
return null;
},
async applyIncoming(record) {
if (record.deleted) {
// Need to supply the sourceSync flag.
await this.remove(record, { sourceSync: true });
return;
}
await super.applyIncoming(record);
},
async getAllIDs() {
let items = {};
let logins = Services.logins.getAllLogins();
let logins = await this.storage.getAllLoginsAsync(true);
for (let i = 0; i < logins.length; i++) {
// Skip over Weave password/passphrase entries.
@ -335,8 +324,7 @@ PasswordStore.prototype = {
async changeItemID(oldID, newID) {
this._log.trace("Changing item ID: " + oldID + " to " + newID);
let oldLogin = await this._getLoginFromGUID(oldID);
if (!oldLogin) {
if (!(await this.itemExists(oldID))) {
this._log.trace("Can't change item ID: item doesn't exist");
return;
}
@ -348,18 +336,20 @@ PasswordStore.prototype = {
let prop = this._newPropertyBag();
prop.setPropertyAsAUTF8String("guid", newID);
Services.logins.modifyLogin(oldLogin, prop);
let oldLogin = await this._getLoginFromGUID(oldID);
this.storage.modifyLogin(oldLogin, prop, true);
},
async itemExists(id) {
return !!(await this._getLoginFromGUID(id));
let login = await this._getLoginFromGUID(id);
return login && !this.storage.loginIsDeleted(id);
},
async createRecord(id, collection) {
let record = new LoginRec(collection, id);
let login = await this._getLoginFromGUID(id);
if (!login) {
if (!login || this.storage.loginIsDeleted(id)) {
record.deleted = true;
return record;
}
@ -399,6 +389,8 @@ PasswordStore.prototype = {
return;
}
login.everSynced = true;
this._log.trace("Adding login for " + record.hostname);
this._log.trace(
"httpRealm: " +
@ -410,7 +402,7 @@ PasswordStore.prototype = {
await Services.logins.addLoginAsync(login);
},
async remove(record) {
async remove(record, { sourceSync = false } = {}) {
this._log.trace("Removing login " + record.id);
let loginItem = await this._getLoginFromGUID(record.id);
@ -419,12 +411,12 @@ PasswordStore.prototype = {
return;
}
Services.logins.removeLogin(loginItem);
this.storage.removeLogin(loginItem, sourceSync);
},
async update(record) {
let loginItem = await this._getLoginFromGUID(record.id);
if (!loginItem) {
if (!loginItem || this.storage.loginIsDeleted(record.id)) {
this._log.trace("Skipping update for unknown item: " + record.hostname);
return;
}
@ -435,17 +427,19 @@ PasswordStore.prototype = {
return;
}
Services.logins.modifyLogin(loginItem, newinfo);
loginItem.everSynced = true;
this.storage.modifyLogin(loginItem, newinfo, true);
},
async wipe() {
Services.logins.removeAllUserFacingLogins();
this.storage.removeAllUserFacingLogins(true);
},
};
Object.setPrototypeOf(PasswordStore.prototype, Store.prototype);
function PasswordTracker(name, engine) {
LegacyTracker.call(this, name, engine);
Tracker.call(this, name, engine);
}
PasswordTracker.prototype = {
onStart() {
@ -461,66 +455,32 @@ PasswordTracker.prototype = {
return;
}
// A single add, remove or change or removing all items
// will trigger a sync for MULTI_DEVICE.
switch (data) {
case "modifyLogin": {
subject.QueryInterface(Ci.nsIArrayExtensions);
let oldLogin = subject.GetElementAt(0);
let newLogin = subject.GetElementAt(1);
if (!isSyncableChange(oldLogin, newLogin)) {
this._log.trace(`${data}: Ignoring change for ${newLogin.guid}`);
break;
}
const tracked = await this._trackLogin(newLogin);
if (tracked) {
this._log.trace(`${data}: Tracking change for ${newLogin.guid}`);
case "modifyLogin":
// The syncCounter should have been incremented only for
// those items that need to be sycned.
if (
subject.QueryInterface(Ci.nsIArrayExtensions).GetElementAt(1)
.syncCounter > 0
) {
this.score += SCORE_INCREMENT_XLARGE;
}
break;
}
case "addLogin":
case "removeLogin":
subject
.QueryInterface(Ci.nsILoginMetaInfo)
.QueryInterface(Ci.nsILoginInfo);
const tracked = await this._trackLogin(subject);
if (tracked) {
this._log.trace(data + ": " + subject.guid);
}
break;
// Bug 1613620: We iterate through the removed logins and track them to ensure
// the logins are deleted across synced devices/accounts
case "removeAllLogins":
subject.QueryInterface(Ci.nsIArrayExtensions);
let count = subject.Count();
for (let i = 0; i < count; i++) {
let currentSubject = subject.GetElementAt(i);
let tracked = await this._trackLogin(currentSubject);
if (tracked) {
this._log.trace(data + ": " + currentSubject.guid);
}
}
this.score += SCORE_INCREMENT_XLARGE;
break;
}
},
async _trackLogin(login) {
if (Utils.getSyncCredentialsHosts().has(login.origin)) {
// Skip over Weave password/passphrase changes.
return false;
case "removeAllLogins":
this.score +=
SCORE_INCREMENT_XLARGE *
(subject.QueryInterface(Ci.nsIArrayExtensions).Count() + 1);
break;
}
const added = await this.addChangedID(login.guid);
if (!added) {
return false;
}
this.score += SCORE_INCREMENT_XLARGE;
return true;
},
};
Object.setPrototypeOf(PasswordTracker.prototype, LegacyTracker.prototype);
Object.setPrototypeOf(PasswordTracker.prototype, Tracker.prototype);
export class PasswordValidator extends CollectionValidator {
constructor() {
@ -563,3 +523,27 @@ export class PasswordValidator extends CollectionValidator {
return Object.assign({ guid: item.id }, item);
}
}
export class PasswordsChangeset extends Changeset {
getModifiedTimestamp(id) {
return this.changes[id].modified;
}
has(id) {
let change = this.changes[id];
if (change) {
return !change.synced;
}
return false;
}
delete(id) {
let change = this.changes[id];
if (change) {
// Mark the change as synced without removing it from the set.
// This allows the sync counter to be reset when sync is complete
// within trackRemainingChanges.
change.synced = true;
}
}
}

View File

@ -45,17 +45,19 @@ add_task(async function test_ignored_fields() {
enableValidationPrefs();
let login = await Services.logins.addLoginAsync(
new LoginInfo(
"https://example.com",
"",
null,
"username",
"password",
"",
""
)
let loginInfo = new LoginInfo(
"https://example.com",
"",
null,
"username",
"password",
"",
""
);
// Setting syncCounter to -1 so that it will be incremented to 0 when added.
loginInfo.syncCounter = -1;
let login = await Services.logins.addLoginAsync(loginInfo);
login.QueryInterface(Ci.nsILoginMetaInfo); // For `guid`.
engine._tracker.start();

View File

@ -23,9 +23,17 @@ add_task(async function test_tracking() {
let recordNum = 0;
_("Verify we've got an empty tracker to work with.");
let changes = await tracker.getChangedIDs();
let changes = await engine.getChangedIDs();
do_check_empty(changes);
let exceptionHappened = false;
try {
await tracker.getChangedIDs();
} catch (ex) {
exceptionHappened = true;
}
ok(exceptionHappened, "tracker does not keep track of changes");
async function createPassword() {
_("RECORD NUM: " + recordNum);
let record = new LoginRec("passwords", "GUID" + recordNum);
@ -45,47 +53,36 @@ add_task(async function test_tracking() {
}
try {
_(
"Create a password record. Won't show because we haven't started tracking yet"
);
await createPassword();
changes = await tracker.getChangedIDs();
do_check_empty(changes);
Assert.equal(tracker.score, 0);
_("Tell the tracker to start tracking changes.");
tracker.start();
await createPassword();
changes = await tracker.getChangedIDs();
changes = await engine.getChangedIDs();
do_check_attribute_count(changes, 1);
Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
_("Starting twice won't do any harm.");
tracker.start();
await createPassword();
changes = await tracker.getChangedIDs();
changes = await engine.getChangedIDs();
do_check_attribute_count(changes, 2);
Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
_("Let's stop tracking again.");
await tracker.clearChangedIDs();
tracker.resetScore();
await tracker.stop();
await createPassword();
changes = await tracker.getChangedIDs();
do_check_empty(changes);
changes = await engine.getChangedIDs();
do_check_attribute_count(changes, 3);
Assert.equal(tracker.score, 0);
_("Stopping twice won't do any harm.");
await tracker.stop();
await createPassword();
changes = await tracker.getChangedIDs();
do_check_empty(changes);
changes = await engine.getChangedIDs();
do_check_attribute_count(changes, 4);
Assert.equal(tracker.score, 0);
} finally {
_("Clean up.");
await store.wipe();
await tracker.clearChangedIDs();
tracker.resetScore();
await tracker.stop();
}
@ -93,7 +90,7 @@ add_task(async function test_tracking() {
add_task(async function test_onWipe() {
_("Verify we've got an empty tracker to work with.");
const changes = await tracker.getChangedIDs();
const changes = await engine.getChangedIDs();
do_check_empty(changes);
Assert.equal(tracker.score, 0);
@ -114,47 +111,58 @@ add_task(async function test_removeAllLogins() {
let recordNum = 0;
_("Verify that all tracked logins are removed.");
async function createPassword() {
_("RECORD NUM: " + recordNum);
let record = new LoginRec("passwords", "GUID" + recordNum);
record.cleartext = {
id: "GUID" + recordNum,
hostname: "http://foo.bar.com",
formSubmitURL: "http://foo.bar.com",
username: "john" + recordNum,
password: "smith",
usernameField: "username",
passwordField: "password",
};
recordNum++;
let login = store._nsLoginInfoFromRecord(record);
await Services.logins.addLoginAsync(login);
await tracker.asyncObserver.promiseObserversComplete();
}
try {
_("Tell tracker to start tracking changes");
tracker.start();
await createPassword();
await createPassword();
let changes = await tracker.getChangedIDs();
do_check_attribute_count(changes, 2);
Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
// Perform this test twice, the first time where a sync is not performed
// between adding and removing items and the second time where a sync is
// performed. In the former case, the logins will just be deleted because
// they have never been synced, so they won't be detected as changes. In
// the latter case, the logins have been synced so they will be marked for
// deletion.
for (let syncBeforeRemove of [false, true]) {
async function createPassword() {
_("RECORD NUM: " + recordNum);
let record = new LoginRec("passwords", "GUID" + recordNum);
record.cleartext = {
id: "GUID" + recordNum,
hostname: "http://foo.bar.com",
formSubmitURL: "http://foo.bar.com",
username: "john" + recordNum,
password: "smith",
usernameField: "username",
passwordField: "password",
};
recordNum++;
let login = store._nsLoginInfoFromRecord(record);
await Services.logins.addLoginAsync(login);
await tracker.clearChangedIDs();
changes = await tracker.getChangedIDs();
do_check_attribute_count(changes, 0);
await tracker.asyncObserver.promiseObserversComplete();
}
try {
_("Tell tracker to start tracking changes");
tracker.start();
await createPassword();
await createPassword();
let changes = await engine.getChangedIDs();
do_check_attribute_count(changes, 2);
Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
_("Tell sync to remove all logins");
Services.logins.removeAllUserFacingLogins();
await tracker.asyncObserver.promiseObserversComplete();
changes = await tracker.getChangedIDs();
do_check_attribute_count(changes, 2);
Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 5);
} finally {
_("Clean up.");
await store.wipe();
await tracker.clearChangedIDs();
tracker.resetScore();
await tracker.stop();
if (syncBeforeRemove) {
let logins = await Services.logins.getAllLoginsAsync(true);
for (let login of logins) {
engine.markSynced(login.guid);
}
}
_("Tell sync to remove all logins");
Services.logins.removeAllUserFacingLogins();
await tracker.asyncObserver.promiseObserversComplete();
changes = await engine.getChangedIDs();
do_check_attribute_count(changes, syncBeforeRemove ? 2 : 0);
Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 5);
} finally {
_("Clean up.");
await store.wipe();
tracker.resetScore();
await tracker.stop();
}
}
});

View File

@ -27,6 +27,9 @@ nsLoginInfo.prototype = {
passwordField: null,
unknownFields: null,
everSynced: false,
syncCounter: 0,
get displayOrigin() {
let displayOrigin = this.origin;
try {
@ -120,6 +123,8 @@ nsLoginInfo.prototype = {
clone.timeLastUsed = this.timeLastUsed;
clone.timePasswordChanged = this.timePasswordChanged;
clone.timesUsed = this.timesUsed;
clone.syncCounter = this.syncCounter;
clone.everSynced = this.everSynced;
// Unknown fields from other clients
clone.unknownFields = this.unknownFields;

View File

@ -100,6 +100,17 @@ interface nsILoginInfo : nsISupports {
*/
attribute AString unknownFields;
/**
* True if the login has ever been synced at some point.
*/
attribute boolean everSynced;
/**
* A counter used to indicate that syncing is occuring. It will get restored to 0
* once syncing is complete.
*/
attribute long syncCounter;
/**
* Initialize a newly created nsLoginInfo object.
*

View File

@ -82,19 +82,19 @@ export class LoginManagerStorage extends LoginManagerStorage_json {
}
/**
* Returns an array of all saved logins that can be decrypted.
* Returns a promise resolving to an array of all saved logins that can be decrypted.
*
* @resolve {nsILoginInfo[]}
*/
async getAllLoginsAsync() {
return this._getLoginsAsync({});
getAllLoginsAsync(includeDeleted) {
return this._getLoginsAsync({}, includeDeleted);
}
async searchLoginsAsync(matchData) {
async searchLoginsAsync(matchData, includeDeleted) {
this.log(
`Searching for matching saved logins for origin: ${matchData.origin}`
);
return this._getLoginsAsync(matchData);
return this._getLoginsAsync(matchData, includeDeleted);
}
_baseHostnameFromOrigin(origin) {
@ -117,7 +117,7 @@ export class LoginManagerStorage extends LoginManagerStorage_json {
}
}
async _getLoginsAsync(matchData) {
async _getLoginsAsync(matchData, includeDeleted) {
let baseHostname = this._baseHostnameFromOrigin(matchData.origin);
// Query all logins for the eTLD+1 and then filter the logins in _searchLogins
@ -164,6 +164,7 @@ export class LoginManagerStorage extends LoginManagerStorage_json {
const [logins] = this._searchLogins(
realMatchData,
includeDeleted,
options,
candidateLogins.map(this._vanillaLoginToStorageLogin)
);

View File

@ -15,6 +15,37 @@ ChromeUtils.defineESModuleGetters(lazy, {
LoginStore: "resource://gre/modules/LoginStore.sys.mjs",
});
const SYNCABLE_LOGIN_FIELDS = [
// `nsILoginInfo` fields.
"hostname",
"formSubmitURL",
"httpRealm",
"username",
"password",
"usernameField",
"passwordField",
// `nsILoginMetaInfo` fields.
"timeCreated",
"timePasswordChanged",
];
// Compares two logins to determine if their syncable fields changed. The login
// manager fires `modifyLogin` for changes to all fields, including ones we
// don't sync. In particular, `timeLastUsed` changes shouldn't mark the login
// for upload; otherwise, we might overwrite changed passwords before they're
// downloaded (bug 973166).
function isSyncableChange(oldLogin, newLogin) {
oldLogin.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
newLogin.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
return SYNCABLE_LOGIN_FIELDS.some(prop => oldLogin[prop] != newLogin[prop]);
}
// Returns true if the argument is for the FxA login.
function isFXAHost(login) {
return login.hostname == lazy.FXA_PWDMGR_HOST;
}
XPCOMUtils.defineLazyModuleGetters(lazy, {
FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js",
FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js",
@ -147,6 +178,29 @@ export class LoginManagerStorage_json {
this._store.saveSoon();
}
#incrementSyncCounter(login) {
login.syncCounter++;
}
async resetSyncCounter(guid, value) {
this._store.ensureDataReady();
// This will also find deleted items.
let login = this._store.data.logins.find(login => login.guid == guid);
if (login?.syncCounter > 0) {
login.syncCounter = Math.max(0, login.syncCounter - value);
login.everSynced = true;
}
this._store.saveSoon();
}
// Returns false if the login has marked as deleted or doesn't exist.
loginIsDeleted(guid) {
let login = this._store.data.logins.find(l => l.guid == guid);
return !!login?.deleted;
}
addLogin(
login,
preEncrypted = false,
@ -225,6 +279,12 @@ export class LoginManagerStorage_json {
loginClone.timesUsed = 1;
}
// If the everSynced is already set, then this login is an incoming
// sync record, so there is no need to mark this as needed to be synced.
if (!loginClone.everSynced && !isFXAHost(loginClone)) {
this.#incrementSyncCounter(loginClone);
}
this._store.data.logins.push({
id: this._store.data.nextId++,
hostname: loginClone.origin,
@ -240,6 +300,8 @@ export class LoginManagerStorage_json {
timeLastUsed: loginClone.timeLastUsed,
timePasswordChanged: loginClone.timePasswordChanged,
timesUsed: loginClone.timesUsed,
syncCounter: loginClone.syncCounter,
everSynced: loginClone.everSynced,
encryptedUnknownFields: encUnknownFields,
});
this._store.saveSoon();
@ -249,7 +311,7 @@ export class LoginManagerStorage_json {
return loginClone;
}
removeLogin(login) {
removeLogin(login, fromSync) {
this._store.ensureDataReady();
let [idToDelete, storedLogin] = this._getIdForLogin(login);
@ -259,14 +321,27 @@ export class LoginManagerStorage_json {
let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
if (foundIndex != -1) {
this._store.data.logins.splice(foundIndex, 1);
this._store.saveSoon();
let login = this._store.data.logins[foundIndex];
if (!login.deleted) {
if (fromSync) {
login.deleted = true;
} else if (login.everSynced) {
// The login has been synced, so mark it as deleted.
login.deleted = true;
this.#incrementSyncCounter(login);
} else {
// The login was never synced, so just remove it from the data.
this._store.data.logins.splice(foundIndex, 1);
}
this._store.saveSoon();
}
}
lazy.LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
}
modifyLogin(oldLogin, newLoginData) {
modifyLogin(oldLogin, newLoginData, fromSync) {
this._store.ensureDataReady();
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
@ -303,12 +378,22 @@ export class LoginManagerStorage_json {
}
}
// Don't sync changes to the accounts password or when changes were only
// made to fields that should not be synced.
if (
!fromSync &&
!isFXAHost(newLogin) &&
isSyncableChange(oldLogin, newLogin)
) {
this.#incrementSyncCounter(newLogin);
}
// Get the encrypted value of the username and password.
let [encUsername, encPassword, encType, encUnknownFields] =
this._encryptLogin(newLogin);
for (let loginItem of this._store.data.logins) {
if (loginItem.id == idToModify) {
if (loginItem.id == idToModify && !loginItem.deleted) {
loginItem.hostname = newLogin.origin;
loginItem.httpRealm = newLogin.httpRealm;
loginItem.formSubmitURL = newLogin.formActionOrigin;
@ -323,6 +408,7 @@ export class LoginManagerStorage_json {
loginItem.timePasswordChanged = newLogin.timePasswordChanged;
loginItem.timesUsed = newLogin.timesUsed;
loginItem.encryptedUnknownFields = encUnknownFields;
loginItem.syncCounter = newLogin.syncCounter;
this._store.saveSoon();
break;
}
@ -383,10 +469,10 @@ export class LoginManagerStorage_json {
*
* @resolve {nsILoginInfo[]}
*/
async getAllLoginsAsync() {
async getAllLoginsAsync(includeDeleted) {
this._store.ensureDataReady();
let [logins] = this._searchLogins({});
let [logins] = this._searchLogins({}, includeDeleted);
if (!logins.length) {
return [];
}
@ -431,9 +517,12 @@ export class LoginManagerStorage_json {
return result;
}
async searchLoginsAsync(matchData) {
async searchLoginsAsync(matchData, includeDeleted) {
this.log(`Searching for matching logins for origin ${matchData.origin}.`);
let result = this.searchLogins(lazy.LoginHelper.newPropertyBag(matchData));
let result = this.searchLogins(
lazy.LoginHelper.newPropertyBag(matchData),
includeDeleted
);
// Emulate being async:
return Promise.resolve(result);
}
@ -444,7 +533,7 @@ export class LoginManagerStorage_json {
*
* @return {nsILoginInfo[]} which are decrypted.
*/
searchLogins(matchData) {
searchLogins(matchData, includeDeleted) {
this._store.ensureDataReady();
let realMatchData = {};
@ -477,7 +566,7 @@ export class LoginManagerStorage_json {
}
}
let [logins] = this._searchLogins(realMatchData, options);
let [logins] = this._searchLogins(realMatchData, includeDeleted, options);
// Decrypt entries found for the caller.
logins = this._decryptLogins(logins);
@ -495,6 +584,7 @@ export class LoginManagerStorage_json {
*/
_searchLogins(
matchData,
includeDeleted = false,
aOptions = {
schemeUpgrades: false,
acceptDifferentSubdomains: false,
@ -579,6 +669,8 @@ export class LoginManagerStorage_json {
case "timeLastUsed":
case "timePasswordChanged":
case "timesUsed":
case "syncCounter":
case "everSynced":
if (wantedValue == null && aLoginItem[storageFieldName]) {
return false;
} else if (aLoginItem[storageFieldName] != wantedValue) {
@ -596,6 +688,10 @@ export class LoginManagerStorage_json {
let foundLogins = [],
foundIds = [];
for (let loginItem of candidateLogins) {
if (loginItem.deleted && !includeDeleted) {
continue; // skip deleted items
}
if (match(loginItem)) {
// Create the new nsLoginInfo object, push to array
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
@ -617,6 +713,8 @@ export class LoginManagerStorage_json {
login.timeLastUsed = loginItem.timeLastUsed;
login.timePasswordChanged = loginItem.timePasswordChanged;
login.timesUsed = loginItem.timesUsed;
login.syncCounter = loginItem.syncCounter;
login.everSynced = loginItem.everSynced;
// Any unknown fields along for the ride
login.unknownFields = loginItem.encryptedUnknownFields;
@ -638,45 +736,58 @@ export class LoginManagerStorage_json {
*
*/
removeAllLogins() {
this._store.ensureDataReady();
this._store.data.logins = [];
this._store.data.potentiallyVulnerablePasswords = [];
this.__decryptedPotentiallyVulnerablePasswords = null;
this._store.data.dismissedBreachAlertsByLoginGUID = {};
this._store.saveSoon();
lazy.LoginHelper.notifyStorageChanged("removeAllLogins", []);
this.#removeLogins(false, true);
}
/**
* Removes all user facing logins from storage. e.g. all logins except the FxA Sync key
*
* If you need to remove the FxA key, use `removeAllLogins` instead
*
* @param fullyRemove remove the logins rather than mark them deleted.
*/
removeAllUserFacingLogins() {
removeAllUserFacingLogins(fullyRemove) {
this.#removeLogins(fullyRemove, false);
}
/**
* Removes all logins from storage. If removeFXALogin is true, then the FxA Sync
* key is also removed.
*
* @param fullyRemove remove the logins rather than mark them deleted.
* @param removeFXALogin also remove the FxA Sync key.
*/
#removeLogins(fullyRemove, removeFXALogin = false) {
this._store.ensureDataReady();
this.log("Removing all logins.");
let [allLogins] = this._searchLogins({});
let fxaKey = this._store.data.logins.find(
login =>
login.hostname == lazy.FXA_PWDMGR_HOST &&
let removedLogins = [];
let remainingLogins = [];
for (let login of this._store.data.logins) {
if (
!removeFXALogin &&
isFXAHost(login) &&
login.httpRealm == lazy.FXA_PWDMGR_REALM
);
if (fxaKey) {
this._store.data.logins = [fxaKey];
allLogins = allLogins.filter(item => item != fxaKey);
} else {
this._store.data.logins = [];
) {
remainingLogins.push(login);
} else {
removedLogins.push(login);
if (!fullyRemove && login?.everSynced) {
// The login has been synced, so mark it as deleted.
login.deleted = true;
this.#incrementSyncCounter(login);
remainingLogins.push(login);
}
}
}
this._store.data.logins = remainingLogins;
this._store.data.potentiallyVulnerablePasswords = [];
this.__decryptedPotentiallyVulnerablePasswords = null;
this._store.data.dismissedBreachAlertsByLoginGUID = {};
this._store.saveSoon();
lazy.LoginHelper.notifyStorageChanged("removeAllLogins", allLogins);
lazy.LoginHelper.notifyStorageChanged("removeAllLogins", removedLogins);
}
findLogins(origin, formActionOrigin, httpRealm) {

View File

@ -102,6 +102,8 @@ add_task(async function test_no_new_properties_to_export() {
"password",
"passwordField",
"unknownFields",
"everSynced",
"syncCounter",
"init",
"equals",
"matches",