diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs')
-rw-r--r-- | browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs b/browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs new file mode 100644 index 0000000000..41d38e52d3 --- /dev/null +++ b/browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs @@ -0,0 +1,176 @@ +/* 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 Windows. + */ + +import { ChromeMigrationUtils } from "resource:///modules/ChromeMigrationUtils.sys.mjs"; + +import { OSCrypto } from "resource://gre/modules/OSCrypto_win.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +/** + * These constants should match those from Chromium. + * + * @see https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc + */ +const AEAD_KEY_LENGTH = 256 / 8; +const ALGORITHM_NAME = "AES-GCM"; +const DPAPI_KEY_PREFIX = "DPAPI"; +const ENCRYPTION_VERSION_PREFIX = "v10"; +const NONCE_LENGTH = 96 / 8; + +const gTextDecoder = new TextDecoder(); +const gTextEncoder = new TextEncoder(); + +/** + * Instances of this class have a shape similar to OSCrypto so it can be dropped + * into code which uses that. The algorithms here are + * specific to what is needed for Chrome login storage on Windows. + */ +export class ChromeWindowsLoginCrypto { + /** + * @param {string} userDataPathSuffix The unique identifier for the variant of + * Chrome that is having its logins imported. These are the keys in the + * SUB_DIRECTORIES object in ChromeMigrationUtils.getDataPath. + */ + constructor(userDataPathSuffix) { + this.osCrypto = new OSCrypto(); + + // Lazily decrypt the key from "Chrome"s local state using OSCrypto and save + // it as the master key to decrypt or encrypt passwords. + XPCOMUtils.defineLazyGetter(this, "_keyPromise", async () => { + let keyData; + try { + // NB: For testing, allow directory service to be faked before getting. + const localState = await ChromeMigrationUtils.getLocalState( + userDataPathSuffix + ); + const withHeader = atob(localState.os_crypt.encrypted_key); + if (!withHeader.startsWith(DPAPI_KEY_PREFIX)) { + throw new Error("Invalid key format"); + } + const encryptedKey = withHeader.slice(DPAPI_KEY_PREFIX.length); + keyData = this.osCrypto.decryptData(encryptedKey, null, "bytes"); + } catch (ex) { + console.error(`${userDataPathSuffix} os_crypt key: ${ex}`); + + // Use a generic key that will fail for actually encrypted data, but for + // testing it'll be consistent for both encrypting and decrypting. + keyData = AEAD_KEY_LENGTH; + } + return crypto.subtle.importKey( + "raw", + new Uint8Array(keyData), + ALGORITHM_NAME, + false, + ["decrypt", "encrypt"] + ); + }); + } + + /** + * Must be invoked once after last use of any of the provided helpers. + */ + finalize() { + this.osCrypto.finalize(); + } + + /** + * 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) { + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes; + } + + /** + * @param {string} ciphertext ciphertext optionally prefixed by the encryption version + * (see ENCRYPTION_VERSION_PREFIX). + * @returns {string} plaintext password + */ + async decryptData(ciphertext) { + const ciphertextString = this.arrayToString(ciphertext); + return ciphertextString.startsWith(ENCRYPTION_VERSION_PREFIX) + ? this._decryptV10(ciphertext) + : this._decryptUnversioned(ciphertextString); + } + + async _decryptUnversioned(ciphertext) { + return this.osCrypto.decryptData(ciphertext); + } + + async _decryptV10(ciphertext) { + const key = await this._keyPromise; + if (!key) { + throw new Error("Cannot decrypt without a key"); + } + + // Split the nonce/iv from the rest of the encrypted value and decrypt. + const nonceIndex = ENCRYPTION_VERSION_PREFIX.length; + const cipherIndex = nonceIndex + NONCE_LENGTH; + const iv = new Uint8Array(ciphertext.slice(nonceIndex, cipherIndex)); + const algorithm = { + name: ALGORITHM_NAME, + iv, + }; + const cipherArray = new Uint8Array(ciphertext.slice(cipherIndex)); + const plaintext = await crypto.subtle.decrypt(algorithm, key, cipherArray); + return gTextDecoder.decode(new Uint8Array(plaintext)); + } + + /** + * @param {USVString} plaintext to encrypt + * @param {?string} version to encrypt default unversioned + * @returns {string} encrypted string consisting of UTF-16 code units prefixed + * by the ENCRYPTION_VERSION_PREFIX. + */ + async encryptData(plaintext, version = undefined) { + return version === ENCRYPTION_VERSION_PREFIX + ? this._encryptV10(plaintext) + : this._encryptUnversioned(plaintext); + } + + async _encryptUnversioned(plaintext) { + return this.osCrypto.encryptData(plaintext); + } + + async _encryptV10(plaintext) { + const key = await this._keyPromise; + if (!key) { + throw new Error("Cannot encrypt without a key"); + } + + // Encrypt and concatenate the prefix, nonce/iv and encrypted value. + const iv = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); + const algorithm = { + name: ALGORITHM_NAME, + iv, + }; + const plainArray = gTextEncoder.encode(plaintext); + const ciphertext = await crypto.subtle.encrypt(algorithm, key, plainArray); + return ( + ENCRYPTION_VERSION_PREFIX + + this.arrayToString(iv) + + this.arrayToString(new Uint8Array(ciphertext)) + ); + } +} |