diff options
Diffstat (limited to 'toolkit/components/passwordmgr/nsLoginManager.js')
-rw-r--r-- | toolkit/components/passwordmgr/nsLoginManager.js | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/nsLoginManager.js b/toolkit/components/passwordmgr/nsLoginManager.js new file mode 100644 index 000000000..514351fa5 --- /dev/null +++ b/toolkit/components/passwordmgr/nsLoginManager.js @@ -0,0 +1,541 @@ +/* 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/. */ + +"use strict"; + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +const PERMISSION_SAVE_LOGINS = "login-saving"; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://gre/modules/LoginManagerContent.jsm"); /* global UserAutoCompleteResult */ + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory", + "resource://gre/modules/LoginManagerContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils", + "resource://gre/modules/InsecurePasswordUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let logger = LoginHelper.createLogger("nsLoginManager"); + return logger; +}); + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +function LoginManager() { + this.init(); +} + +LoginManager.prototype = { + + classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManager, + Ci.nsISupportsWeakReference, + Ci.nsIInterfaceRequestor]), + getInterface(aIID) { + if (aIID.equals(Ci.mozIStorageConnection) && this._storage) { + let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor); + return ir.getInterface(aIID); + } + + if (aIID.equals(Ci.nsIVariant)) { + // Allows unwrapping the JavaScript object for regression tests. + return this; + } + + throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE); + }, + + + /* ---------- private members ---------- */ + + + __formFillService: null, // FormFillController, for username autocompleting + get _formFillService() { + if (!this.__formFillService) { + this.__formFillService = Cc["@mozilla.org/satchel/form-fill-controller;1"]. + getService(Ci.nsIFormFillController); + } + return this.__formFillService; + }, + + + _storage: null, // Storage component which contains the saved logins + _prefBranch: null, // Preferences service + _remember: true, // mirrors signon.rememberSignons preference + + + /** + * Initialize the Login Manager. Automatically called when service + * is created. + * + * Note: Service created in /browser/base/content/browser.js, + * delayedStartup() + */ + init() { + + // Cache references to current |this| in utility objects + this._observer._pwmgr = this; + + // Preferences. Add observer so we get notified of changes. + this._prefBranch = Services.prefs.getBranch("signon."); + this._prefBranch.addObserver("rememberSignons", this._observer, false); + + this._remember = this._prefBranch.getBoolPref("rememberSignons"); + this._autoCompleteLookupPromise = null; + + // Form submit observer checks forms for new logins and pw changes. + Services.obs.addObserver(this._observer, "xpcom-shutdown", false); + + if (Services.appinfo.processType === + Services.appinfo.PROCESS_TYPE_DEFAULT) { + Services.obs.addObserver(this._observer, "passwordmgr-storage-replace", + false); + + // Initialize storage so that asynchronous data loading can start. + this._initStorage(); + } + + Services.obs.addObserver(this._observer, "gather-telemetry", false); + }, + + + _initStorage() { + let contractID; + if (AppConstants.platform == "android") { + contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; + } else { + contractID = "@mozilla.org/login-manager/storage/json;1"; + } + try { + let catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + contractID = catMan.getCategoryEntry("login-manager-storage", + "nsILoginManagerStorage"); + log.debug("Found alternate nsILoginManagerStorage with contract ID:", contractID); + } catch (e) { + log.debug("No alternate nsILoginManagerStorage registered"); + } + + this._storage = Cc[contractID]. + createInstance(Ci.nsILoginManagerStorage); + this.initializationPromise = this._storage.initialize(); + }, + + + /* ---------- Utility objects ---------- */ + + + /** + * Internal utility object, implements the nsIObserver interface. + * Used to receive notification for: form submission, preference changes. + */ + _observer: { + _pwmgr: null, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + // nsIObserver + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + var prefName = data; + log.debug("got change to", prefName, "preference"); + + if (prefName == "rememberSignons") { + this._pwmgr._remember = + this._pwmgr._prefBranch.getBoolPref("rememberSignons"); + } else { + log.debug("Oops! Pref not handled, change ignored."); + } + } else if (topic == "xpcom-shutdown") { + delete this._pwmgr.__formFillService; + delete this._pwmgr._storage; + delete this._pwmgr._prefBranch; + this._pwmgr = null; + } else if (topic == "passwordmgr-storage-replace") { + Task.spawn(function* () { + yield this._pwmgr._storage.terminate(); + this._pwmgr._initStorage(); + yield this._pwmgr.initializationPromise; + Services.obs.notifyObservers(null, + "passwordmgr-storage-replace-complete", null); + }.bind(this)); + } else if (topic == "gather-telemetry") { + // When testing, the "data" parameter is a string containing the + // reference time in milliseconds for time-based statistics. + this._pwmgr._gatherTelemetry(data ? parseInt(data) + : new Date().getTime()); + } else { + log.debug("Oops! Unexpected notification:", topic); + } + } + }, + + /** + * Collects statistics about the current logins and settings. The telemetry + * histograms used here are not accumulated, but are reset each time this + * function is called, since it can be called multiple times in a session. + * + * This function might also not be called at all in the current session. + * + * @param referenceTimeMs + * Current time used to calculate time-based statistics, expressed as + * the number of milliseconds since January 1, 1970, 00:00:00 UTC. + * This is set to a fake value during unit testing. + */ + _gatherTelemetry(referenceTimeMs) { + function clearAndGetHistogram(histogramId) { + let histogram = Services.telemetry.getHistogramById(histogramId); + histogram.clear(); + return histogram; + } + + clearAndGetHistogram("PWMGR_BLOCKLIST_NUM_SITES").add( + this.getAllDisabledHosts({}).length + ); + clearAndGetHistogram("PWMGR_NUM_SAVED_PASSWORDS").add( + this.countLogins("", "", "") + ); + clearAndGetHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS").add( + this.countLogins("", null, "") + ); + + // This is a boolean histogram, and not a flag, because we don't want to + // record any value if _gatherTelemetry is not called. + clearAndGetHistogram("PWMGR_SAVING_ENABLED").add(this._remember); + + // Don't try to get logins if MP is enabled, since we don't want to show a MP prompt. + if (!this.isLoggedIn) { + return; + } + + let logins = this.getAllLogins({}); + + let usernamePresentHistogram = clearAndGetHistogram("PWMGR_USERNAME_PRESENT"); + let loginLastUsedDaysHistogram = clearAndGetHistogram("PWMGR_LOGIN_LAST_USED_DAYS"); + + let hostnameCount = new Map(); + for (let login of logins) { + usernamePresentHistogram.add(!!login.username); + + let hostname = login.hostname; + hostnameCount.set(hostname, (hostnameCount.get(hostname) || 0 ) + 1); + + login.QueryInterface(Ci.nsILoginMetaInfo); + let timeLastUsedAgeMs = referenceTimeMs - login.timeLastUsed; + if (timeLastUsedAgeMs > 0) { + loginLastUsedDaysHistogram.add( + Math.floor(timeLastUsedAgeMs / MS_PER_DAY) + ); + } + } + + let passwordsCountHistogram = clearAndGetHistogram("PWMGR_NUM_PASSWORDS_PER_HOSTNAME"); + for (let count of hostnameCount.values()) { + passwordsCountHistogram.add(count); + } + }, + + + + + + /* ---------- Primary Public interfaces ---------- */ + + + + + /** + * @type Promise + * This promise is resolved when initialization is complete, and is rejected + * in case the asynchronous part of initialization failed. + */ + initializationPromise: null, + + + /** + * Add a new login to login storage. + */ + addLogin(login) { + // Sanity check the login + if (login.hostname == null || login.hostname.length == 0) { + throw new Error("Can't add a login with a null or empty hostname."); + } + + // For logins w/o a username, set to "", not null. + if (login.username == null) { + throw new Error("Can't add a login with a null username."); + } + + if (login.password == null || login.password.length == 0) { + throw new Error("Can't add a login with a null or empty password."); + } + + if (login.formSubmitURL || login.formSubmitURL == "") { + // We have a form submit URL. Can't have a HTTP realm. + if (login.httpRealm != null) { + throw new Error("Can't add a login with both a httpRealm and formSubmitURL."); + } + } else if (login.httpRealm) { + // We have a HTTP realm. Can't have a form submit URL. + if (login.formSubmitURL != null) { + throw new Error("Can't add a login with both a httpRealm and formSubmitURL."); + } + } else { + // Need one or the other! + throw new Error("Can't add a login without a httpRealm or formSubmitURL."); + } + + + // Look for an existing entry. + var logins = this.findLogins({}, login.hostname, login.formSubmitURL, + login.httpRealm); + + if (logins.some(l => login.matches(l, true))) { + throw new Error("This login already exists."); + } + + log.debug("Adding login"); + return this._storage.addLogin(login); + }, + + /** + * Remove the specified login from the stored logins. + */ + removeLogin(login) { + log.debug("Removing login"); + return this._storage.removeLogin(login); + }, + + + /** + * Change the specified login to match the new login. + */ + modifyLogin(oldLogin, newLogin) { + log.debug("Modifying login"); + return this._storage.modifyLogin(oldLogin, newLogin); + }, + + + /** + * Get a dump of all stored logins. Used by the login manager UI. + * + * @param count - only needed for XPCOM. + * @return {nsILoginInfo[]} - If there are no logins, the array is empty. + */ + getAllLogins(count) { + log.debug("Getting a list of all logins"); + return this._storage.getAllLogins(count); + }, + + + /** + * Remove all stored logins. + */ + removeAllLogins() { + log.debug("Removing all logins"); + this._storage.removeAllLogins(); + }, + + /** + * Get a list of all origins for which logins are disabled. + * + * @param {Number} count - only needed for XPCOM. + * + * @return {String[]} of disabled origins. If there are no disabled origins, + * the array is empty. + */ + getAllDisabledHosts(count) { + log.debug("Getting a list of all disabled origins"); + + let disabledHosts = []; + let enumerator = Services.perms.enumerator; + + while (enumerator.hasMoreElements()) { + let perm = enumerator.getNext(); + if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) { + disabledHosts.push(perm.principal.URI.prePath); + } + } + + if (count) + count.value = disabledHosts.length; // needed for XPCOM + + log.debug("getAllDisabledHosts: returning", disabledHosts.length, "disabled hosts."); + return disabledHosts; + }, + + + /** + * Search for the known logins for entries matching the specified criteria. + */ + findLogins(count, origin, formActionOrigin, httpRealm) { + log.debug("Searching for logins matching origin:", origin, + "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm); + + return this._storage.findLogins(count, origin, formActionOrigin, + httpRealm); + }, + + + /** + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a + * JavaScript object and decrypt the results. + * + * @return {nsILoginInfo[]} which are decrypted. + */ + searchLogins(count, matchData) { + log.debug("Searching for logins"); + + matchData.QueryInterface(Ci.nsIPropertyBag2); + if (!matchData.hasKey("guid")) { + if (!matchData.hasKey("hostname")) { + log.warn("searchLogins: A `hostname` is recommended"); + } + + if (!matchData.hasKey("formSubmitURL") && !matchData.hasKey("httpRealm")) { + log.warn("searchLogins: `formSubmitURL` or `httpRealm` is recommended"); + } + } + + return this._storage.searchLogins(count, matchData); + }, + + + /** + * Search for the known logins for entries matching the specified criteria, + * returns only the count. + */ + countLogins(origin, formActionOrigin, httpRealm) { + log.debug("Counting logins matching origin:", origin, + "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm); + + return this._storage.countLogins(origin, formActionOrigin, httpRealm); + }, + + + get uiBusy() { + return this._storage.uiBusy; + }, + + + get isLoggedIn() { + return this._storage.isLoggedIn; + }, + + + /** + * Check to see if user has disabled saving logins for the origin. + */ + getLoginSavingEnabled(origin) { + log.debug("Checking if logins to", origin, "can be saved."); + if (!this._remember) { + return false; + } + + let uri = Services.io.newURI(origin, null, null); + return Services.perms.testPermission(uri, PERMISSION_SAVE_LOGINS) != Services.perms.DENY_ACTION; + }, + + + /** + * Enable or disable storing logins for the specified origin. + */ + setLoginSavingEnabled(origin, enabled) { + // Throws if there are bogus values. + LoginHelper.checkHostnameValue(origin); + + let uri = Services.io.newURI(origin, null, null); + if (enabled) { + Services.perms.remove(uri, PERMISSION_SAVE_LOGINS); + } else { + Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION); + } + + log.debug("Login saving for", origin, "now enabled?", enabled); + LoginHelper.notifyStorageChanged(enabled ? "hostSavingEnabled" : "hostSavingDisabled", origin); + }, + + /** + * Yuck. This is called directly by satchel: + * nsFormFillController::StartSearch() + * [toolkit/components/satchel/nsFormFillController.cpp] + * + * We really ought to have a simple way for code to register an + * auto-complete provider, and not have satchel calling pwmgr directly. + */ + autoCompleteSearchAsync(aSearchString, aPreviousResult, + aElement, aCallback) { + // aPreviousResult is an nsIAutoCompleteResult, aElement is + // nsIDOMHTMLInputElement + + let form = LoginFormFactory.createFromField(aElement); + let isSecure = InsecurePasswordUtils.isFormSecure(form); + let isPasswordField = aElement.type == "password"; + + let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => { + // If the search was canceled before we got our + // results, don't bother reporting them. + if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) { + return; + } + + this._autoCompleteLookupPromise = null; + let results = new UserAutoCompleteResult(aSearchString, logins, { + messageManager, + isSecure, + isPasswordField, + }); + aCallback.onSearchCompletion(results); + }; + + if (isPasswordField && aSearchString) { + // Return empty result on password fields with password already filled. + let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); + return; + } + + if (!this._remember) { + let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); + return; + } + + log.debug("AutoCompleteSearch invoked. Search is:", aSearchString); + + let previousResult; + if (aPreviousResult) { + previousResult = { searchString: aPreviousResult.searchString, + logins: aPreviousResult.wrappedJSObject.logins }; + } else { + previousResult = null; + } + + let rect = BrowserUtils.getElementBoundingScreenRect(aElement); + let acLookupPromise = this._autoCompleteLookupPromise = + LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult, + aElement, rect); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)) + .then(null, Cu.reportError); + }, + + stopSearch() { + this._autoCompleteLookupPromise = null; + }, +}; // end of LoginManager implementation + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]); |