ChromeMacOSLoginCrypto.sys.mjs (5884B)
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/. */ 4 5 /** 6 * Class to handle encryption and decryption of logins stored in Chrome/Chromium 7 * on macOS. 8 */ 9 10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 11 12 const lazy = {}; 13 14 XPCOMUtils.defineLazyServiceGetter( 15 lazy, 16 "gKeychainUtils", 17 "@mozilla.org/profile/migrator/keychainmigrationutils;1", 18 Ci.nsIKeychainMigrationUtils 19 ); 20 21 const gTextEncoder = new TextEncoder(); 22 const gTextDecoder = new TextDecoder(); 23 24 /** 25 * From macOS' CommonCrypto/CommonCryptor.h 26 */ 27 const kCCBlockSizeAES128 = 16; 28 29 /* Chromium constants */ 30 31 /** 32 * kSalt from Chromium. 33 * 34 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=43&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0 35 */ 36 const SALT = "saltysalt"; 37 38 /** 39 * kDerivedKeySizeInBits from Chromium. 40 * 41 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=46&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0 42 */ 43 const DERIVED_KEY_SIZE_BITS = 128; 44 45 /** 46 * kEncryptionIterations from Chromium. 47 * 48 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=49&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0 49 */ 50 const ITERATIONS = 1003; 51 52 /** 53 * kEncryptionVersionPrefix from Chromium. 54 * 55 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=61&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0 56 */ 57 const ENCRYPTION_VERSION_PREFIX = "v10"; 58 59 /** 60 * The initialization vector is 16 space characters (character code 32 in decimal). 61 * 62 * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=220&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0 63 */ 64 const IV = new Uint8Array(kCCBlockSizeAES128).fill(32); 65 66 /** 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. 72 */ 73 export class ChromeMacOSLoginCrypto { 74 /** 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. 81 */ 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; 88 try { 89 encKey = lazy.gKeychainUtils.getGenericPassword(serviceName, accountName); 90 } catch (ex) { 91 if (!testingPassphrase) { 92 throw ex; 93 } 94 } 95 96 this.ALGORITHM = "AES-CBC"; 97 98 this._keyPromise = crypto.subtle 99 .importKey("raw", gTextEncoder.encode(encKey), "PBKDF2", false, [ 100 "deriveKey", 101 ]) 102 .then(key => { 103 return crypto.subtle.deriveKey( 104 { 105 name: "PBKDF2", 106 salt: gTextEncoder.encode(SALT), 107 iterations: ITERATIONS, 108 hash: "SHA-1", 109 }, 110 key, 111 { name: this.ALGORITHM, length: DERIVED_KEY_SIZE_BITS }, 112 false, 113 ["decrypt", "encrypt"] 114 ); 115 }) 116 .catch(console.error); 117 } 118 119 /** 120 * Convert an array containing only two bytes unsigned numbers to a string. 121 * 122 * @param {number[]} arr - the array that needs to be converted. 123 * @returns {string} the string representation of the array. 124 */ 125 arrayToString(arr) { 126 let str = ""; 127 for (let i = 0; i < arr.length; i++) { 128 str += String.fromCharCode(arr[i]); 129 } 130 return str; 131 } 132 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); 138 } 139 return bytes; 140 } 141 142 /** 143 * @param {Array} ciphertextArray ciphertext prefixed by the encryption version 144 * (see ENCRYPTION_VERSION_PREFIX). 145 * @returns {string} plaintext password 146 */ 147 async decryptData(ciphertextArray) { 148 let ciphertext = this.arrayToString(ciphertextArray); 149 if (!ciphertext.startsWith(ENCRYPTION_VERSION_PREFIX)) { 150 throw new Error("Unknown encryption version"); 151 } 152 let key = await this._keyPromise; 153 if (!key) { 154 throw new Error("Cannot decrypt without a key"); 155 } 156 let plaintext = await crypto.subtle.decrypt( 157 { name: this.ALGORITHM, iv: IV }, 158 key, 159 this.stringToArray(ciphertext.substring(ENCRYPTION_VERSION_PREFIX.length)) 160 ); 161 return gTextDecoder.decode(plaintext); 162 } 163 164 /** 165 * @param {USVString} plaintext to encrypt 166 * @returns {string} encrypted string consisting of UTF-16 code units prefixed 167 * by the ENCRYPTION_VERSION_PREFIX. 168 */ 169 async encryptData(plaintext) { 170 let key = await this._keyPromise; 171 if (!key) { 172 throw new Error("Cannot encrypt without a key"); 173 } 174 175 let ciphertext = await crypto.subtle.encrypt( 176 { name: this.ALGORITHM, iv: IV }, 177 key, 178 gTextEncoder.encode(plaintext) 179 ); 180 return ( 181 ENCRYPTION_VERSION_PREFIX + 182 String.fromCharCode(...new Uint8Array(ciphertext)) 183 ); 184 } 185 }