/* 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/. */ /** * Helpers for the Master Password Dialog. * In the future the Master Password implementation may move here. */ "use strict"; this.EXPORTED_SYMBOLS = [ "MasterPassword", ]; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR", "@mozilla.org/login-manager/crypto/SDR;1", Ci.nsILoginManagerCrypto); this.MasterPassword = { get _token() { let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB); return tokendb.getInternalKeyToken(); }, /** * @returns {boolean} True if a master password is set and false otherwise. */ get isEnabled() { return this._token.hasPassword; }, /** * Display the master password login prompt no matter it's logged in or not. * If an existing MP prompt is already open, the result from it will be used instead. * * @returns {Promise} True if it's logged in or no password is set and false * if it's still not logged in (prompt canceled or other error). */ async prompt() { if (!this.isEnabled) { return true; } // If a prompt is already showing then wait for and focus it. if (Services.logins.uiBusy) { return this.waitForExistingDialog(); } let token = this._token; try { // 'true' means always prompt for token password. User will be prompted until // clicking 'Cancel' or entering the correct password. token.login(true); } catch (e) { // An exception will be thrown if the user cancels the login prompt dialog. // User is also logged out. } // If we triggered a master password prompt, notify observers. if (token.isLoggedIn()) { Services.obs.notifyObservers(null, "passwordmgr-crypto-login"); } else { Services.obs.notifyObservers(null, "passwordmgr-crypto-loginCanceled"); } return token.isLoggedIn(); }, /** * Decrypts cipherText. * * @param {string} cipherText Encrypted string including the algorithm details. * @param {boolean} reauth True if we want to force the prompt to show up * even if the user is already logged in. * @returns {Promise} resolves to the decrypted string, or rejects otherwise. */ async decrypt(cipherText, reauth = false) { let loggedIn = false; if (reauth) { loggedIn = await this.prompt(); } else { loggedIn = await this.waitForExistingDialog(); } if (!loggedIn) { throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT); } return cryptoSDR.decrypt(cipherText); }, /** * Encrypts a string and returns cipher text containing algorithm information used for decryption. * * @param {string} plainText Original string without encryption. * @returns {Promise} resolves to the encrypted string (with algorithm), otherwise rejects. */ async encrypt(plainText) { if (Services.logins.uiBusy && !await this.waitForExistingDialog()) { throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT); } return cryptoSDR.encrypt(plainText); }, /** * Resolve when master password dialogs are closed, immediately if none are open. * * An existing MP dialog will be focused and will request attention. * * @returns {Promise} * Resolves with whether the user is logged in to MP. */ async waitForExistingDialog() { if (!Services.logins.uiBusy) { log.debug("waitForExistingDialog: Dialog isn't showing. isLoggedIn:", Services.logins.isLoggedIn); return Services.logins.isLoggedIn; } return new Promise((resolve) => { log.debug("waitForExistingDialog: Observing the open dialog"); let observer = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver, Ci.nsISupportsWeakReference, ]), observe(subject, topic, data) { log.debug("waitForExistingDialog: Got notification:", topic); // Only run observer once. Services.obs.removeObserver(this, "passwordmgr-crypto-login"); Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); if (topic == "passwordmgr-crypto-loginCanceled") { resolve(false); return; } resolve(true); }, }; // Possible leak: it's possible that neither of these notifications // will fire, and if that happens, we'll leak the observer (and // never return). We should guarantee that at least one of these // will fire. // See bug XXX. Services.obs.addObserver(observer, "passwordmgr-crypto-login"); Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled"); // Focus and draw attention to the existing master password dialog for the // occassions where it's not attached to the current window. let promptWin = Services.wm.getMostRecentWindow("prompt:promptPassword"); promptWin.focus(); promptWin.getAttention(); }); }, }; XPCOMUtils.defineLazyGetter(this, "log", () => { let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI; return new ConsoleAPI({ maxLogLevelPref: "masterPassword.loglevel", prefix: "Master Password", }); });