up archive, in bytes, prior to base64 encoding.
   *
   * @type {number}
   */
  get ARCHIVE_MAX_BYTES_SIZE() {
    return 34359738368; // 2 ^ 35 bytes (32 GiB)
  },

  /**
   * The AES-GCM tag length applied to each encrypted chunk, in bits.
   *
   * @type {number}
   */
  get TAG_LENGTH() {
    return 128;
  },

  /**
   * The AES-GCM tag length applied to each encrypted chunk, in bytes.
   *
   * @type {number}
   */
  get TAG_LENGTH_BYTES() {
    return this.TAG_LENGTH / 8;
  },

  /**
   * @typedef {object} ComputeKeysResult
   * @property {Uint8Array} backupAuthKey
   *   The computed BackupAuthKey. This is returned as a Uint8Array because
   *   this key is used as a salt for other derived keys.
   * @property {CryptoKey} backupEncKey
   *   The computed BackupEncKey. This is an AES-GCM key used to encrypt and
   *   decrypt the secrets contained within a backup archive.
   */

  /**
   * Computes the BackupAuthKey and BackupEncKey from a recovery code and a
   * salt.
   *
   * @param {string} recoveryCode
   *   A recovery code. Callers are responsible for checking the length /
   *   entropy of the recovery code.
   * @param {Uint8Array} salt
   *   A salt that should be used for computing the keys.
   * @returns {ComputeKeysResult}
   */
  async computeBackupKeys(recoveryCode, salt) {
    let textEncoder = new TextEncoder();
    let recoveryCodeBytes = textEncoder.encode(recoveryCode);

    let keyMaterial = await crypto.subtle.importKey(
      "raw",
      recoveryCodeBytes,
      "PBKDF2",
      false /* extractable */,
      ["deriveBits"]
    );

    // Then we derive the "backup key", using
    // PBKDF2(recoveryCode, saltPrefix || SALT_SUFFIX, SHA-256, 600,000)
    const ITERATIONS = 600_000;

    let backupKeyBits = await crypto.subtle.deriveBits(
      {
        name: "PBKDF2",
        salt,
        iterations: ITERATIONS,
        hash: "SHA-256",
      },
      keyMaterial,
      256
    );

    // This is a little awkward, but the way that the WebCrypto API currently
    // works is that we have to read in those bits as a "raw HKDF key", and
    // only then can we derive our other HKDF keys from it.
    let backupKeyHKDF = await crypto.subtle.importKey(
      "raw",
      backupKeyBits,
      {
        name: "HKDF",
        hash: "SHA-256",
      },
      false /* extractable */,
      ["deriveKey", "deriveBits"]
    );

    // Re-derive BackupAuthKey as HKDF(backupKey, “backupkey-auth”, salt=None)
    let backupAuthKey = new Uint8Array(
      await crypto.subtle.deriveBits(
        {
          name: "HKDF",
          salt: new Uint8Array(0), // no salt
          info: textEncoder.encode("backupkey-auth"),
          hash: "SHA-256",
        },
        backupKeyHKDF,
        256
      )
    );

    let backupEncKey = await crypto.subtle.deriveKey(
      {
        name: "HKDF",
        salt: new Uint8Array(0), // no salt
        info: textEncoder.encode("backupkey-enc-key"),
        hash: "SHA-256",
      },
      backupKeyHKDF,
      { name: "AES-GCM", length: 256 },
      false /* extractable */,
      ["encrypt", "decrypt", "wrapKey"]
    );

    return { backupAuthKey, backupEncKey };
  },

  /**
   * @typedef {object} ComputeEncryptionKeysResult
   * @property {CryptoKey} archiveEncKey
   *   This is an AES-GCM key used to encrypt chunks of a backup archive.
   * @property {CryptoKey} authKey
   *   This is a unique authKey for a particular backup that lets us
   *   generate the confirmation HMAC for the backup metadata.
   */

  /**
   * Computes the encryption keys for a particular archive.
   *
   * @param {Uint8Array} archiveKeyMaterial
   *   The key material used to generate the encryption keys.
   * @param {Uint8Array} backupAuthKey
   *   The backupAuthKey returned from computeBackupKeys.
   * @returns {ComputeEncryptionKeysResult}
   */
  async computeEncryptionKeys(archiveKeyMaterial, backupAuthKey) {
    let archiveKey = await crypto.subtle.importKey(
      "raw",
      archiveKeyMaterial,
      { name: "HKDF" },
      false, // Not extractable
      ["deriveKey", "deriveBits"]
    );

    let textEncoder = new TextEncoder();
    // Derive the EncKey as HKDF(salt=BackupAuthkey, key=ArchiveKey,info=’archive-enc-key’)
    let archiveEncKey = await crypto.subtle.deriveKey(
      {
        name: "HKDF",
        salt: backupAuthKey,
        info: textEncoder.encode("archive-enc-key"),
        hash: "SHA-256",
      },
      archiveKey,
      { name: "AES-GCM", length: 256 },
      true /* extractable */,
      ["decrypt", "encrypt"]
    );

    // Derive the AuthKey as HKDF(salt=BackupAuthkey, key=ArchiveKey, info=‘archive-auth-key’)
    // Note - this is distinct for this particular backup. It is not the same as
    // the BackupAuthKey from ArchiveEncryptionState. It only uses the
    // BackupAuthKey from the ArchiveEncryptionState as a salt.
    let authKey = await crypto.subtle.deriveKey(
      {
        name: "HKDF",
        salt: backupAuthKey,
        info: textEncoder.encode("archive-auth-key"),
        hash: "SHA-256",
      },
      archiveKey,
      { name: "HMAC", hash: "SHA-256", length: 256 },
      false /* extractable */,
      ["sign", "verify"]
    );

    return { archiveEncKey, authKey };
  },

  /**
   * Given a string decoded from a byte buffer by `TextDecoder.decode`,
   * returns the number of bytes (0-3) at the start of the string that
   * could not be decoded.
   *
   * This assumes undecoded content will only appear at the beginning of
   * the string. This also assumes undecoded content spans no more than
   * 3 bytes. These assumptions are based on running `TextDecoder.decode`
   * on an arbitrary span of bytes from a valid UTF-8 string.
   *
   * @param {string} str
   *   String whose beginning you want to inspect for the Unicode replacement
   *   character: U+FFFD (�).
   * @returns {number}
   *   Number of characters, between 0 and 3, at the beginning of the string
   *   that could not be decoded by `TextDecoder` and so were replaced by the
   *   Unicode replacement character: U+FFFD (�).
   *
   * @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/fatal
   *
   * @example countReplacementCharacters("\uFFFD\uFFFD\uFFFD🌞") == 3
   * @example countReplacementCharacters("\uFFFD\uFFFD🌞") == 2
   * @example countReplacementCharacters("\uFFFD🌞") == 1
   * @example countReplacementCharacters("🌞") == 0
   */
  countReplacementCharacters(str) {
    let count = 0;
    let lengthToCheck = Math.min(4, str.length);

    for (let index = 0; index < lengthToCheck; index += 1) {
      if (str[index] == "\uFFFD") {
        count += 1;
      }
    }

    return count;
  },
};
PK