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