Bug 1388238 - Add encrypt/decrypt methods to MasterPassword.jsm. r=MattN draft
authorsteveck-chung <schung@mozilla.com>
Mon, 07 Aug 2017 20:44:08 -0700
changeset 642997 5c324c252d1ac55ece40dd15ab7275c5d927d75d
parent 642996 713902e23974b19900d9f28352d20686871e4e09
child 725168 77c0113d8b3f61104953736591d7951669f642f9
push id72946
push usermozilla@noorenberghe.ca
push dateWed, 09 Aug 2017 03:56:28 +0000
reviewersMattN
bugs1388238
milestone57.0a1
Bug 1388238 - Add encrypt/decrypt methods to MasterPassword.jsm. r=MattN MozReview-Commit-ID: AHpzYNbnnWv
browser/extensions/formautofill/MasterPassword.jsm
browser/extensions/formautofill/test/unit/test_masterPassword.js
browser/extensions/formautofill/test/unit/xpcshell.ini
--- a/browser/extensions/formautofill/MasterPassword.jsm
+++ b/browser/extensions/formautofill/MasterPassword.jsm
@@ -13,18 +13,107 @@ 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<boolean>} 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<string>} 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<string>} 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<boolean>}
    *          Resolves with whether the user is logged in to MP.
    */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_masterPassword.js
@@ -0,0 +1,62 @@
+/**
+ * Tests of MasterPassword.jsm
+ */
+
+"use strict";
+
+let {MasterPassword} = Cu.import("resource://formautofill/MasterPassword.jsm", {});
+
+const TESTCASES = [{
+  description: "With master password set",
+  masterPassword: "fakemp",
+  mpEnabled: true,
+},
+{
+  description: "Without master password set",
+  masterPassword: "", // "" means no master password
+  mpEnabled: false,
+}];
+
+add_task(async function test_without_init_crypto() {
+  MasterPassword._token.reset();
+  Assert.equal(MasterPassword._token.needsUserInit, true);
+});
+
+TESTCASES.forEach(testcase => {
+  let token = MasterPassword._token;
+
+  add_task(async function test_encrypt_decrypt() {
+    do_print("Starting testcase: " + testcase.description);
+    token.initPassword(testcase.masterPassword);
+
+    // Test only: Force the token login without asking for master password
+    token.login(/* force */ false);
+    Assert.equal(testcase.mpEnabled, token.isLoggedIn(), "Token should now be logged into");
+    Assert.equal(MasterPassword.isEnabled, testcase.mpEnabled);
+
+    let testText = "test string";
+    let cipherText = await MasterPassword.encrypt(testText);
+    Assert.notEqual(testText, cipherText);
+    let plainText = await MasterPassword.decrypt(cipherText);
+    Assert.equal(testText, plainText);
+    if (token.isLoggedIn()) {
+      token.logoutSimple();
+      ok(!token.isLoggedIn(),
+         "Token should be logged out after calling logoutSimple()");
+      try {
+        await MasterPassword.encrypt(testText);
+        throw new Error("Not receiving canceled master password error");
+      } catch (e) {
+        Assert.equal(e.message, "User canceled master password entry");
+      }
+      try {
+        await MasterPassword.decrypt(cipherText);
+        throw new Error("Not receiving canceled master password error");
+      } catch (e) {
+        Assert.equal(e.message, "User canceled master password entry");
+      }
+    }
+
+    token.reset();
+  });
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -26,16 +26,17 @@ support-files =
 [test_getAdaptedProfiles.js]
 [test_getCategoriesFromFieldNames.js]
 [test_getFormInputDetails.js]
 [test_getInfo.js]
 [test_getRecords.js]
 [test_isCJKName.js]
 [test_isFieldEligibleForAutofill.js]
 [test_markAsAutofillField.js]
+[test_masterPassword.js]
 [test_migrateRecords.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
 [test_profileAutocompleteResult.js]
 [test_phoneNumber.js]
 [test_reconcile.js]
 [test_savedFieldNames.js]
 [test_toOneLineAddress.js]