diff options
Diffstat (limited to 'toolkit/components/passwordmgr/storage-json.sys.mjs')
-rw-r--r-- | toolkit/components/passwordmgr/storage-json.sys.mjs | 879 |
1 files changed, 879 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/storage-json.sys.mjs b/toolkit/components/passwordmgr/storage-json.sys.mjs new file mode 100644 index 0000000000..2345ed8dc9 --- /dev/null +++ b/toolkit/components/passwordmgr/storage-json.sys.mjs @@ -0,0 +1,879 @@ +/* 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/. */ + +/** + * nsILoginManagerStorage implementation for the JSON back-end. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + LoginStore: "resource://gre/modules/LoginStore.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js", + FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js", +}); + +export class LoginManagerStorage_json { + constructor() { + this.__crypto = null; // nsILoginManagerCrypto service + this.__decryptedPotentiallyVulnerablePasswords = null; + } + + get classID() { + return Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"); + } + + get QueryInterface() { + return ChromeUtils.generateQI(["nsILoginManagerStorage"]); + } + + get _crypto() { + if (!this.__crypto) { + this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService( + Ci.nsILoginManagerCrypto + ); + } + return this.__crypto; + } + + 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. + // See bug 717490 comment 17. + this._crypto; + + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path; + + // Set the reference to LoginStore synchronously. + let jsonPath = PathUtils.join(profileDir, "logins.json"); + let backupPath = ""; + let loginsBackupEnabled = Services.prefs.getBoolPref( + "signon.backup.enabled" + ); + if (loginsBackupEnabled) { + backupPath = PathUtils.join(profileDir, "logins-backup.json"); + } + this._store = new lazy.LoginStore(jsonPath, backupPath); + + return (async () => { + // Load the data asynchronously. + this.log(`Opening database at ${this._store.path}.`); + await this._store.load(); + })().catch(console.error); + } catch (e) { + this.log(`Initialization failed ${e.name}.`); + throw new Error("Initialization failed"); + } + } + + /** + * Internal method used by regression tests only. It is called before + * replacing this storage module with a new instance. + */ + terminate() { + this._store._saver.disarm(); + 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 primary + * 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 primary-password prompt is cancelled). This is + * OK for Sync as it can't even begin syncing if the primary-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, + plaintextUsername = null, + plaintextPassword = null + ) { + if ( + preEncrypted && + (typeof plaintextUsername != "string" || + typeof plaintextPassword != "string") + ) { + throw new Error( + "plaintextUsername and plaintextPassword are required when preEncrypted is true" + ); + } + + this._store.ensureDataReady(); + + // Throws if there are bogus values. + lazy.LoginHelper.checkLoginValues(login); + + let [encUsername, encPassword, encType, encUnknownFields] = preEncrypted + ? [ + login.username, + login.password, + this._crypto.defaultEncType, + login.unknownFields, + ] + : this._encryptLogin(login); + + // Reset the username and password to keep the same guarantees for preEncrypted + if (preEncrypted) { + login.username = plaintextUsername; + login.password = plaintextPassword; + } + + // Clone the login, so we don't modify the caller's object. + let loginClone = login.clone(); + + // Initialize the nsILoginMetaInfo fields, unless the caller gave us values + loginClone.QueryInterface(Ci.nsILoginMetaInfo); + if (loginClone.guid) { + let guid = loginClone.guid; + if (!this._isGuidUnique(guid)) { + // We have an existing GUID, but it's possible that entry is unable + // to be decrypted - if that's the case we remove the existing one + // and allow this one to be added. + let existing = this._searchLogins({ guid })[0]; + if (this._decryptLogins(existing).length) { + // Existing item is good, so it's an error to try and re-add it. + throw new Error("specified GUID already exists"); + } + // find and remove the existing bad entry. + let foundIndex = this._store.data.logins.findIndex(l => l.guid == guid); + if (foundIndex == -1) { + throw new Error("can't find a matching GUID to remove"); + } + this._store.data.logins.splice(foundIndex, 1); + } + } else { + loginClone.guid = Services.uuid.generateUUID().toString(); + } + + // Set timestamps + let currentTime = Date.now(); + if (!loginClone.timeCreated) { + loginClone.timeCreated = currentTime; + } + if (!loginClone.timeLastUsed) { + loginClone.timeLastUsed = currentTime; + } + if (!loginClone.timePasswordChanged) { + loginClone.timePasswordChanged = currentTime; + } + if (!loginClone.timesUsed) { + loginClone.timesUsed = 1; + } + + this._store.data.logins.push({ + id: this._store.data.nextId++, + hostname: loginClone.origin, + httpRealm: loginClone.httpRealm, + formSubmitURL: loginClone.formActionOrigin, + usernameField: loginClone.usernameField, + passwordField: loginClone.passwordField, + encryptedUsername: encUsername, + encryptedPassword: encPassword, + guid: loginClone.guid, + encType, + timeCreated: loginClone.timeCreated, + timeLastUsed: loginClone.timeLastUsed, + timePasswordChanged: loginClone.timePasswordChanged, + timesUsed: loginClone.timesUsed, + encryptedUnknownFields: encUnknownFields, + }); + this._store.saveSoon(); + + // Send a notification that a login was added. + lazy.LoginHelper.notifyStorageChanged("addLogin", loginClone); + return loginClone; + } + + removeLogin(login) { + this._store.ensureDataReady(); + + let [idToDelete, storedLogin] = this._getIdForLogin(login); + if (!idToDelete) { + throw new Error("No matching logins"); + } + + let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete); + if (foundIndex != -1) { + this._store.data.logins.splice(foundIndex, 1); + this._store.saveSoon(); + } + + lazy.LoginHelper.notifyStorageChanged("removeLogin", storedLogin); + } + + modifyLogin(oldLogin, newLoginData) { + this._store.ensureDataReady(); + + let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin); + if (!idToModify) { + throw new Error("No matching logins"); + } + + let newLogin = lazy.LoginHelper.buildModifiedLogin( + oldStoredLogin, + newLoginData + ); + + // Check if the new GUID is duplicate. + if ( + newLogin.guid != oldStoredLogin.guid && + !this._isGuidUnique(newLogin.guid) + ) { + throw new Error("specified GUID already exists"); + } + + // Look for an existing entry in case key properties changed. + if (!newLogin.matches(oldLogin, true)) { + let logins = this.findLogins( + newLogin.origin, + newLogin.formActionOrigin, + newLogin.httpRealm + ); + + let matchingLogin = logins.find(login => newLogin.matches(login, true)); + if (matchingLogin) { + throw lazy.LoginHelper.createLoginAlreadyExistsError( + matchingLogin.guid + ); + } + } + + // 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) { + loginItem.hostname = newLogin.origin; + loginItem.httpRealm = newLogin.httpRealm; + loginItem.formSubmitURL = newLogin.formActionOrigin; + loginItem.usernameField = newLogin.usernameField; + loginItem.passwordField = newLogin.passwordField; + loginItem.encryptedUsername = encUsername; + loginItem.encryptedPassword = encPassword; + loginItem.guid = newLogin.guid; + loginItem.encType = encType; + loginItem.timeCreated = newLogin.timeCreated; + loginItem.timeLastUsed = newLogin.timeLastUsed; + loginItem.timePasswordChanged = newLogin.timePasswordChanged; + loginItem.timesUsed = newLogin.timesUsed; + loginItem.encryptedUnknownFields = encUnknownFields; + this._store.saveSoon(); + break; + } + } + + lazy.LoginHelper.notifyStorageChanged("modifyLogin", [ + oldStoredLogin, + newLogin, + ]); + } + + recordPasswordUse(login) { + // Update the lastUsed timestamp and increment the use count. + let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag + ); + propBag.setProperty("timeLastUsed", Date.now()); + propBag.setProperty("timesUsedIncrement", 1); + this.modifyLogin(login, propBag); + } + + async recordBreachAlertDismissal(loginGUID) { + this._store.ensureDataReady(); + const dismissedBreachAlertsByLoginGUID = + this._store._data.dismissedBreachAlertsByLoginGUID; + + dismissedBreachAlertsByLoginGUID[loginGUID] = { + timeBreachAlertDismissed: new Date().getTime(), + }; + + return this._store.saveSoon(); + } + + getBreachAlertDismissalsByLoginGUID() { + this._store.ensureDataReady(); + return this._store._data.dismissedBreachAlertsByLoginGUID; + } + + /** + * @return {nsILoginInfo[]} + */ + getAllLogins() { + this._store.ensureDataReady(); + + let [logins] = this._searchLogins({}); + + // decrypt entries for caller. + logins = this._decryptLogins(logins); + + this.log(`Returning ${logins.length} logins.`); + return logins; + } + + /** + * Returns an array of nsILoginInfo. If decryption of a login + * fails due to a corrupt entry, the login is not included in + * the resulting array. + * + * @resolve {nsILoginInfo[]} + */ + async getAllLoginsAsync() { + this._store.ensureDataReady(); + + let [logins] = this._searchLogins({}); + if (!logins.length) { + return []; + } + let ciphertexts = logins + .map(l => l.username) + .concat(logins.map(l => l.password)); + let plaintexts = await this._crypto.decryptMany(ciphertexts); + let usernames = plaintexts.slice(0, logins.length); + let passwords = plaintexts.slice(logins.length); + + let result = []; + for (let i = 0; i < logins.length; i++) { + if (!usernames[i] || !passwords[i]) { + // If the username or password is blank it means that decryption may have + // failed during decryptMany but we can't differentiate an empty string + // value from a failure so we attempt to decrypt again and check the + // result. + let login = logins[i]; + try { + this._crypto.decrypt(login.username); + this._crypto.decrypt(login.password); + } catch (e) { + // If decryption failed (corrupt entry?), just skip it. + // Rethrow other errors (like canceling entry of a primary pw) + if (e.result == Cr.NS_ERROR_FAILURE) { + this.log( + `Could not decrypt login: ${ + login.QueryInterface(Ci.nsILoginMetaInfo).guid + }.` + ); + continue; + } + throw e; + } + } + + logins[i].username = usernames[i]; + logins[i].password = passwords[i]; + result.push(logins[i]); + } + + return result; + } + + async searchLoginsAsync(matchData) { + this.log(`Searching for matching logins for origin ${matchData.origin}.`); + let result = this.searchLogins(lazy.LoginHelper.newPropertyBag(matchData)); + // Emulate being async: + return Promise.resolve(result); + } + + /** + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a + * JavaScript object and decrypt the results. + * + * @return {nsILoginInfo[]} which are decrypted. + */ + searchLogins(matchData) { + this._store.ensureDataReady(); + + let realMatchData = {}; + let options = {}; + + matchData.QueryInterface(Ci.nsIPropertyBag2); + if (matchData.hasKey("guid")) { + // Enforce GUID-based filtering when available, since the origin of the + // login may not match the origin of the form in the case of scheme + // upgrades. + realMatchData = { guid: matchData.getProperty("guid") }; + } else { + // Convert nsIPropertyBag to normal JS object. + for (let prop of matchData.enumerator) { + switch (prop.name) { + // Some property names aren't field names but are special options to + // affect the search. + case "acceptDifferentSubdomains": + case "schemeUpgrades": + case "acceptRelatedRealms": + case "relatedRealms": { + options[prop.name] = prop.value; + break; + } + default: { + realMatchData[prop.name] = prop.value; + break; + } + } + } + } + + let [logins] = this._searchLogins(realMatchData, options); + + // Decrypt entries found for the caller. + logins = this._decryptLogins(logins); + + return logins; + } + + /** + * Private method to perform arbitrary searches on any field. Decryption is + * left to the caller. + * + * Returns [logins, ids] for logins that match the arguments, where logins + * is an array of encrypted nsLoginInfo and ids is an array of associated + * ids in the database. + */ + _searchLogins( + matchData, + aOptions = { + schemeUpgrades: false, + acceptDifferentSubdomains: false, + acceptRelatedRealms: false, + relatedRealms: [], + }, + candidateLogins = this._store.data.logins + ) { + if ( + "formActionOrigin" in matchData && + matchData.formActionOrigin === "" && + // Carve an exception out for a unit test in test_legacy_empty_formSubmitURL.js + Object.keys(matchData).length != 1 + ) { + throw new Error( + "Searching with an empty `formActionOrigin` doesn't do a wildcard search" + ); + } + + function match(aLoginItem) { + for (let field in matchData) { + let wantedValue = matchData[field]; + + // Override the storage field name for some fields due to backwards + // compatibility with Sync/storage. + let storageFieldName = field; + switch (field) { + case "formActionOrigin": { + storageFieldName = "formSubmitURL"; + break; + } + case "origin": { + storageFieldName = "hostname"; + break; + } + } + + switch (field) { + case "formActionOrigin": + if (wantedValue != null) { + // Historical compatibility requires this special case + if (aLoginItem.formSubmitURL == "") { + break; + } + if ( + !lazy.LoginHelper.isOriginMatching( + aLoginItem[storageFieldName], + wantedValue, + aOptions + ) + ) { + return false; + } + break; + } + // fall through + case "origin": + if (wantedValue != null) { + // needed for formActionOrigin fall through + if ( + !lazy.LoginHelper.isOriginMatching( + aLoginItem[storageFieldName], + wantedValue, + aOptions + ) + ) { + return false; + } + break; + } + // Normal cases. + // fall through + case "httpRealm": + case "id": + case "usernameField": + case "passwordField": + case "encryptedUsername": + case "encryptedPassword": + case "guid": + case "encType": + case "timeCreated": + case "timeLastUsed": + case "timePasswordChanged": + case "timesUsed": + if (wantedValue == null && aLoginItem[storageFieldName]) { + return false; + } else if (aLoginItem[storageFieldName] != wantedValue) { + return false; + } + break; + // Fail if caller requests an unknown property. + default: + throw new Error("Unexpected field: " + field); + } + } + return true; + } + + let foundLogins = [], + foundIds = []; + for (let loginItem of candidateLogins) { + if (match(loginItem)) { + // Create the new nsLoginInfo object, push to array + let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + login.init( + loginItem.hostname, + loginItem.formSubmitURL, + loginItem.httpRealm, + loginItem.encryptedUsername, + loginItem.encryptedPassword, + loginItem.usernameField, + loginItem.passwordField + ); + // set nsILoginMetaInfo values + login.QueryInterface(Ci.nsILoginMetaInfo); + login.guid = loginItem.guid; + login.timeCreated = loginItem.timeCreated; + login.timeLastUsed = loginItem.timeLastUsed; + login.timePasswordChanged = loginItem.timePasswordChanged; + login.timesUsed = loginItem.timesUsed; + + // Any unknown fields along for the ride + login.unknownFields = loginItem.encryptedUnknownFields; + foundLogins.push(login); + foundIds.push(loginItem.id); + } + } + + this.log( + `Returning ${foundLogins.length} logins for specified origin with options ${aOptions}` + ); + return [foundLogins, foundIds]; + } + + /** + * Removes all logins from local storage, including FxA Sync key. + * + * NOTE: You probably want removeAllUserFacingLogins instead of this function. + * + */ + 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", []); + } + + /** + * 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 + */ + removeAllUserFacingLogins() { + 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 && + login.httpRealm == lazy.FXA_PWDMGR_REALM + ); + if (fxaKey) { + this._store.data.logins = [fxaKey]; + allLogins = allLogins.filter(item => item != fxaKey); + } else { + this._store.data.logins = []; + } + + this._store.data.potentiallyVulnerablePasswords = []; + this.__decryptedPotentiallyVulnerablePasswords = null; + this._store.data.dismissedBreachAlertsByLoginGUID = {}; + this._store.saveSoon(); + + lazy.LoginHelper.notifyStorageChanged("removeAllLogins", allLogins); + } + + findLogins(origin, formActionOrigin, httpRealm) { + this._store.ensureDataReady(); + + let loginData = { + origin, + formActionOrigin, + httpRealm, + }; + let matchData = {}; + for (let field of ["origin", "formActionOrigin", "httpRealm"]) { + if (loginData[field] != "") { + matchData[field] = loginData[field]; + } + } + let [logins] = this._searchLogins(matchData); + + // Decrypt entries found for the caller. + logins = this._decryptLogins(logins); + + this.log(`Returning ${logins.length} logins.`); + return logins; + } + + countLogins(origin, formActionOrigin, httpRealm) { + this._store.ensureDataReady(); + + let loginData = { + origin, + formActionOrigin, + httpRealm, + }; + let matchData = {}; + for (let field of ["origin", "formActionOrigin", "httpRealm"]) { + if (loginData[field] != "") { + matchData[field] = loginData[field]; + } + } + let [logins] = this._searchLogins(matchData); + + this.log(`Counted ${logins.length} logins.`); + 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 + ); + } + + clearAllPotentiallyVulnerablePasswords() { + this._store.ensureDataReady(); + if (!this._store.data.potentiallyVulnerablePasswords.length) { + // No need to write to disk + return; + } + this._store.data.potentiallyVulnerablePasswords = []; + this._store.saveSoon(); + this.__decryptedPotentiallyVulnerablePasswords = null; + } + + get uiBusy() { + return this._crypto.uiBusy; + } + + get isLoggedIn() { + return this._crypto.isLoggedIn; + } + + /** + * Returns an array with two items: [id, login]. If the login was not + * found, both items will be null. The returned login contains the actual + * stored login (useful for looking at the actual nsILoginMetaInfo values). + */ + _getIdForLogin(login) { + this._store.ensureDataReady(); + + let matchData = {}; + for (let field of ["origin", "formActionOrigin", "httpRealm"]) { + if (login[field] != "") { + matchData[field] = login[field]; + } + } + let [logins, ids] = this._searchLogins(matchData); + + let id = null; + let foundLogin = null; + + // The specified login isn't encrypted, so we need to ensure + // the logins we're comparing with are decrypted. We decrypt one entry + // at a time, lest _decryptLogins return fewer entries and screw up + // indices between the two. + for (let i = 0; i < logins.length; i++) { + let [decryptedLogin] = this._decryptLogins([logins[i]]); + + if (!decryptedLogin || !decryptedLogin.equals(login)) { + continue; + } + + // We've found a match, set id and break + foundLogin = decryptedLogin; + id = ids[i]; + break; + } + + return [id, foundLogin]; + } + + /** + * Checks to see if the specified GUID already exists. + */ + _isGuidUnique(guid) { + this._store.ensureDataReady(); + + return this._store.data.logins.every(l => l.guid != guid); + } + + /** + * Returns the encrypted username, password, and encrypton type for the specified + * login. Can throw if the user cancels a primary password entry. + */ + _encryptLogin(login) { + let encUsername = this._crypto.encrypt(login.username); + let encPassword = this._crypto.encrypt(login.password); + + // Unknown fields should be encrypted since we can't know whether new fields + // from other clients will contain sensitive data or not + let encUnknownFields = null; + if (login.unknownFields) { + encUnknownFields = this._crypto.encrypt(login.unknownFields); + } + let encType = this._crypto.defaultEncType; + + return [encUsername, encPassword, encType, encUnknownFields]; + } + + /** + * Decrypts username and password fields in the provided array of + * logins. + * + * The entries specified by the array will be decrypted, if possible. + * An array of successfully decrypted logins will be returned. The return + * value should be given to external callers (since still-encrypted + * entries are useless), whereas internal callers generally don't want + * to lose unencrypted entries (eg, because the user clicked Cancel + * instead of entering their primary password) + */ + _decryptLogins(logins) { + let result = []; + + for (let login of logins) { + try { + login.username = this._crypto.decrypt(login.username); + login.password = this._crypto.decrypt(login.password); + // Verify unknownFields actually has a value + if (login.unknownFields) { + login.unknownFields = this._crypto.decrypt(login.unknownFields); + } + } catch (e) { + // If decryption failed (corrupt entry?), just skip it. + // Rethrow other errors (like canceling entry of a primary pw) + if (e.result == Cr.NS_ERROR_FAILURE) { + continue; + } + throw e; + } + result.push(login); + } + + return result; + } +} + +XPCOMUtils.defineLazyGetter(LoginManagerStorage_json.prototype, "log", () => { + let logger = lazy.LoginHelper.createLogger("Login storage"); + return logger.log.bind(logger); +}); |