Bug 1377100 - Store sync metadata in the LoginManager. r=lina,MattN

This is done so that if the login manager loses all data sync is "reset",
meaning will do a full reconcile with the server and pull all sync records
down and apply them locally - ie, sync will be ble to recover passwords for
many users.

The sync GUID is stored encrypted, so that even if the login manager data
isn't lost, but the encryption key is, it will still reset and recover.

This will also make restoring an older logins.json a little more reliable for
sync - if an old version is restored then sync should "rewind" itself to the
state it was in the backup.

Differential Revision: https://phabricator.services.mozilla.com/D82663
This commit is contained in:
Mark Hammond 2020-07-28 04:19:43 +00:00
Родитель 0f69748e1a
Коммит cd4854dc3a
8 изменённых файлов: 272 добавлений и 1 удалений

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

@ -90,6 +90,59 @@ PasswordEngine.prototype = {
syncPriority: 2,
// Metadata for syncing is stored in the login manager. We also migrate it
// from preferences which can be removed eventually via bug 1651568. Note that
// we don't support profile downgrades - once it's migrated, the login manager
// becomes the single source of truth.
// Note also that the syncID is stored encrypted and null is returned if it
// can't be decrypted - this is done for that 'return null' side-effect rather
// than due to privacy - we want failure to decrypt the store to be treated as
// an engine reset.
async getSyncID() {
let legacyValue = this._syncID; // the base preference getter.
if (legacyValue) {
await Services.logins.setSyncID(legacyValue);
Svc.Prefs.reset(this.name + ".syncID");
this._log.debug(`migrated syncID of ${legacyValue} to the logins store`);
return legacyValue;
}
return Services.logins.getSyncID();
},
async ensureCurrentSyncID(newSyncID) {
// getSyncID above really only exists for this function - the rest of sync
// has already moved away from it, and even our tests barely use it.
// When we remove the migration code (bug 1651568) we should consider
// removing getSyncID() from both here and the login manager, and pushing
// this ensureCurrentSyncID() function down into the login manager.
let existingSyncID = await this.getSyncID();
if (existingSyncID == newSyncID) {
return existingSyncID;
}
this._log.debug("Engine syncIDs: " + [newSyncID, existingSyncID]);
await Services.logins.setSyncID(newSyncID);
await Services.logins.setLastSync(0);
return newSyncID;
},
async getLastSync() {
let legacyValue = await super.getLastSync();
if (legacyValue) {
await this.setLastSync(legacyValue);
Svc.Prefs.reset(this.name + ".lastSync");
this._log.debug(
`migrated timestamp of ${legacyValue} to the logins store`
);
return legacyValue;
}
return Services.logins.getLastSync();
},
async setLastSync(timestamp) {
await Services.logins.setLastSync(timestamp);
},
async _syncFinish() {
await SyncEngine.prototype._syncFinish.call(this);

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

@ -22,7 +22,9 @@ async function cleanup(engine, server) {
await engine.wipeClient();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
await promiseStopServer(server);
if (server) {
await promiseStopServer(server);
}
}
add_task(async function setup() {
@ -489,3 +491,43 @@ add_task(async function test_sync_password_validation() {
await cleanup(engine, server);
}
});
add_task(async function test_migrate_metadata() {
_("Ensure we correctly migrate metadata from prefs to the LoginManager");
// Sadly we first need to manually reset the login manager data for this.
Services.logins.setSyncID(null);
Services.logins.setLastSync(0);
// And set the pref values we want to migrate from.
Svc.Prefs.set("passwords.syncID", "pref-value");
Svc.Prefs.set("passwords.lastSync", 1);
let engine = Service.engineManager.get("passwords");
try {
equal(await engine.getSyncID(), "pref-value");
equal(await engine.getLastSync(), 1);
// check we removed the prefs.
ok(!Svc.Prefs.isSet("passwords.syncID"));
ok(!Svc.Prefs.isSet("passwords.lastSync"));
// explicitly set them - prefs should not be touched.
await engine.ensureCurrentSyncID("new-value");
await engine.setLastSync(2);
equal(await engine.getSyncID(), "new-value");
equal(await engine.getLastSync(), 2);
ok(!Svc.Prefs.isSet("passwords.syncID"));
ok(!Svc.Prefs.isSet("passwords.lastSync"));
// Perform an engine reset.
await engine._resetClient();
// timestamp should have been reset (but syncID isn't)
equal(await engine.getSyncID(), "new-value");
equal(await engine.getLastSync(), 0);
// and still no prefs.
ok(!Svc.Prefs.isSet("passwords.syncID"));
ok(!Svc.Prefs.isSet("passwords.lastSync"));
} finally {
await cleanup(engine, null);
}
});

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

@ -531,6 +531,23 @@ LoginManager.prototype = {
return this._storage.countLogins(origin, formActionOrigin, httpRealm);
},
/* Sync metadata functions - see nsILoginManagerStorage for details */
async getSyncID() {
return this._storage.getSyncID();
},
async setSyncID(id) {
await this._storage.setSyncID(id);
},
async getLastSync() {
return this._storage.getLastSync();
},
async setLastSync(timestamp) {
await this._storage.setLastSync(timestamp);
},
get uiBusy() {
return this._storage.uiBusy;
},

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

@ -232,6 +232,37 @@ interface nsILoginManager : nsISupports {
*/
Array<nsILoginInfo> searchLogins(in nsIPropertyBag matchData);
/**
* Returns the "sync id" used by Sync to know whether the store is current with
* respect to the sync servers.
*
* Returns null if the data doesn't exist or if the data can't be
* decrypted (including if the master-password prompt is cancelled). This is
* OK for Sync as it can't even begin syncing if the master-password is
* locked as the sync encrytion keys are stored in this login manager.
*/
Promise getSyncID();
/**
* Sets the "sync id" used by Sync to know whether the store is current with
* respect to the sync servers. May be set to null.
*
* Throws if the data can't be encrypted (including if the master-password
* prompt is cancelled)
*/
Promise setSyncID(in AString syncID);
/**
* Returns the timestamp of the last sync as a double (in seconds since Epoch
* rounded to two decimal places), or 0.0 if the data doesn't exist.
*/
Promise getLastSync();
/**
* Sets the timestamp of the last sync.
*/
Promise setLastSync(in double timestamp);
/**
* True when a master password prompt is being displayed.
*/

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

@ -202,6 +202,38 @@ interface nsILoginManagerStorage : nsISupports {
*/
unsigned long countLogins(in AString aOrigin, in AString aActionOrigin,
in AString aHttpRealm);
/**
* Returns the "sync id" used by Sync to know whether the store is current with
* respect to the sync servers.
*
* Returns null if the data doesn't exist or if the data can't be
* decrypted (including if the master-password prompt is cancelled). This is
* OK for Sync as it can't even begin syncing if the master-password is
* locked as the sync encrytion keys are stored in this login manager.
*/
Promise getSyncID();
/**
* Sets the "sync id" used by Sync to know whether the store is current with
* respect to the sync servers. May be set to null.
*
* Throws if the data can't be encrypted (including if the master-password
* prompt is cancelled)
*/
Promise setSyncID(in AString syncID);
/**
* Returns the timestamp of the last sync as a double (in seconds since Epoch
* rounded to two decimal places), or 0.0 if the data doesn't exist.
*/
Promise getLastSync();
/**
* Sets the timestamp of the last sync.
*/
Promise setLastSync(in double timestamp);
/**
* True when a master password prompt is being shown.
*/

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

@ -227,6 +227,25 @@ class LoginManagerStorage_geckoview extends LoginManagerStorage_json {
_decryptLogins(logins) {
return logins;
}
/**
* Sync metadata, which isn't supported by GeckoView.
*/
async getSyncID() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async setSyncID(syncID) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async getLastSync() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
async setLastSync(timestamp) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
}
XPCOMUtils.defineLazyGetter(

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

@ -121,6 +121,62 @@ class LoginManagerStorage_json {
return this._store._save();
}
/**
* Returns the "sync id" used by Sync to know whether the store is current with
* respect to the sync servers. It is stored encrypted, but only so we
* can detect failure to decrypt (for example, a "reset" of the master
* password will leave all logins alone, but they will fail to decrypt. We
* also want this metadata to be unavailable in that scenario)
*
* Returns null if the data doesn't exist or if the data can't be
* decrypted (including if the master-password prompt is cancelled). This is
* OK for Sync as it can't even begin syncing if the master-password is
* locked as the sync encrytion keys are stored in this login manager.
*/
async getSyncID() {
await this._store.load();
if (!this._store.data.sync) {
return null;
}
let raw = this._store.data.sync.syncID;
try {
return raw ? this._crypto.decrypt(raw) : null;
} catch (e) {
if (e.result == Cr.NS_ERROR_FAILURE) {
this.log("Could not decrypt the syncID - returning null");
return null;
}
// any other errors get re-thrown.
throw e;
}
}
async setSyncID(syncID) {
await this._store.load();
if (!this._store.data.sync) {
this._store.data.sync = {};
}
this._store.data.sync.syncID = syncID ? this._crypto.encrypt(syncID) : null;
this._store.saveSoon();
}
async getLastSync() {
await this._store.load();
if (!this._store.data.sync) {
return 0;
}
return this._store.data.sync.lastSync || 0.0;
}
async setLastSync(timestamp) {
await this._store.load();
if (!this._store.data.sync) {
this._store.data.sync = {};
}
this._store.data.sync.lastSync = timestamp;
this._store.saveSoon();
}
addLogin(
login,
preEncrypted = false,

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

@ -153,3 +153,24 @@ add_task(function test_add_logins_with_decrypt_failure() {
Services.logins.removeAllLogins();
});
// Test the "syncID" metadata works as expected on decryption failure.
add_task(async function test_sync_metadata_with_decrypt_failure() {
// And some sync metadata
await Services.logins.setSyncID("sync-id");
await Services.logins.setLastSync(123);
equal(await Services.logins.getSyncID(), "sync-id");
equal(await Services.logins.getLastSync(), 123);
// This makes the existing login and syncID non-decryptable.
resetMasterPassword();
// The syncID is now null.
equal(await Services.logins.getSyncID(), null);
// The sync timestamp isn't impacted.
equal(await Services.logins.getLastSync(), 123);
// But we should be able to set it again.
await Services.logins.setSyncID("new-id");
equal(await Services.logins.getSyncID(), "new-id");
});