tor-browser

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

ArchiveUtils.sys.mjs (9613B)


      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 // This module expects to be able to load in both main-thread module contexts,
      6 // as well as ChromeWorker contexts. Do not ChromeUtils.importESModule
      7 // anything there at the top-level that's not compatible with both contexts.
      8 
      9 export const ArchiveUtils = {
     10  /**
     11   * Convert an array containing only two bytes unsigned numbers to a base64
     12   * encoded string.
     13   *
     14   * @param {number[]} anArray
     15   *   The array that needs to be converted.
     16   * @returns {string}
     17   *   The string representation of the array.
     18   */
     19  arrayToBase64(anArray) {
     20    let result = "";
     21    let bytes = new Uint8Array(anArray);
     22    for (let i = 0; i < bytes.length; i++) {
     23      result += String.fromCharCode(bytes[i]);
     24    }
     25    return btoa(result);
     26  },
     27 
     28  /**
     29   * Convert a base64 encoded string to an Uint8Array.
     30   *
     31   * @param {string} base64Str
     32   *   The base64 encoded string that needs to be converted.
     33   * @returns {Uint8Array[]}
     34   *   The array representation of the string.
     35   */
     36  stringToArray(base64Str) {
     37    let binaryStr = atob(base64Str);
     38    let len = binaryStr.length;
     39    let bytes = new Uint8Array(len);
     40    for (let i = 0; i < len; i++) {
     41      bytes[i] = binaryStr.charCodeAt(i);
     42    }
     43    return bytes;
     44  },
     45 
     46  /**
     47   * The current shared schema version between the BackupManifest and the
     48   * ArchiveJSONBlock schemas.
     49   *
     50   * @type {number}
     51   */
     52  get SCHEMA_VERSION() {
     53    return 1;
     54  },
     55 
     56  /**
     57   * The version of the single-file archive that this version of the
     58   * application is expected to produce. Versions greater than this are not
     59   * interpretable by the application, and will cause an exception to be
     60   * thrown when loading the archive.
     61   *
     62   * Note: Until we can interpolate strings in our templates, changing this
     63   * value will require manual changes to the archive.template.html version
     64   * number in the header, as well as any test templates.
     65   *
     66   * @type {number}
     67   */
     68  get ARCHIVE_FILE_VERSION() {
     69    return 1;
     70  },
     71 
     72  /**
     73   * The HTML document comment start block, also indicating the start of the
     74   * inline MIME message block.
     75   *
     76   * @type {string}
     77   */
     78  get INLINE_MIME_START_MARKER() {
     79    return "<!-- Begin inline MIME --";
     80  },
     81 
     82  /**
     83   * The HTML document comment end block, also indicating the end of the
     84   * inline MIME message block.
     85   *
     86   * @type {string}
     87   */
     88  get INLINE_MIME_END_MARKER() {
     89    return "---- End inline MIME -->";
     90  },
     91 
     92  /**
     93   * The maximum number of bytes to read and encode when constructing the
     94   * single-file archive.
     95   *
     96   * @type {number}
     97   */
     98  get ARCHIVE_CHUNK_MAX_BYTES_SIZE() {
     99    return 1048576; // 2 ^ 20 bytes, per guidance from security engineering.
    100  },
    101 
    102  /**
    103   * The maximum size of a backup archive, in bytes, prior to base64 encoding.
    104   *
    105   * @type {number}
    106   */
    107  get ARCHIVE_MAX_BYTES_SIZE() {
    108    return 34359738368; // 2 ^ 35 bytes (32 GiB)
    109  },
    110 
    111  /**
    112   * The AES-GCM tag length applied to each encrypted chunk, in bits.
    113   *
    114   * @type {number}
    115   */
    116  get TAG_LENGTH() {
    117    return 128;
    118  },
    119 
    120  /**
    121   * The AES-GCM tag length applied to each encrypted chunk, in bytes.
    122   *
    123   * @type {number}
    124   */
    125  get TAG_LENGTH_BYTES() {
    126    return this.TAG_LENGTH / 8;
    127  },
    128 
    129  /**
    130   * @typedef {object} ComputeKeysResult
    131   * @property {Uint8Array} backupAuthKey
    132   *   The computed BackupAuthKey. This is returned as a Uint8Array because
    133   *   this key is used as a salt for other derived keys.
    134   * @property {CryptoKey} backupEncKey
    135   *   The computed BackupEncKey. This is an AES-GCM key used to encrypt and
    136   *   decrypt the secrets contained within a backup archive.
    137   */
    138 
    139  /**
    140   * Computes the BackupAuthKey and BackupEncKey from a recovery code and a
    141   * salt.
    142   *
    143   * @param {string} recoveryCode
    144   *   A recovery code. Callers are responsible for checking the length /
    145   *   entropy of the recovery code.
    146   * @param {Uint8Array} salt
    147   *   A salt that should be used for computing the keys.
    148   * @returns {ComputeKeysResult}
    149   */
    150  async computeBackupKeys(recoveryCode, salt) {
    151    let textEncoder = new TextEncoder();
    152    let recoveryCodeBytes = textEncoder.encode(recoveryCode);
    153 
    154    let keyMaterial = await crypto.subtle.importKey(
    155      "raw",
    156      recoveryCodeBytes,
    157      "PBKDF2",
    158      false /* extractable */,
    159      ["deriveBits"]
    160    );
    161 
    162    // Then we derive the "backup key", using
    163    // PBKDF2(recoveryCode, saltPrefix || SALT_SUFFIX, SHA-256, 600,000)
    164    const ITERATIONS = 600_000;
    165 
    166    let backupKeyBits = await crypto.subtle.deriveBits(
    167      {
    168        name: "PBKDF2",
    169        salt,
    170        iterations: ITERATIONS,
    171        hash: "SHA-256",
    172      },
    173      keyMaterial,
    174      256
    175    );
    176 
    177    // This is a little awkward, but the way that the WebCrypto API currently
    178    // works is that we have to read in those bits as a "raw HKDF key", and
    179    // only then can we derive our other HKDF keys from it.
    180    let backupKeyHKDF = await crypto.subtle.importKey(
    181      "raw",
    182      backupKeyBits,
    183      {
    184        name: "HKDF",
    185        hash: "SHA-256",
    186      },
    187      false /* extractable */,
    188      ["deriveKey", "deriveBits"]
    189    );
    190 
    191    // Re-derive BackupAuthKey as HKDF(backupKey, “backupkey-auth”, salt=None)
    192    let backupAuthKey = new Uint8Array(
    193      await crypto.subtle.deriveBits(
    194        {
    195          name: "HKDF",
    196          salt: new Uint8Array(0), // no salt
    197          info: textEncoder.encode("backupkey-auth"),
    198          hash: "SHA-256",
    199        },
    200        backupKeyHKDF,
    201        256
    202      )
    203    );
    204 
    205    let backupEncKey = await crypto.subtle.deriveKey(
    206      {
    207        name: "HKDF",
    208        salt: new Uint8Array(0), // no salt
    209        info: textEncoder.encode("backupkey-enc-key"),
    210        hash: "SHA-256",
    211      },
    212      backupKeyHKDF,
    213      { name: "AES-GCM", length: 256 },
    214      false /* extractable */,
    215      ["encrypt", "decrypt", "wrapKey"]
    216    );
    217 
    218    return { backupAuthKey, backupEncKey };
    219  },
    220 
    221  /**
    222   * @typedef {object} ComputeEncryptionKeysResult
    223   * @property {CryptoKey} archiveEncKey
    224   *   This is an AES-GCM key used to encrypt chunks of a backup archive.
    225   * @property {CryptoKey} authKey
    226   *   This is a unique authKey for a particular backup that lets us
    227   *   generate the confirmation HMAC for the backup metadata.
    228   */
    229 
    230  /**
    231   * Computes the encryption keys for a particular archive.
    232   *
    233   * @param {Uint8Array} archiveKeyMaterial
    234   *   The key material used to generate the encryption keys.
    235   * @param {Uint8Array} backupAuthKey
    236   *   The backupAuthKey returned from computeBackupKeys.
    237   * @returns {ComputeEncryptionKeysResult}
    238   */
    239  async computeEncryptionKeys(archiveKeyMaterial, backupAuthKey) {
    240    let archiveKey = await crypto.subtle.importKey(
    241      "raw",
    242      archiveKeyMaterial,
    243      { name: "HKDF" },
    244      false, // Not extractable
    245      ["deriveKey", "deriveBits"]
    246    );
    247 
    248    let textEncoder = new TextEncoder();
    249    // Derive the EncKey as HKDF(salt=BackupAuthkey, key=ArchiveKey,info=’archive-enc-key’)
    250    let archiveEncKey = await crypto.subtle.deriveKey(
    251      {
    252        name: "HKDF",
    253        salt: backupAuthKey,
    254        info: textEncoder.encode("archive-enc-key"),
    255        hash: "SHA-256",
    256      },
    257      archiveKey,
    258      { name: "AES-GCM", length: 256 },
    259      true /* extractable */,
    260      ["decrypt", "encrypt"]
    261    );
    262 
    263    // Derive the AuthKey as HKDF(salt=BackupAuthkey, key=ArchiveKey, info=‘archive-auth-key’)
    264    // Note - this is distinct for this particular backup. It is not the same as
    265    // the BackupAuthKey from ArchiveEncryptionState. It only uses the
    266    // BackupAuthKey from the ArchiveEncryptionState as a salt.
    267    let authKey = await crypto.subtle.deriveKey(
    268      {
    269        name: "HKDF",
    270        salt: backupAuthKey,
    271        info: textEncoder.encode("archive-auth-key"),
    272        hash: "SHA-256",
    273      },
    274      archiveKey,
    275      { name: "HMAC", hash: "SHA-256", length: 256 },
    276      false /* extractable */,
    277      ["sign", "verify"]
    278    );
    279 
    280    return { archiveEncKey, authKey };
    281  },
    282 
    283  /**
    284   * Given a string decoded from a byte buffer by `TextDecoder.decode`,
    285   * returns the number of bytes (0-3) at the start of the string that
    286   * could not be decoded.
    287   *
    288   * This assumes undecoded content will only appear at the beginning of
    289   * the string. This also assumes undecoded content spans no more than
    290   * 3 bytes. These assumptions are based on running `TextDecoder.decode`
    291   * on an arbitrary span of bytes from a valid UTF-8 string.
    292   *
    293   * @param {string} str
    294   *   String whose beginning you want to inspect for the Unicode replacement
    295   *   character: U+FFFD (�).
    296   * @returns {number}
    297   *   Number of characters, between 0 and 3, at the beginning of the string
    298   *   that could not be decoded by `TextDecoder` and so were replaced by the
    299   *   Unicode replacement character: U+FFFD (�).
    300   *
    301   * @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/fatal
    302   *
    303   * @example countReplacementCharacters("\uFFFD\uFFFD\uFFFD🌞") == 3
    304   * @example countReplacementCharacters("\uFFFD\uFFFD🌞") == 2
    305   * @example countReplacementCharacters("\uFFFD🌞") == 1
    306   * @example countReplacementCharacters("🌞") == 0
    307   */
    308  countReplacementCharacters(str) {
    309    let count = 0;
    310    let lengthToCheck = Math.min(4, str.length);
    311 
    312    for (let index = 0; index < lengthToCheck; index += 1) {
    313      if (str[index] == "\uFFFD") {
    314        count += 1;
    315      }
    316    }
    317 
    318    return count;
    319  },
    320 };