/* 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)) ); } }