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