summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/tests/unit/test_oskeystore.js
diff options
context:
space:
mode:
Diffstat (limited to 'security/manager/ssl/tests/unit/test_oskeystore.js')
-rw-r--r--security/manager/ssl/tests/unit/test_oskeystore.js413
1 files changed, 413 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/unit/test_oskeystore.js b/security/manager/ssl/tests/unit/test_oskeystore.js
new file mode 100644
index 0000000000..a5ef0f9172
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_oskeystore.js
@@ -0,0 +1,413 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+"use strict";
+
+// Tests the methods and attributes for interfacing with nsIOSKeyStore.
+
+// Ensure that the appropriate initialization has happened.
+do_get_profile();
+
+const LABELS = ["mylabel1", "mylabel2", "mylabel3"];
+
+async function delete_all_secrets() {
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+ for (let label of LABELS) {
+ if (await keystore.asyncSecretAvailable(label)) {
+ await keystore.asyncDeleteSecret(label);
+ ok(
+ !(await keystore.asyncSecretAvailable(label)),
+ label + " should be deleted now."
+ );
+ }
+ }
+}
+
+// Test that Firefox handles locking and unlocking of the OSKeyStore properly.
+// Does so by mocking out the actual dialog and "filling in" the
+// password. Also tests that providing an incorrect password will fail (well,
+// technically the user will just get prompted again, but if they then cancel
+// the dialog the overall operation will fail).
+
+var gMockPrompter = {
+ passwordToTry: null,
+ numPrompts: 0,
+
+ // This intentionally does not use arrow function syntax to avoid an issue
+ // where in the context of the arrow function, |this != gMockPrompter| due to
+ // how objects get wrapped when going across xpcom boundaries.
+ promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
+ this.numPrompts++;
+ equal(
+ text,
+ "Please enter your Primary Password.",
+ "password prompt text should be as expected"
+ );
+ equal(checkMsg, null, "checkMsg should be null");
+ ok(this.passwordToTry, "passwordToTry should be non-null");
+ if (this.passwordToTry == "DontTryThisPassword") {
+ // Cancel the prompt in this case.
+ return false;
+ }
+ password.value = this.passwordToTry;
+ return true;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
+};
+
+// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
+// to call promptPassword. We return the mock one, above.
+var gWindowWatcher = {
+ getNewPrompter: () => gMockPrompter,
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]),
+};
+
+async function encrypt_decrypt_test() {
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+ ok(
+ !(await keystore.asyncSecretAvailable(LABELS[0])),
+ "The secret should not be available yet."
+ );
+
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(recoveryPhrase, "A recovery phrase should've been created.");
+ let recoveryPhrase2 = await keystore.asyncGenerateSecret(LABELS[1]);
+ ok(recoveryPhrase2, "A recovery phrase should've been created.");
+
+ let text = new Uint8Array([0x01, 0x00, 0x01]);
+ let ciphertext = "";
+ try {
+ ciphertext = await keystore.asyncEncryptBytes(LABELS[0], text);
+ ok(ciphertext, "We should have a ciphertext now.");
+ } catch (e) {
+ ok(false, "Error encrypting " + e);
+ }
+
+ // Decrypting should give us the plaintext bytes again.
+ try {
+ let plaintext = await keystore.asyncDecryptBytes(LABELS[0], ciphertext);
+ ok(
+ plaintext.toString() == text.toString(),
+ "Decrypted plaintext should be the same as text."
+ );
+ } catch (e) {
+ ok(false, "Error decrypting ciphertext " + e);
+ }
+
+ // Decrypting with a wrong key should throw an error.
+ try {
+ await keystore.asyncDecryptBytes(LABELS[1], ciphertext);
+ ok(false, "Decrypting with the wrong key should fail.");
+ } catch (e) {
+ ok(true, "Decrypting with the wrong key should fail " + e);
+ }
+}
+
+add_task(async function() {
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+ let windowWatcherCID;
+ if (keystore.isNSSKeyStore) {
+ windowWatcherCID = MockRegistrar.register(
+ "@mozilla.org/embedcomp/window-watcher;1",
+ gWindowWatcher
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(windowWatcherCID);
+ });
+ }
+
+ await delete_all_secrets();
+ await encrypt_decrypt_test();
+ await delete_all_secrets();
+
+ if (
+ AppConstants.platform == "macosx" ||
+ AppConstants.platform == "win" ||
+ AppConstants.platform == "linux"
+ ) {
+ ok(
+ !keystore.isNSSKeyStore,
+ "OS X, Windows, and Linux should use the non-NSS implementation"
+ );
+ }
+
+ if (keystore.isNSSKeyStore) {
+ // If we use the NSS key store implementation test that everything works
+ // when a master password is set.
+ // Set an initial password.
+ let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
+ Ci.nsIPK11TokenDB
+ );
+ let token = tokenDB.getInternalKeyToken();
+ token.initPassword("hunter2");
+
+ // Lock the key store. This should be equivalent to token.logoutSimple()
+ await keystore.asyncLock();
+
+ // Set the correct password so that the test operations should succeed.
+ gMockPrompter.passwordToTry = "hunter2";
+ await encrypt_decrypt_test();
+ ok(
+ gMockPrompter.numPrompts == 1,
+ "There should've been one password prompt."
+ );
+ await delete_all_secrets();
+ }
+
+ // Check lock/unlock behaviour.
+ // Unfortunately we can only test this automatically for the NSS key store.
+ // Uncomment the outer keystore.isNSSKeyStore to test other key stores manually.
+ if (keystore.isNSSKeyStore) {
+ await delete_all_secrets();
+ await encrypt_decrypt_test();
+ await keystore.asyncLock();
+ info("Keystore should be locked. Cancel the login request.");
+ try {
+ if (keystore.isNSSKeyStore) {
+ gMockPrompter.passwordToTry = "DontTryThisPassword";
+ }
+ await keystore.asyncUnlock();
+ ok(false, "Unlock should've rejected.");
+ } catch (e) {
+ ok(
+ e.result == Cr.NS_ERROR_FAILURE || e.result == Cr.NS_ERROR_ABORT,
+ "Rejected login prompt."
+ );
+ }
+ // clean up
+ if (keystore.isNSSKeyStore) {
+ gMockPrompter.passwordToTry = "hunter2";
+ }
+ await delete_all_secrets();
+ }
+});
+
+// Test that if we kick off a background operation and then call a synchronous function on the
+// keystore, we don't deadlock.
+add_task(async function() {
+ await delete_all_secrets();
+
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(recoveryPhrase, "A recovery phrase should've been created.");
+
+ try {
+ let text = new Uint8Array(8192);
+ let promise = keystore.asyncEncryptBytes(LABELS[0], text);
+ /* eslint-disable no-unused-expressions */
+ keystore.isNSSKeyStore; // we don't care what this is - we just need to access it
+ /* eslint-enable no-unused-expressions */
+ let ciphertext = await promise;
+ ok(ciphertext, "We should have a ciphertext now.");
+ } catch (e) {
+ ok(false, "Error encrypting " + e);
+ }
+
+ await delete_all_secrets();
+});
+
+// Test that using a recovery phrase works.
+add_task(async function() {
+ await delete_all_secrets();
+
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(recoveryPhrase, "A recovery phrase should've been created.");
+
+ let text = new Uint8Array([0x01, 0x00, 0x01]);
+ let ciphertext = await keystore.asyncEncryptBytes(LABELS[0], text);
+ ok(ciphertext, "We should have a ciphertext now.");
+
+ await keystore.asyncDeleteSecret(LABELS[0]);
+ // Decrypting should fail after deleting the secret.
+ await keystore
+ .asyncDecryptBytes(LABELS[0], ciphertext)
+ .then(() =>
+ ok(false, "decrypting didn't throw as expected after deleting the secret")
+ )
+ .catch(() =>
+ ok(true, "decrypting threw as expected after deleting the secret")
+ );
+
+ await keystore.asyncRecoverSecret(LABELS[0], recoveryPhrase);
+ let plaintext = await keystore.asyncDecryptBytes(LABELS[0], ciphertext);
+ ok(
+ plaintext.toString() == text.toString(),
+ "Decrypted plaintext should be the same as text."
+ );
+
+ await delete_all_secrets();
+});
+
+// Test that trying to use a non-base64 recovery phrase fails.
+add_task(async function() {
+ await delete_all_secrets();
+
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+ await keystore
+ .asyncRecoverSecret(LABELS[0], "@##$^&*()#$^&*(@#%&*_")
+ .then(() =>
+ ok(false, "base64-decoding non-base64 should have failed but didn't")
+ )
+ .catch(() => ok(true, "base64-decoding non-base64 failed as expected"));
+
+ ok(
+ !(await keystore.asyncSecretAvailable(LABELS[0])),
+ "we didn't recover a secret, so the secret shouldn't be available"
+ );
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(
+ recoveryPhrase && !!recoveryPhrase.length,
+ "we should be able to re-use that label to generate a new secret"
+ );
+ await delete_all_secrets();
+});
+
+// Test that re-using a label overwrites any previously-stored secret.
+add_task(async function() {
+ await delete_all_secrets();
+
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(recoveryPhrase, "A recovery phrase should've been created.");
+
+ let text = new Uint8Array([0x66, 0x6f, 0x6f, 0x66]);
+ let ciphertext = await keystore.asyncEncryptBytes(LABELS[0], text);
+ ok(ciphertext, "We should have a ciphertext now.");
+
+ let newRecoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(newRecoveryPhrase, "A new recovery phrase should've been created.");
+
+ // The new secret replaced the old one so we shouldn't be able to decrypt the ciphertext now.
+ await keystore
+ .asyncDecryptBytes(LABELS[0], ciphertext)
+ .then(() =>
+ ok(false, "decrypting without the original key should have failed")
+ )
+ .catch(() =>
+ ok(true, "decrypting without the original key failed as expected")
+ );
+
+ await keystore.asyncRecoverSecret(LABELS[0], recoveryPhrase);
+ let plaintext = await keystore.asyncDecryptBytes(LABELS[0], ciphertext);
+ ok(
+ plaintext.toString() == text.toString(),
+ "Decrypted plaintext should be the same as text (once we have the original key again)."
+ );
+
+ await delete_all_secrets();
+});
+
+// Test that re-using a label (this time using a recovery phrase) overwrites any previously-stored
+// secret.
+add_task(async function() {
+ await delete_all_secrets();
+
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(recoveryPhrase, "A recovery phrase should've been created.");
+
+ let newRecoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(newRecoveryPhrase, "A new recovery phrase should've been created.");
+
+ let text = new Uint8Array([0x66, 0x6f, 0x6f, 0x66]);
+ let ciphertext = await keystore.asyncEncryptBytes(LABELS[0], text);
+ ok(ciphertext, "We should have a ciphertext now.");
+
+ await keystore.asyncRecoverSecret(LABELS[0], recoveryPhrase);
+
+ // We recovered the old secret, so decrypting ciphertext that had been encrypted with the newer
+ // key should fail.
+ await keystore
+ .asyncDecryptBytes(LABELS[0], ciphertext)
+ .then(() => ok(false, "decrypting without the new key should have failed"))
+ .catch(() => ok(true, "decrypting without the new key failed as expected"));
+
+ await keystore.asyncRecoverSecret(LABELS[0], newRecoveryPhrase);
+ let plaintext = await keystore.asyncDecryptBytes(LABELS[0], ciphertext);
+ ok(
+ plaintext.toString() == text.toString(),
+ "Decrypted plaintext should be the same as text (once we have the new key again)."
+ );
+
+ await delete_all_secrets();
+});
+
+// Test that trying to use recovery phrases that are the wrong size fails.
+add_task(async function() {
+ await delete_all_secrets();
+
+ let keystore = Cc["@mozilla.org/security/oskeystore;1"].getService(
+ Ci.nsIOSKeyStore
+ );
+
+ await keystore
+ .asyncRecoverSecret(LABELS[0], "")
+ .then(() => ok(false, "'recovering' with an empty key should have failed"))
+ .catch(() => ok(true, "'recovering' with an empty key failed as expected"));
+ ok(
+ !(await keystore.asyncSecretAvailable(LABELS[0])),
+ "we didn't recover a secret, so the secret shouldn't be available"
+ );
+
+ await keystore
+ .asyncRecoverSecret(LABELS[0], "AAAAAA")
+ .then(() =>
+ ok(false, "recovering with a key that is too short should have failed")
+ )
+ .catch(() =>
+ ok(true, "recovering with a key that is too short failed as expected")
+ );
+ ok(
+ !(await keystore.asyncSecretAvailable(LABELS[0])),
+ "we didn't recover a secret, so the secret shouldn't be available"
+ );
+
+ await keystore
+ .asyncRecoverSecret(
+ LABELS[0],
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ )
+ .then(() =>
+ ok(false, "recovering with a key that is too long should have failed")
+ )
+ .catch(() =>
+ ok(true, "recovering with a key that is too long failed as expected")
+ );
+ ok(
+ !(await keystore.asyncSecretAvailable(LABELS[0])),
+ "we didn't recover a secret, so the secret shouldn't be available"
+ );
+
+ let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
+ ok(
+ recoveryPhrase && !!recoveryPhrase.length,
+ "we should be able to use that label to generate a new secret"
+ );
+ ok(
+ await keystore.asyncSecretAvailable(LABELS[0]),
+ "the generated secret should now be available"
+ );
+
+ await delete_all_secrets();
+});