/* 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]);