tor-browser

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

ChromeMacOSLoginCrypto.sys.mjs (5884B)


      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 macOS.
      8 */
      9 
     10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     11 
     12 const lazy = {};
     13 
     14 XPCOMUtils.defineLazyServiceGetter(
     15  lazy,
     16  "gKeychainUtils",
     17  "@mozilla.org/profile/migrator/keychainmigrationutils;1",
     18  Ci.nsIKeychainMigrationUtils
     19 );
     20 
     21 const gTextEncoder = new TextEncoder();
     22 const gTextDecoder = new TextDecoder();
     23 
     24 /**
     25 * From macOS' CommonCrypto/CommonCryptor.h
     26 */
     27 const kCCBlockSizeAES128 = 16;
     28 
     29 /* Chromium constants */
     30 
     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";
     37 
     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;
     44 
     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;
     51 
     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";
     58 
     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);
     65 
     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    }
     95 
     96    this.ALGORITHM = "AES-CBC";
     97 
     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  }
    118 
    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  }
    132 
    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  }
    141 
    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  }
    163 
    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    }
    174 
    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  }
    185 }