summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/storage-json.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/storage-json.js')
-rw-r--r--toolkit/components/passwordmgr/storage-json.js514
1 files changed, 514 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/storage-json.js b/toolkit/components/passwordmgr/storage-json.js
new file mode 100644
index 000000000..20834d45b
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-json.js
@@ -0,0 +1,514 @@
+/* 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.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
+ "resource://gre/modules/LoginImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+this.LoginManagerStorage_json = function () {};
+
+this.LoginManagerStorage_json.prototype = {
+ classID: Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
+
+ __crypto: null, // nsILoginManagerCrypto service
+ get _crypto() {
+ if (!this.__crypto)
+ this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
+ getService(Ci.nsILoginManagerCrypto);
+ return this.__crypto;
+ },
+
+ initialize() {
+ try {
+ // Force initialization of the crypto module.
+ // See bug 717490 comment 17.
+ this._crypto;
+
+ // Set the reference to LoginStore synchronously.
+ let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "logins.json");
+ this._store = new LoginStore(jsonPath);
+
+ return Task.spawn(function* () {
+ // Load the data asynchronously.
+ this.log("Opening database at", this._store.path);
+ yield this._store.load();
+
+ // The import from previous versions operates the first time
+ // that this built-in storage back-end is used. This may be
+ // later than expected, in case add-ons have registered an
+ // alternate storage that disabled the default one.
+ try {
+ if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
+ return;
+ }
+ } catch (ex) {
+ // If the preference does not exist, we need to import.
+ }
+
+ // Import only happens asynchronously.
+ let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
+ "signons.sqlite");
+ if (yield OS.File.exists(sqlitePath)) {
+ let loginImport = new LoginImport(this._store, sqlitePath);
+ // Failures during import, for example due to a corrupt
+ // file or a schema version that is too old, will not
+ // prevent us from marking the operation as completed.
+ // At the next startup, we will not try the import again.
+ yield loginImport.import().catch(Cu.reportError);
+ this._store.saveSoon();
+ }
+
+ // We won't attempt import again on next startup.
+ Services.prefs.setBoolPref("signon.importedFromSqlite", true);
+ }.bind(this)).catch(Cu.reportError);
+ } catch (e) {
+ this.log("Initialization failed:", e);
+ 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();
+ },
+
+ addLogin(login) {
+ this._store.ensureDataReady();
+
+ // Throws if there are bogus values.
+ LoginHelper.checkLoginValues(login);
+
+ let [encUsername, encPassword, encType] = this._encryptLogin(login);
+
+ // 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) {
+ if (!this._isGuidUnique(loginClone.guid))
+ throw new Error("specified GUID already exists");
+ } else {
+ loginClone.guid = gUUIDGenerator.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.hostname,
+ httpRealm: loginClone.httpRealm,
+ formSubmitURL: loginClone.formSubmitURL,
+ usernameField: loginClone.usernameField,
+ passwordField: loginClone.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: loginClone.guid,
+ encType: encType,
+ timeCreated: loginClone.timeCreated,
+ timeLastUsed: loginClone.timeLastUsed,
+ timePasswordChanged: loginClone.timePasswordChanged,
+ timesUsed: loginClone.timesUsed
+ });
+ this._store.saveSoon();
+
+ // Send a notification that a login was added.
+ 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();
+ }
+
+ 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 = 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.hostname,
+ newLogin.formSubmitURL,
+ newLogin.httpRealm);
+
+ if (logins.some(login => newLogin.matches(login, true)))
+ throw new Error("This login already exists.");
+ }
+
+ // Get the encrypted value of the username and password.
+ let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
+
+ for (let loginItem of this._store.data.logins) {
+ if (loginItem.id == idToModify) {
+ loginItem.hostname = newLogin.hostname;
+ loginItem.httpRealm = newLogin.httpRealm;
+ loginItem.formSubmitURL = newLogin.formSubmitURL;
+ 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;
+ this._store.saveSoon();
+ break;
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
+ },
+
+ /**
+ * @return {nsILoginInfo[]}
+ */
+ getAllLogins(count) {
+ let [logins, ids] = this._searchLogins({});
+
+ // decrypt entries for caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_getAllLogins: returning", logins.length, "logins.");
+ if (count)
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins(count, matchData) {
+ let realMatchData = {};
+ let options = {};
+ // Convert nsIPropertyBag to normal JS object
+ let propEnum = matchData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
+ }
+
+ let [logins, ids] = this._searchLogins(realMatchData, options);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ count.value = logins.length; // needed for XPCOM
+ 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,
+ }) {
+ this._store.ensureDataReady();
+
+ function match(aLogin) {
+ for (let field in matchData) {
+ let wantedValue = matchData[field];
+ switch (field) {
+ case "formSubmitURL":
+ if (wantedValue != null) {
+ // Historical compatibility requires this special case
+ if (aLogin.formSubmitURL == "") {
+ break;
+ }
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
+ case "hostname":
+ if (wantedValue != null) { // needed for formSubmitURL fall through
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
+ // Normal cases.
+ 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 && aLogin[field]) {
+ return false;
+ } else if (aLogin[field] != 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 this._store.data.logins) {
+ 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;
+ foundLogins.push(login);
+ foundIds.push(loginItem.id);
+ }
+ }
+
+ this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData,
+ "with options", aOptions);
+ return [foundLogins, foundIds];
+ },
+
+ /**
+ * Removes all logins from storage.
+ */
+ removeAllLogins() {
+ this._store.ensureDataReady();
+
+ this.log("Removing all logins");
+ this._store.data.logins = [];
+ this._store.saveSoon();
+
+ LoginHelper.notifyStorageChanged("removeAllLogins", null);
+ },
+
+ findLogins(count, hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_findLogins: returning", logins.length, "logins");
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ countLogins(hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ this.log("_countLogins: counted logins:", logins.length);
+ return logins.length;
+ },
+
+ 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) {
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "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 master password entry.
+ */
+ _encryptLogin(login) {
+ let encUsername = this._crypto.encrypt(login.username);
+ let encPassword = this._crypto.encrypt(login.password);
+ let encType = this._crypto.defaultEncType;
+
+ return [encUsername, encPassword, encType];
+ },
+
+ /**
+ * 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 master 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);
+ } catch (e) {
+ // If decryption failed (corrupt entry?), just skip it.
+ // Rethrow other errors (like canceling entry of a master pw)
+ if (e.result == Cr.NS_ERROR_FAILURE)
+ continue;
+ throw e;
+ }
+ result.push(login);
+ }
+
+ return result;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_json.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);