tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ChromeWindowsLoginCrypto.sys.mjs (5836B)


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