diff options
Diffstat (limited to 'browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs')
-rw-r--r-- | browser/components/migration/ChromeMacOSLoginCrypto.sys.mjs | 185 |
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)) + ); + } +} |