ChromeWindowsLoginCrypto.sys.mjs (5836B)
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 Windows. 8 */ 9 10 import { ChromeMigrationUtils } from "resource:///modules/ChromeMigrationUtils.sys.mjs"; 11 12 import { OSCrypto } from "resource://gre/modules/OSCrypto_win.sys.mjs"; 13 14 /** 15 * These constants should match those from Chromium. 16 * 17 * @see https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc 18 */ 19 const AEAD_KEY_LENGTH = 256 / 8; 20 const ALGORITHM_NAME = "AES-GCM"; 21 const DPAPI_KEY_PREFIX = "DPAPI"; 22 const ENCRYPTION_VERSION_PREFIX = "v10"; 23 const NONCE_LENGTH = 96 / 8; 24 25 const gTextDecoder = new TextDecoder(); 26 const gTextEncoder = new TextEncoder(); 27 28 /** 29 * Instances of this class have a shape similar to OSCrypto so it can be dropped 30 * into code which uses that. The algorithms here are 31 * specific to what is needed for Chrome login storage on Windows. 32 */ 33 export class ChromeWindowsLoginCrypto { 34 /** 35 * @param {string} userDataPathSuffix The unique identifier for the variant of 36 * Chrome that is having its logins imported. These are the keys in the 37 * SUB_DIRECTORIES object in ChromeMigrationUtils.getDataPath. 38 */ 39 constructor(userDataPathSuffix) { 40 this.osCrypto = new OSCrypto(); 41 42 // Lazily decrypt the key from "Chrome"s local state using OSCrypto and save 43 // it as the master key to decrypt or encrypt passwords. 44 ChromeUtils.defineLazyGetter(this, "_keyPromise", async () => { 45 let keyData; 46 try { 47 // NB: For testing, allow directory service to be faked before getting. 48 const localState = 49 await ChromeMigrationUtils.getLocalState(userDataPathSuffix); 50 const withHeader = atob(localState.os_crypt.encrypted_key); 51 if (!withHeader.startsWith(DPAPI_KEY_PREFIX)) { 52 throw new Error("Invalid key format"); 53 } 54 const encryptedKey = withHeader.slice(DPAPI_KEY_PREFIX.length); 55 keyData = this.osCrypto.decryptData(encryptedKey, null, "bytes"); 56 } catch (ex) { 57 console.error(`${userDataPathSuffix} os_crypt key:`, ex); 58 59 // Use a generic key that will fail for actually encrypted data, but for 60 // testing it'll be consistent for both encrypting and decrypting. 61 keyData = AEAD_KEY_LENGTH; 62 } 63 return crypto.subtle.importKey( 64 "raw", 65 new Uint8Array(keyData), 66 ALGORITHM_NAME, 67 false, 68 ["decrypt", "encrypt"] 69 ); 70 }); 71 } 72 73 /** 74 * Must be invoked once after last use of any of the provided helpers. 75 */ 76 finalize() { 77 this.osCrypto.finalize(); 78 } 79 80 /** 81 * Convert an array containing only two bytes unsigned numbers to a string. 82 * 83 * @param {number[]} arr - the array that needs to be converted. 84 * @returns {string} the string representation of the array. 85 */ 86 arrayToString(arr) { 87 let str = ""; 88 for (let i = 0; i < arr.length; i++) { 89 str += String.fromCharCode(arr[i]); 90 } 91 return str; 92 } 93 94 stringToArray(binary_string) { 95 const len = binary_string.length; 96 const bytes = new Uint8Array(len); 97 for (let i = 0; i < len; i++) { 98 bytes[i] = binary_string.charCodeAt(i); 99 } 100 return bytes; 101 } 102 103 /** 104 * @param {string} ciphertext ciphertext optionally prefixed by the encryption version 105 * (see ENCRYPTION_VERSION_PREFIX). 106 * @returns {string} plaintext password 107 */ 108 async decryptData(ciphertext) { 109 const ciphertextString = this.arrayToString(ciphertext); 110 return ciphertextString.startsWith(ENCRYPTION_VERSION_PREFIX) 111 ? this._decryptV10(ciphertext) 112 : this._decryptUnversioned(ciphertextString); 113 } 114 115 async _decryptUnversioned(ciphertext) { 116 return this.osCrypto.decryptData(ciphertext); 117 } 118 119 async _decryptV10(ciphertext) { 120 const key = await this._keyPromise; 121 if (!key) { 122 throw new Error("Cannot decrypt without a key"); 123 } 124 125 // Split the nonce/iv from the rest of the encrypted value and decrypt. 126 const nonceIndex = ENCRYPTION_VERSION_PREFIX.length; 127 const cipherIndex = nonceIndex + NONCE_LENGTH; 128 const iv = new Uint8Array(ciphertext.slice(nonceIndex, cipherIndex)); 129 const algorithm = { 130 name: ALGORITHM_NAME, 131 iv, 132 }; 133 const cipherArray = new Uint8Array(ciphertext.slice(cipherIndex)); 134 const plaintext = await crypto.subtle.decrypt(algorithm, key, cipherArray); 135 return gTextDecoder.decode(new Uint8Array(plaintext)); 136 } 137 138 /** 139 * @param {USVString} plaintext to encrypt 140 * @param {?string} version to encrypt default unversioned 141 * @returns {string} encrypted string consisting of UTF-16 code units prefixed 142 * by the ENCRYPTION_VERSION_PREFIX. 143 */ 144 async encryptData(plaintext, version = undefined) { 145 return version === ENCRYPTION_VERSION_PREFIX 146 ? this._encryptV10(plaintext) 147 : this._encryptUnversioned(plaintext); 148 } 149 150 async _encryptUnversioned(plaintext) { 151 return this.osCrypto.encryptData(plaintext); 152 } 153 154 async _encryptV10(plaintext) { 155 const key = await this._keyPromise; 156 if (!key) { 157 throw new Error("Cannot encrypt without a key"); 158 } 159 160 // Encrypt and concatenate the prefix, nonce/iv and encrypted value. 161 const iv = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); 162 const algorithm = { 163 name: ALGORITHM_NAME, 164 iv, 165 }; 166 const plainArray = gTextEncoder.encode(plaintext); 167 const ciphertext = await crypto.subtle.encrypt(algorithm, key, plainArray); 168 return ( 169 ENCRYPTION_VERSION_PREFIX + 170 this.arrayToString(iv) + 171 this.arrayToString(new Uint8Array(ciphertext)) 172 ); 173 } 174 }