summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs185
1 files changed, 185 insertions, 0 deletions
diff --git a/browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs b/browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs
new file mode 100644
index 0000000000..595bbc28c4
--- /dev/null
+++ b/browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs
@@ -0,0 +1,185 @@
+/* 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/. */
+
+/**
+ * Class to handle encryption and decryption of logins stored in Chrome/Chromium
+ * on macOS.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gKeychainUtils",
+ "@mozilla.org/profile/migrator/keychainmigrationutils;1",
+ "nsIKeychainMigrationUtils"
+);
+
+const gTextEncoder = new TextEncoder();
+const gTextDecoder = new TextDecoder();
+
+/**
+ * From macOS' CommonCrypto/CommonCryptor.h
+ */
+const kCCBlockSizeAES128 = 16;
+
+/* Chromium constants */
+
+/**
+ * kSalt from Chromium.
+ *
+ * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=43&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
+ */
+const SALT = "saltysalt";
+
+/**
+ * kDerivedKeySizeInBits from Chromium.
+ *
+ * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=46&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
+ */
+const DERIVED_KEY_SIZE_BITS = 128;
+
+/**
+ * kEncryptionIterations from Chromium.
+ *
+ * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=49&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
+ */
+const ITERATIONS = 1003;
+
+/**
+ * kEncryptionVersionPrefix from Chromium.
+ *
+ * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=61&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
+ */
+const ENCRYPTION_VERSION_PREFIX = "v10";
+
+/**
+ * The initialization vector is 16 space characters (character code 32 in decimal).
+ *
+ * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=220&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
+ */
+const IV = new Uint8Array(kCCBlockSizeAES128).fill(32);
+
+/**
+ * Instances of this class have a shape similar to OSCrypto so it can be dropped
+ * into code which uses that. This isn't implemented as OSCrypto_mac.js since
+ * it isn't calling into encryption functions provided by macOS but instead
+ * relies on OS encryption key storage in Keychain. The algorithms here are
+ * specific to what is needed for Chrome login storage on macOS.
+ */
+export class ChromeMacOSLoginCrypto {
+ /**
+ * @param {string} serviceName of the Keychain Item to use to derive a key.
+ * @param {string} accountName of the Keychain Item to use to derive a key.
+ * @param {string?} [testingPassphrase = null] A string to use as the passphrase
+ * to derive a key for testing purposes rather than retrieving
+ * it from the macOS Keychain since we don't yet have a way to
+ * mock the Keychain auth dialog.
+ */
+ constructor(serviceName, accountName, testingPassphrase = null) {
+ // We still exercise the keychain migration utils code when using a
+ // `testingPassphrase` in order to get some test coverage for that
+ // component, even though it's expected to throw since a login item with the
+ // service name and account name usually won't be found.
+ let encKey = testingPassphrase;
+ try {
+ encKey = lazy.gKeychainUtils.getGenericPassword(serviceName, accountName);
+ } catch (ex) {
+ if (!testingPassphrase) {
+ throw ex;
+ }
+ }
+
+ this.ALGORITHM = "AES-CBC";
+
+ this._keyPromise = crypto.subtle
+ .importKey("raw", gTextEncoder.encode(encKey), "PBKDF2", false, [
+ "deriveKey",
+ ])
+ .then(key => {
+ return crypto.subtle.deriveKey(
+ {
+ name: "PBKDF2",
+ salt: gTextEncoder.encode(SALT),
+ iterations: ITERATIONS,
+ hash: "SHA-1",
+ },
+ key,
+ { name: this.ALGORITHM, length: DERIVED_KEY_SIZE_BITS },
+ false,
+ ["decrypt", "encrypt"]
+ );
+ })
+ .catch(console.error);
+ }
+
+ /**
+ * Convert an array containing only two bytes unsigned numbers to a string.
+ *
+ * @param {number[]} arr - the array that needs to be converted.
+ * @returns {string} the string representation of the array.
+ */
+ arrayToString(arr) {
+ let str = "";
+ for (let i = 0; i < arr.length; i++) {
+ str += String.fromCharCode(arr[i]);
+ }
+ return str;
+ }
+
+ stringToArray(binary_string) {
+ let len = binary_string.length;
+ let bytes = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ bytes[i] = binary_string.charCodeAt(i);
+ }
+ return bytes;
+ }
+
+ /**
+ * @param {Array} ciphertextArray ciphertext prefixed by the encryption version
+ * (see ENCRYPTION_VERSION_PREFIX).
+ * @returns {string} plaintext password
+ */
+ async decryptData(ciphertextArray) {
+ let ciphertext = this.arrayToString(ciphertextArray);
+ if (!ciphertext.startsWith(ENCRYPTION_VERSION_PREFIX)) {
+ throw new Error("Unknown encryption version");
+ }
+ let key = await this._keyPromise;
+ if (!key) {
+ throw new Error("Cannot decrypt without a key");
+ }
+ let plaintext = await crypto.subtle.decrypt(
+ { name: this.ALGORITHM, iv: IV },
+ key,
+ this.stringToArray(ciphertext.substring(ENCRYPTION_VERSION_PREFIX.length))
+ );
+ return gTextDecoder.decode(plaintext);
+ }
+
+ /**
+ * @param {USVString} plaintext to encrypt
+ * @returns {string} encrypted string consisting of UTF-16 code units prefixed
+ * by the ENCRYPTION_VERSION_PREFIX.
+ */
+ async encryptData(plaintext) {
+ let key = await this._keyPromise;
+ if (!key) {
+ throw new Error("Cannot encrypt without a key");
+ }
+
+ let ciphertext = await crypto.subtle.encrypt(
+ { name: this.ALGORITHM, iv: IV },
+ key,
+ gTextEncoder.encode(plaintext)
+ );
+ return (
+ ENCRYPTION_VERSION_PREFIX +
+ String.fromCharCode(...new Uint8Array(ciphertext))
+ );
+ }
+}