1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Class to handle encryption and decryption of logins stored in Chrome/Chromium
10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 XPCOMUtils.defineLazyServiceGetter(
17 "@mozilla.org/profile/migrator/keychainmigrationutils;1",
18 "nsIKeychainMigrationUtils"
21 const gTextEncoder = new TextEncoder();
22 const gTextDecoder = new TextDecoder();
25 * From macOS' CommonCrypto/CommonCryptor.h
27 const kCCBlockSizeAES128 = 16;
29 /* Chromium constants */
32 * kSalt from Chromium.
34 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=43&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
36 const SALT = "saltysalt";
39 * kDerivedKeySizeInBits from Chromium.
41 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=46&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
43 const DERIVED_KEY_SIZE_BITS = 128;
46 * kEncryptionIterations from Chromium.
48 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=49&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
50 const ITERATIONS = 1003;
53 * kEncryptionVersionPrefix from Chromium.
55 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=61&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
57 const ENCRYPTION_VERSION_PREFIX = "v10";
60 * The initialization vector is 16 space characters (character code 32 in decimal).
62 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=220&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
64 const IV = new Uint8Array(kCCBlockSizeAES128).fill(32);
67 * Instances of this class have a shape similar to OSCrypto so it can be dropped
68 * into code which uses that. This isn't implemented as OSCrypto_mac.js since
69 * it isn't calling into encryption functions provided by macOS but instead
70 * relies on OS encryption key storage in Keychain. The algorithms here are
71 * specific to what is needed for Chrome login storage on macOS.
73 export class ChromeMacOSLoginCrypto {
75 * @param {string} serviceName of the Keychain Item to use to derive a key.
76 * @param {string} accountName of the Keychain Item to use to derive a key.
77 * @param {string?} [testingPassphrase = null] A string to use as the passphrase
78 * to derive a key for testing purposes rather than retrieving
79 * it from the macOS Keychain since we don't yet have a way to
80 * mock the Keychain auth dialog.
82 constructor(serviceName, accountName, testingPassphrase = null) {
83 // We still exercise the keychain migration utils code when using a
84 // `testingPassphrase` in order to get some test coverage for that
85 // component, even though it's expected to throw since a login item with the
86 // service name and account name usually won't be found.
87 let encKey = testingPassphrase;
89 encKey = lazy.gKeychainUtils.getGenericPassword(serviceName, accountName);
91 if (!testingPassphrase) {
96 this.ALGORITHM = "AES-CBC";
98 this._keyPromise = crypto.subtle
99 .importKey("raw", gTextEncoder.encode(encKey), "PBKDF2", false, [
103 return crypto.subtle.deriveKey(
106 salt: gTextEncoder.encode(SALT),
107 iterations: ITERATIONS,
111 { name: this.ALGORITHM, length: DERIVED_KEY_SIZE_BITS },
113 ["decrypt", "encrypt"]
116 .catch(console.error);
120 * Convert an array containing only two bytes unsigned numbers to a string.
122 * @param {number[]} arr - the array that needs to be converted.
123 * @returns {string} the string representation of the array.
127 for (let i = 0; i < arr.length; i++) {
128 str += String.fromCharCode(arr[i]);
133 stringToArray(binary_string) {
134 let len = binary_string.length;
135 let bytes = new Uint8Array(len);
136 for (var i = 0; i < len; i++) {
137 bytes[i] = binary_string.charCodeAt(i);
143 * @param {Array} ciphertextArray ciphertext prefixed by the encryption version
144 * (see ENCRYPTION_VERSION_PREFIX).
145 * @returns {string} plaintext password
147 async decryptData(ciphertextArray) {
148 let ciphertext = this.arrayToString(ciphertextArray);
149 if (!ciphertext.startsWith(ENCRYPTION_VERSION_PREFIX)) {
150 throw new Error("Unknown encryption version");
152 let key = await this._keyPromise;
154 throw new Error("Cannot decrypt without a key");
156 let plaintext = await crypto.subtle.decrypt(
157 { name: this.ALGORITHM, iv: IV },
159 this.stringToArray(ciphertext.substring(ENCRYPTION_VERSION_PREFIX.length))
161 return gTextDecoder.decode(plaintext);
165 * @param {USVString} plaintext to encrypt
166 * @returns {string} encrypted string consisting of UTF-16 code units prefixed
167 * by the ENCRYPTION_VERSION_PREFIX.
169 async encryptData(plaintext) {
170 let key = await this._keyPromise;
172 throw new Error("Cannot encrypt without a key");
175 let ciphertext = await crypto.subtle.encrypt(
176 { name: this.ALGORITHM, iv: IV },
178 gTextEncoder.encode(plaintext)
181 ENCRYPTION_VERSION_PREFIX +
182 String.fromCharCode(...new Uint8Array(ciphertext))