summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs')
-rw-r--r--browser/components/migration/ChromeWindowsLoginCrypto.sys.mjs176
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))
+ );
+ }
+}