Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / migration / ChromeMacOSLoginCrypto.sys.mjs
blob595bbc28c4d73c66bc250ee638e5c83b8a4b6d1f
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 macOS.
8  */
10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
12 const lazy = {};
14 XPCOMUtils.defineLazyServiceGetter(
15   lazy,
16   "gKeychainUtils",
17   "@mozilla.org/profile/migrator/keychainmigrationutils;1",
18   "nsIKeychainMigrationUtils"
21 const gTextEncoder = new TextEncoder();
22 const gTextDecoder = new TextDecoder();
24 /**
25  * From macOS' CommonCrypto/CommonCryptor.h
26  */
27 const kCCBlockSizeAES128 = 16;
29 /* Chromium constants */
31 /**
32  * kSalt from Chromium.
33  *
34  * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=43&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
35  */
36 const SALT = "saltysalt";
38 /**
39  * kDerivedKeySizeInBits from Chromium.
40  *
41  * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=46&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
42  */
43 const DERIVED_KEY_SIZE_BITS = 128;
45 /**
46  * kEncryptionIterations from Chromium.
47  *
48  * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=49&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
49  */
50 const ITERATIONS = 1003;
52 /**
53  * kEncryptionVersionPrefix from Chromium.
54  *
55  * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=61&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
56  */
57 const ENCRYPTION_VERSION_PREFIX = "v10";
59 /**
60  * The initialization vector is 16 space characters (character code 32 in decimal).
61  *
62  * @see https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm?l=220&rcl=1771751f87e3e99bb6cd67b5d0e159ae487f8db0
63  */
64 const IV = new Uint8Array(kCCBlockSizeAES128).fill(32);
66 /**
67  * Instances of this class have a shape similar to OSCrypto so it can be dropped
68  * into code which uses that. This isn't implemented as OSCrypto_mac.js since
69  * it isn't calling into encryption functions provided by macOS but instead
70  * relies on OS encryption key storage in Keychain. The algorithms here are
71  * specific to what is needed for Chrome login storage on macOS.
72  */
73 export class ChromeMacOSLoginCrypto {
74   /**
75    * @param {string} serviceName of the Keychain Item to use to derive a key.
76    * @param {string} accountName of the Keychain Item to use to derive a key.
77    * @param {string?} [testingPassphrase = null] A string to use as the passphrase
78    *                  to derive a key for testing purposes rather than retrieving
79    *                  it from the macOS Keychain since we don't yet have a way to
80    *                  mock the Keychain auth dialog.
81    */
82   constructor(serviceName, accountName, testingPassphrase = null) {
83     // We still exercise the keychain migration utils code when using a
84     // `testingPassphrase` in order to get some test coverage for that
85     // component, even though it's expected to throw since a login item with the
86     // service name and account name usually won't be found.
87     let encKey = testingPassphrase;
88     try {
89       encKey = lazy.gKeychainUtils.getGenericPassword(serviceName, accountName);
90     } catch (ex) {
91       if (!testingPassphrase) {
92         throw ex;
93       }
94     }
96     this.ALGORITHM = "AES-CBC";
98     this._keyPromise = crypto.subtle
99       .importKey("raw", gTextEncoder.encode(encKey), "PBKDF2", false, [
100         "deriveKey",
101       ])
102       .then(key => {
103         return crypto.subtle.deriveKey(
104           {
105             name: "PBKDF2",
106             salt: gTextEncoder.encode(SALT),
107             iterations: ITERATIONS,
108             hash: "SHA-1",
109           },
110           key,
111           { name: this.ALGORITHM, length: DERIVED_KEY_SIZE_BITS },
112           false,
113           ["decrypt", "encrypt"]
114         );
115       })
116       .catch(console.error);
117   }
119   /**
120    * Convert an array containing only two bytes unsigned numbers to a string.
121    *
122    * @param {number[]} arr - the array that needs to be converted.
123    * @returns {string} the string representation of the array.
124    */
125   arrayToString(arr) {
126     let str = "";
127     for (let i = 0; i < arr.length; i++) {
128       str += String.fromCharCode(arr[i]);
129     }
130     return str;
131   }
133   stringToArray(binary_string) {
134     let len = binary_string.length;
135     let bytes = new Uint8Array(len);
136     for (var i = 0; i < len; i++) {
137       bytes[i] = binary_string.charCodeAt(i);
138     }
139     return bytes;
140   }
142   /**
143    * @param {Array} ciphertextArray ciphertext prefixed by the encryption version
144    *                            (see ENCRYPTION_VERSION_PREFIX).
145    * @returns {string} plaintext password
146    */
147   async decryptData(ciphertextArray) {
148     let ciphertext = this.arrayToString(ciphertextArray);
149     if (!ciphertext.startsWith(ENCRYPTION_VERSION_PREFIX)) {
150       throw new Error("Unknown encryption version");
151     }
152     let key = await this._keyPromise;
153     if (!key) {
154       throw new Error("Cannot decrypt without a key");
155     }
156     let plaintext = await crypto.subtle.decrypt(
157       { name: this.ALGORITHM, iv: IV },
158       key,
159       this.stringToArray(ciphertext.substring(ENCRYPTION_VERSION_PREFIX.length))
160     );
161     return gTextDecoder.decode(plaintext);
162   }
164   /**
165    * @param {USVString} plaintext to encrypt
166    * @returns {string} encrypted string consisting of UTF-16 code units prefixed
167    *                   by the ENCRYPTION_VERSION_PREFIX.
168    */
169   async encryptData(plaintext) {
170     let key = await this._keyPromise;
171     if (!key) {
172       throw new Error("Cannot encrypt without a key");
173     }
175     let ciphertext = await crypto.subtle.encrypt(
176       { name: this.ALGORITHM, iv: IV },
177       key,
178       gTextEncoder.encode(plaintext)
179     );
180     return (
181       ENCRYPTION_VERSION_PREFIX +
182       String.fromCharCode(...new Uint8Array(ciphertext))
183     );
184   }