tor-browser

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

WeaveCrypto.sys.mjs (6357B)


      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 const CRYPT_ALGO = "AES-CBC";
      6 const CRYPT_ALGO_LENGTH = 256;
      7 const CRYPT_ALGO_USAGES = ["encrypt", "decrypt"];
      8 const AES_CBC_IV_SIZE = 16;
      9 const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 };
     10 const UTF_LABEL = "utf-8";
     11 
     12 export function WeaveCrypto() {
     13  this.init();
     14 }
     15 
     16 WeaveCrypto.prototype = {
     17  prefBranch: null,
     18  debug: true, // services.sync.log.cryptoDebug
     19 
     20  observer: {
     21    _self: null,
     22 
     23    QueryInterface: ChromeUtils.generateQI([
     24      "nsIObserver",
     25      "nsISupportsWeakReference",
     26    ]),
     27 
     28    observe(subject, topic) {
     29      let self = this._self;
     30      self.log("Observed " + topic + " topic.");
     31      if (topic == "nsPref:changed") {
     32        self.debug = self.prefBranch.getBoolPref("cryptoDebug");
     33      }
     34    },
     35  },
     36 
     37  init() {
     38    // Preferences. Add observer so we get notified of changes.
     39    this.prefBranch = Services.prefs.getBranch("services.sync.log.");
     40    this.prefBranch.addObserver("cryptoDebug", this.observer);
     41    this.observer._self = this;
     42    this.debug = this.prefBranch.getBoolPref("cryptoDebug", false);
     43    ChromeUtils.defineLazyGetter(
     44      this,
     45      "encoder",
     46      () => new TextEncoder(UTF_LABEL)
     47    );
     48    ChromeUtils.defineLazyGetter(
     49      this,
     50      "decoder",
     51      () => new TextDecoder(UTF_LABEL, { fatal: true })
     52    );
     53  },
     54 
     55  log(message) {
     56    if (!this.debug) {
     57      return;
     58    }
     59    dump("WeaveCrypto: " + message + "\n");
     60    Services.console.logStringMessage("WeaveCrypto: " + message);
     61  },
     62 
     63  // /!\ Only use this for tests! /!\
     64  _getCrypto() {
     65    return crypto;
     66  },
     67 
     68  async encrypt(clearTextUCS2, symmetricKey, iv) {
     69    this.log("encrypt() called");
     70    let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
     71    let encrypted = await this._commonCrypt(
     72      clearTextBuffer,
     73      symmetricKey,
     74      iv,
     75      OPERATIONS.ENCRYPT
     76    );
     77    return this.encodeBase64(encrypted);
     78  },
     79 
     80  async decrypt(cipherText, symmetricKey, iv) {
     81    this.log("decrypt() called");
     82    if (cipherText.length) {
     83      cipherText = atob(cipherText);
     84    }
     85    let cipherTextBuffer = this.byteCompressInts(cipherText);
     86    let decrypted = await this._commonCrypt(
     87      cipherTextBuffer,
     88      symmetricKey,
     89      iv,
     90      OPERATIONS.DECRYPT
     91    );
     92    return this.decoder.decode(decrypted);
     93  },
     94 
     95  /**
     96   * _commonCrypt
     97   *
     98   * @param {ArrayBuffer} data - data to encrypt/decrypt.
     99   * @param {string} symKeyStr - symmetric key (Base64 String).
    100   * @param {string} ivStr - initialization vector (Base64 String).
    101   * @param {number} operation - operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT)
    102   * @returns {ArrayBuffer}
    103   *   The encrypted/decrypted data.
    104   */
    105  async _commonCrypt(data, symKeyStr, ivStr, operation) {
    106    this.log("_commonCrypt() called");
    107    ivStr = atob(ivStr);
    108 
    109    if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) {
    110      throw new Error("Unsupported operation in _commonCrypt.");
    111    }
    112    // We never want an IV longer than the block size, which is 16 bytes
    113    // for AES, neither do we want one smaller; throw in both cases.
    114    if (ivStr.length !== AES_CBC_IV_SIZE) {
    115      throw new Error(`Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`);
    116    }
    117 
    118    let iv = this.byteCompressInts(ivStr);
    119    let symKey = await this.importSymKey(symKeyStr, operation);
    120    let cryptMethod = (
    121      operation === OPERATIONS.ENCRYPT
    122        ? crypto.subtle.encrypt
    123        : crypto.subtle.decrypt
    124    ).bind(crypto.subtle);
    125    let algo = { name: CRYPT_ALGO, iv };
    126 
    127    let keyBytes = await cryptMethod.call(crypto.subtle, algo, symKey, data);
    128    return new Uint8Array(keyBytes);
    129  },
    130 
    131  async generateRandomKey() {
    132    this.log("generateRandomKey() called");
    133    let algo = {
    134      name: CRYPT_ALGO,
    135      length: CRYPT_ALGO_LENGTH,
    136    };
    137    let key = await crypto.subtle.generateKey(algo, true, CRYPT_ALGO_USAGES);
    138    let keyBytes = await crypto.subtle.exportKey("raw", key);
    139    return this.encodeBase64(new Uint8Array(keyBytes));
    140  },
    141 
    142  generateRandomIV() {
    143    return this.generateRandomBytes(AES_CBC_IV_SIZE);
    144  },
    145 
    146  generateRandomBytes(byteCount) {
    147    this.log("generateRandomBytes() called");
    148 
    149    let randBytes = new Uint8Array(byteCount);
    150    crypto.getRandomValues(randBytes);
    151 
    152    return this.encodeBase64(randBytes);
    153  },
    154 
    155  //
    156  // SymKey CryptoKey memoization.
    157  //
    158 
    159  // Memoize the import of symmetric keys. We do this by using the base64
    160  // string itself as a key.
    161  _encryptionSymKeyMemo: {},
    162  _decryptionSymKeyMemo: {},
    163  async importSymKey(encodedKeyString, operation) {
    164    let memo;
    165 
    166    // We use two separate memos for thoroughness: operation is an input to
    167    // key import.
    168    switch (operation) {
    169      case OPERATIONS.ENCRYPT:
    170        memo = this._encryptionSymKeyMemo;
    171        break;
    172      case OPERATIONS.DECRYPT:
    173        memo = this._decryptionSymKeyMemo;
    174        break;
    175      default:
    176        throw new Error("Unsupported operation in importSymKey.");
    177    }
    178 
    179    if (encodedKeyString in memo) {
    180      return memo[encodedKeyString];
    181    }
    182 
    183    let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true);
    184    let algo = { name: CRYPT_ALGO };
    185    let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"];
    186    let symKey = await crypto.subtle.importKey(
    187      "raw",
    188      symmetricKeyBuffer,
    189      algo,
    190      false,
    191      usages
    192    );
    193    memo[encodedKeyString] = symKey;
    194    return symKey;
    195  },
    196 
    197  //
    198  // Utility functions
    199  //
    200 
    201  /**
    202   * Returns an Uint8Array filled with a JS string,
    203   * which means we only keep utf-16 characters from 0x00 to 0xFF.
    204   */
    205  byteCompressInts(str) {
    206    let arrayBuffer = new Uint8Array(str.length);
    207    for (let i = 0; i < str.length; i++) {
    208      arrayBuffer[i] = str.charCodeAt(i) & 0xff;
    209    }
    210    return arrayBuffer;
    211  },
    212 
    213  expandData(data) {
    214    let expanded = "";
    215    for (let i = 0; i < data.length; i++) {
    216      expanded += String.fromCharCode(data[i]);
    217    }
    218    return expanded;
    219  },
    220 
    221  encodeBase64(data) {
    222    return btoa(this.expandData(data));
    223  },
    224 
    225  makeUint8Array(input, isEncoded) {
    226    if (isEncoded) {
    227      input = atob(input);
    228    }
    229    return this.byteCompressInts(input);
    230  },
    231 };