summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/storage-json.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/storage-json.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/storage-json.sys.mjs879
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);
+});