Bug 1824490 - Use the end page value rather than the start page value of the previous...
[gecko.git] / browser / components / migration / ChromeWindowsLoginCrypto.sys.mjs
blob47ce0a9339f3c62b506dbea61174c726953ccab8
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/. */
5 /**
6  * Class to handle encryption and decryption of logins stored in Chrome/Chromium
7  * on Windows.
8  */
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";
17 /**
18  * These constants should match those from Chromium.
19  *
20  * @see https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc
21  */
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();
31 /**
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.
35  */
36 export class ChromeWindowsLoginCrypto {
37   /**
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.
41    */
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 () => {
48       let keyData;
49       try {
50         // NB: For testing, allow directory service to be faked before getting.
51         const localState = await ChromeMigrationUtils.getLocalState(
52           userDataPathSuffix
53         );
54         const withHeader = atob(localState.os_crypt.encrypted_key);
55         if (!withHeader.startsWith(DPAPI_KEY_PREFIX)) {
56           throw new Error("Invalid key format");
57         }
58         const encryptedKey = withHeader.slice(DPAPI_KEY_PREFIX.length);
59         keyData = this.osCrypto.decryptData(encryptedKey, null, "bytes");
60       } catch (ex) {
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;
66       }
67       return crypto.subtle.importKey(
68         "raw",
69         new Uint8Array(keyData),
70         ALGORITHM_NAME,
71         false,
72         ["decrypt", "encrypt"]
73       );
74     });
75   }
77   /**
78    * Must be invoked once after last use of any of the provided helpers.
79    */
80   finalize() {
81     this.osCrypto.finalize();
82   }
84   /**
85    * Convert an array containing only two bytes unsigned numbers to a string.
86    *
87    * @param {number[]} arr - the array that needs to be converted.
88    * @returns {string} the string representation of the array.
89    */
90   arrayToString(arr) {
91     let str = "";
92     for (let i = 0; i < arr.length; i++) {
93       str += String.fromCharCode(arr[i]);
94     }
95     return str;
96   }
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);
103     }
104     return bytes;
105   }
107   /**
108    * @param {string} ciphertext ciphertext optionally prefixed by the encryption version
109    *                            (see ENCRYPTION_VERSION_PREFIX).
110    * @returns {string} plaintext password
111    */
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);
117   }
119   async _decryptUnversioned(ciphertext) {
120     return this.osCrypto.decryptData(ciphertext);
121   }
123   async _decryptV10(ciphertext) {
124     const key = await this._keyPromise;
125     if (!key) {
126       throw new Error("Cannot decrypt without a key");
127     }
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));
133     const algorithm = {
134       name: ALGORITHM_NAME,
135       iv,
136     };
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));
140   }
142   /**
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.
147    */
148   async encryptData(plaintext, version = undefined) {
149     return version === ENCRYPTION_VERSION_PREFIX
150       ? this._encryptV10(plaintext)
151       : this._encryptUnversioned(plaintext);
152   }
154   async _encryptUnversioned(plaintext) {
155     return this.osCrypto.encryptData(plaintext);
156   }
158   async _encryptV10(plaintext) {
159     const key = await this._keyPromise;
160     if (!key) {
161       throw new Error("Cannot encrypt without a key");
162     }
164     // Encrypt and concatenate the prefix, nonce/iv and encrypted value.
165     const iv = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
166     const algorithm = {
167       name: ALGORITHM_NAME,
168       iv,
169     };
170     const plainArray = gTextEncoder.encode(plaintext);
171     const ciphertext = await crypto.subtle.encrypt(algorithm, key, plainArray);
172     return (
173       ENCRYPTION_VERSION_PREFIX +
174       this.arrayToString(iv) +
175       this.arrayToString(new Uint8Array(ciphertext))
176     );
177   }