mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			627 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			627 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
// This module expects to be able to load in both main-thread module contexts,
 | 
						|
// as well as ChromeWorker contexts. Do not ChromeUtils.importESModule
 | 
						|
// anything there at the top-level that's not compatible with both contexts.
 | 
						|
 | 
						|
// The ArchiveUtils module is designed to be imported in both worker and
 | 
						|
// main thread contexts.
 | 
						|
import { ArchiveUtils } from "resource:///modules/backup/ArchiveUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(
 | 
						|
  lazy,
 | 
						|
  {
 | 
						|
    BackupError: "resource:///modules/backup/BackupError.mjs",
 | 
						|
    ERRORS: "chrome://browser/content/backup/backup-constants.mjs",
 | 
						|
  },
 | 
						|
  { global: "contextual" }
 | 
						|
);
 | 
						|
 | 
						|
/**
 | 
						|
 * Both ArchiveEncryptor and ArchiveDecryptor maintain an internal nonce used as
 | 
						|
 * a big-endian chunk counter. That counter is Uint8Array(16) array, which makes
 | 
						|
 * doing simple things like adding to the counter somewhat cumbersome.
 | 
						|
 * NonceUtils contains helper methods to do nonce-related management and
 | 
						|
 * arithmetic.
 | 
						|
 */
 | 
						|
export const NonceUtils = {
 | 
						|
  /**
 | 
						|
   * Flips the bit in the nonce to indicate that the nonce will be used for the
 | 
						|
   * last chunk to be encrypted. The specification calls for this bit to be the
 | 
						|
   * 12th bit from the end.
 | 
						|
   *
 | 
						|
   * @param {Uint8Array} nonce
 | 
						|
   *   The nonce to flip the bit on.
 | 
						|
   */
 | 
						|
  setLastChunkOnNonce(nonce) {
 | 
						|
    if (nonce[4] != 0) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Last chunk byte on nonce already set!",
 | 
						|
        lazy.ERRORS.ENCRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    // The nonce is 16 bytes so that we can use DataView / getBigUint64 for
 | 
						|
    // arithmetic, but the spec says that we set the top byte of a 12-byte nonce
 | 
						|
    // to 0x01. We ignore the first 4 bytes of the 16-byte nonce then, and stick
 | 
						|
    // the 1 on the 12th byte (which in big-endian order is the 4th byte).
 | 
						|
    nonce[4] = 1;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns true if `setLastChunkOnNonce` has been called on the nonce already.
 | 
						|
   *
 | 
						|
   * @param {Uint8Array} nonce
 | 
						|
   *   The nonce to check for the bit on.
 | 
						|
   * @returns {boolean}
 | 
						|
   */
 | 
						|
  lastChunkSetOnNonce(nonce) {
 | 
						|
    return nonce[4] == 1;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Increments a nonce by some amount (defaulting to 1). The nonce should be
 | 
						|
   * incremented once per chunk of maximum ARCHIVE_CHUNK_MAX_BYTES_SIZE bytes.
 | 
						|
   * If this incrementing indicates that the number of bytes encrypted exceeds
 | 
						|
   * ARCHIVE_MAX_BYTES_SIZE, an exception is thrown.
 | 
						|
   *
 | 
						|
   * @param {Uint8Array} nonce
 | 
						|
   *   The nonce to increment.
 | 
						|
   * @param {number} [incrementBy=1]
 | 
						|
   *   The amount to increment the nonce by, defaulting to 1.
 | 
						|
   */
 | 
						|
  incrementNonce(nonce, incrementBy = 1) {
 | 
						|
    let view = new DataView(nonce.buffer, 8);
 | 
						|
    let nonceBigInt = view.getBigUint64(0);
 | 
						|
    nonceBigInt += BigInt(incrementBy);
 | 
						|
    if (
 | 
						|
      nonceBigInt * BigInt(ArchiveUtils.ARCHIVE_CHUNK_MAX_BYTES_SIZE) >
 | 
						|
      BigInt(ArchiveUtils.ARCHIVE_MAX_BYTES_SIZE)
 | 
						|
    ) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Exceeded archive maximum size.",
 | 
						|
        lazy.ERRORS.ENCRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    view.setBigUint64(0, nonceBigInt);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * A class that is used to encrypt one or more chunks of a backup archive.
 | 
						|
 * Callers must use the async static initialize() method to create an
 | 
						|
 * ArchiveEncryptor, and then can encrypt() individual chunks. Callers can
 | 
						|
 * call confirm() to generate the serializable JSON block to be included with
 | 
						|
 * the archive.
 | 
						|
 */
 | 
						|
export class ArchiveEncryptor {
 | 
						|
  /**
 | 
						|
   * A hack that lets us ensure that an ArchiveEncryptor cannot be
 | 
						|
   * constructed except via the ArchiveEncryptor.initialize static
 | 
						|
   * method.
 | 
						|
   *
 | 
						|
   * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties#simulating_private_constructors
 | 
						|
   */
 | 
						|
  static #isInternalConstructing = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The RSA-OAEP public key generated via an ArchiveEncryptionState to
 | 
						|
   * encrypt a backup.
 | 
						|
   *
 | 
						|
   * @type {CryptoKey}
 | 
						|
   */
 | 
						|
  #publicKey = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * A unique key generated for the individual archive, used to MAC the
 | 
						|
   * metadata for a backup.
 | 
						|
   *
 | 
						|
   * @type {CryptoKey}
 | 
						|
   */
 | 
						|
  #authKey = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The wrapped archive encryption key material. The archive encryption key
 | 
						|
   * material is randomly generated per backup to derive the encryption keys
 | 
						|
   * for encrypting the backup, and is then wrapped using the #publicKey.
 | 
						|
   *
 | 
						|
   * @type {Uint8Array}
 | 
						|
   */
 | 
						|
  #wrappedArchiveKeyMaterial = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The derived AES-GCM encryption key used to encrypt chunks of the archive.
 | 
						|
   *
 | 
						|
   * @type {CryptoKey}
 | 
						|
   */
 | 
						|
  #encKey = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * A big-endian counter nonce, incremented for each subsequent chunk of the
 | 
						|
   * encrypted archive. The size of the nonce must be a multiple of 8 in order
 | 
						|
   * to simplify the arithmetic via DataView / getBigUint64 / setBigUint64.
 | 
						|
   *
 | 
						|
   * @type {Uint8Array}
 | 
						|
   */
 | 
						|
  #nonce = new Uint8Array(16);
 | 
						|
 | 
						|
  /**
 | 
						|
   * @see ArchiveEncryptor.#isInternalConstructing
 | 
						|
   */
 | 
						|
  constructor() {
 | 
						|
    if (!ArchiveEncryptor.#isInternalConstructing) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "ArchiveEncryptor is not constructable.",
 | 
						|
        lazy.ERRORS.UNKNOWN
 | 
						|
      );
 | 
						|
    }
 | 
						|
    ArchiveEncryptor.#isInternalConstructing = false;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * True if the last chunk flag has been set on the nonce already. Once this
 | 
						|
   * returns true, no further chunks can be encrypted.
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   */
 | 
						|
  #isDone() {
 | 
						|
    return NonceUtils.lastChunkSetOnNonce(this.#nonce);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Constructs an ArchiveEncryptor to prepare it to encrypt chunks of an
 | 
						|
   * archive. This must only be called via the ArchiveEncryptor.initialize
 | 
						|
   * static method.
 | 
						|
   *
 | 
						|
   * @param {CryptoKey} publicKey
 | 
						|
   *   The RSA-OAEP public key generated by an ArchiveEncryptionState.
 | 
						|
   * @param {CryptoKey} backupAuthKey
 | 
						|
   *   The AES-GCM BackupAuthKey generated by an ArchiveEncryptionState.
 | 
						|
   * @returns {Promise<undefined>}
 | 
						|
   */
 | 
						|
  async #initialize(publicKey, backupAuthKey) {
 | 
						|
    this.#publicKey = publicKey;
 | 
						|
 | 
						|
    // Generate a random archive key ArchiveKey. The key material is 256 random
 | 
						|
    // bits.
 | 
						|
    let archiveKeyMaterial = crypto.getRandomValues(new Uint8Array(32));
 | 
						|
 | 
						|
    // Encrypt ArchiveKey with the RSA-OEAP Public Key to form WrappedArchiveKey
 | 
						|
    this.#wrappedArchiveKeyMaterial = new Uint8Array(
 | 
						|
      await crypto.subtle.encrypt(
 | 
						|
        {
 | 
						|
          name: "RSA-OAEP",
 | 
						|
        },
 | 
						|
        this.#publicKey,
 | 
						|
        archiveKeyMaterial
 | 
						|
      )
 | 
						|
    );
 | 
						|
 | 
						|
    let { archiveEncKey, authKey } = await ArchiveUtils.computeEncryptionKeys(
 | 
						|
      archiveKeyMaterial,
 | 
						|
      backupAuthKey
 | 
						|
    );
 | 
						|
    this.#authKey = authKey;
 | 
						|
    this.#encKey = archiveEncKey;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Encrypts a chunk from a backup archive.
 | 
						|
   *
 | 
						|
   * @param {Uint8Array} plaintextChunk
 | 
						|
   *   The plaintext chunk of bytes to encrypt.
 | 
						|
   * @param {boolean} [isLastChunk=false]
 | 
						|
   *   Callers should set this to true if the chunk being encrypted is the
 | 
						|
   *   last chunk. Once this is done, no additional chunk can be encrypted.
 | 
						|
   * @returns {Promise<Uint8Array>}
 | 
						|
   */
 | 
						|
  async encrypt(plaintextChunk, isLastChunk = false) {
 | 
						|
    if (this.#isDone()) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Cannot encrypt any more chunks with this ArchiveEncryptor.",
 | 
						|
        lazy.ERRORS.ENCRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (plaintextChunk.byteLength > ArchiveUtils.ARCHIVE_CHUNK_MAX_BYTES_SIZE) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        `Chunk is too large to encrypt: ${plaintextChunk.byteLength} bytes`,
 | 
						|
        lazy.ERRORS.ENCRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      plaintextChunk.byteLength != ArchiveUtils.ARCHIVE_CHUNK_MAX_BYTES_SIZE &&
 | 
						|
      !isLastChunk
 | 
						|
    ) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Only last chunk can be smaller than the chunk max size",
 | 
						|
        lazy.ERRORS.ENCRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (isLastChunk) {
 | 
						|
      NonceUtils.setLastChunkOnNonce(this.#nonce);
 | 
						|
    }
 | 
						|
 | 
						|
    let ciphertextChunk;
 | 
						|
    try {
 | 
						|
      ciphertextChunk = await crypto.subtle.encrypt(
 | 
						|
        {
 | 
						|
          name: "AES-GCM",
 | 
						|
          // Take only the last 12 bytes of the nonce, since the WebCrypto API
 | 
						|
          // starts to behave differently when the IV is > 96 bits.
 | 
						|
          iv: this.#nonce.subarray(4),
 | 
						|
          tagLength: ArchiveUtils.TAG_LENGTH,
 | 
						|
        },
 | 
						|
        this.#encKey,
 | 
						|
        plaintextChunk
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Failed to encrypt a chunk.",
 | 
						|
        lazy.ERRORS.ENCRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    NonceUtils.incrementNonce(this.#nonce);
 | 
						|
 | 
						|
    return new Uint8Array(ciphertextChunk);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Signs the metadata of a backup archive. This signature is used to both
 | 
						|
   * provide an easy way of checking that a recovery code is valid, but also to
 | 
						|
   * ensure that the metadata has not been tampered with. The returned Promise
 | 
						|
   * resolves with the JSON block that can be written to the backup archive
 | 
						|
   * file.
 | 
						|
   *
 | 
						|
   * @param {object} meta
 | 
						|
   *   The metadata of a backup archive.
 | 
						|
   * @param {Uint8Array} wrappedSecrets
 | 
						|
   *   The encrypted backup secrets computed by ArchiveEncryptionState.
 | 
						|
   * @param {Uint8Array} salt
 | 
						|
   *   The salt used by ArchiveEncryptionState for the PBKDF2 stretching of the
 | 
						|
   *   recovery code.
 | 
						|
   * @param {Uint8Array} nonce
 | 
						|
   *   The nonce used by ArchiveEncryptionState when wrapping the private key
 | 
						|
   *   and OSKeyStore secret
 | 
						|
   * @returns {Promise<Uint8Array>}
 | 
						|
   *   The confirmation signature of the JSON block.
 | 
						|
   */
 | 
						|
  async confirm(meta, wrappedSecrets, salt, nonce) {
 | 
						|
    let textEncoder = new TextEncoder();
 | 
						|
    let metaBytes = textEncoder.encode(JSON.stringify(meta));
 | 
						|
    let confirmation = new Uint8Array(
 | 
						|
      await crypto.subtle.sign("HMAC", this.#authKey, metaBytes)
 | 
						|
    );
 | 
						|
 | 
						|
    return {
 | 
						|
      version: ArchiveUtils.SCHEMA_VERSION,
 | 
						|
      encConfig: {
 | 
						|
        wrappedSecrets: ArchiveUtils.arrayToBase64(wrappedSecrets),
 | 
						|
        wrappedArchiveKeyMaterial: ArchiveUtils.arrayToBase64(
 | 
						|
          this.#wrappedArchiveKeyMaterial
 | 
						|
        ),
 | 
						|
        salt: ArchiveUtils.arrayToBase64(salt),
 | 
						|
        nonce: ArchiveUtils.arrayToBase64(nonce),
 | 
						|
        confirmation: ArchiveUtils.arrayToBase64(confirmation),
 | 
						|
      },
 | 
						|
      meta,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initializes an ArchiveEncryptor so that a caller can begin encrypting
 | 
						|
   * chunks of a backup archive.
 | 
						|
   *
 | 
						|
   * @param {CryptoKey} publicKey
 | 
						|
   *   The RSA-OAEP public key from an ArchiveEncryptionState.
 | 
						|
   * @param {CryptoKey} backupAuthKey
 | 
						|
   *   The AES-GCM BackupAuthKey from an ArchiveEncryptionState.
 | 
						|
   * @returns {Promise<ArchiveEncryptor>}
 | 
						|
   */
 | 
						|
  static async initialize(publicKey, backupAuthKey) {
 | 
						|
    ArchiveEncryptor.#isInternalConstructing = true;
 | 
						|
    let instance = new ArchiveEncryptor();
 | 
						|
    await instance.#initialize(publicKey, backupAuthKey);
 | 
						|
    return instance;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * A class that is used to decrypt one or more chunks of a backup archive.
 | 
						|
 * Callers must use the async static initialize() method to create an
 | 
						|
 * ArchiveDecryptor, and then can decrypt() individual chunks.
 | 
						|
 */
 | 
						|
export class ArchiveDecryptor {
 | 
						|
  /**
 | 
						|
   * A hack that lets us ensure that an ArchiveEncryptor cannot be
 | 
						|
   * constructed except via the ArchiveEncryptor.initialize static
 | 
						|
   * method.
 | 
						|
   *
 | 
						|
   * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties#simulating_private_constructors
 | 
						|
   */
 | 
						|
  static #isInternalConstructing = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The unwrapped RSA-OAEP private key extracted from the wrapped secrets of
 | 
						|
   * a backup.
 | 
						|
   *
 | 
						|
   * @type {CryptoKey}
 | 
						|
   */
 | 
						|
  #privateKey = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The unique AES-GCM encryption key used to encrypt this particular backup,
 | 
						|
   * derived from the wrappedArchiveKeyMaterial.
 | 
						|
   *
 | 
						|
   * @type {CryptoKey}
 | 
						|
   */
 | 
						|
  #archiveEncKey = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @see ArchiveDecryptor.OSKeyStoreSecret
 | 
						|
   *
 | 
						|
   * @type {string}
 | 
						|
   */
 | 
						|
  #_OSKeyStoreSecret = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * A big-endian counter nonce, incremented for each subsequent chunk of the
 | 
						|
   * encrypted archive. The size of the nonce must be a multiple of 8 in order
 | 
						|
   * to simplify the arithmetic via DataView / getBigUint64 / setBigUint64.
 | 
						|
   *
 | 
						|
   * @type {Uint8Array}
 | 
						|
   */
 | 
						|
  #nonce = new Uint8Array(16);
 | 
						|
 | 
						|
  /**
 | 
						|
   * @see ArchiveDecryptor.#isInternalConstructing
 | 
						|
   */
 | 
						|
  constructor() {
 | 
						|
    if (!ArchiveDecryptor.#isInternalConstructing) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "ArchiveDecryptor is not constructable.",
 | 
						|
        lazy.ERRORS.UNKNOWN
 | 
						|
      );
 | 
						|
    }
 | 
						|
    ArchiveDecryptor.#isInternalConstructing = false;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * The unwrapped OSKeyStore secret that was stored within the JSON block.
 | 
						|
   *
 | 
						|
   * @type {string}
 | 
						|
   */
 | 
						|
  get OSKeyStoreSecret() {
 | 
						|
    if (!this.isDone()) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Cannot access OSKeyStoreSecret until all chunks are decrypted.",
 | 
						|
        lazy.ERRORS.UNKNOWN
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return this.#_OSKeyStoreSecret;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initializes an ArchiveDecryptor to decrypt a backup. This will throw if
 | 
						|
   * the recovery code is not valid, or the meta property of the JSON block
 | 
						|
   * appears to have been tampered with since signing. It is assumed that a
 | 
						|
   * caller of this function has already validated that the JSON block has been
 | 
						|
   * validated against the appropriate ArchiveJSONBlock JSON schema.
 | 
						|
   *
 | 
						|
   * @param {string} recoveryCode
 | 
						|
   *   The recovery code originally used to encrypt the backup archive.
 | 
						|
   * @param {object} jsonBlock
 | 
						|
   *   The parsed JSON block that was stored with the backup archive. See the
 | 
						|
   *   ArchiveJSONBlock JSON schema.
 | 
						|
   */
 | 
						|
  async #initialize(recoveryCode, jsonBlock) {
 | 
						|
    if (jsonBlock.version > ArchiveUtils.SCHEMA_VERSION) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        `JSON block version ${jsonBlock.version} is greater than we can handle`,
 | 
						|
        lazy.ERRORS.UNSUPPORTED_BACKUP_VERSION
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    let { encConfig, meta } = jsonBlock;
 | 
						|
    let salt = ArchiveUtils.stringToArray(encConfig.salt);
 | 
						|
    let nonce = ArchiveUtils.stringToArray(encConfig.nonce);
 | 
						|
    let wrappedSecrets = ArchiveUtils.stringToArray(encConfig.wrappedSecrets);
 | 
						|
    let wrappedArchiveKeyMaterial = ArchiveUtils.stringToArray(
 | 
						|
      encConfig.wrappedArchiveKeyMaterial
 | 
						|
    );
 | 
						|
    let confirmation = ArchiveUtils.stringToArray(encConfig.confirmation);
 | 
						|
 | 
						|
    // First, recompute the BackupAuthKey and BackupEncKey from the recovery
 | 
						|
    // code and salt
 | 
						|
    let { backupAuthKey, backupEncKey } = await ArchiveUtils.computeBackupKeys(
 | 
						|
      recoveryCode,
 | 
						|
      salt
 | 
						|
    );
 | 
						|
 | 
						|
    // Next, unwrap the secrets - the private RSA-OAEP key, and the
 | 
						|
    // OSKeyStore secret.
 | 
						|
    let unwrappedSecrets;
 | 
						|
    try {
 | 
						|
      unwrappedSecrets = new Uint8Array(
 | 
						|
        await crypto.subtle.decrypt(
 | 
						|
          {
 | 
						|
            name: "AES-GCM",
 | 
						|
            iv: nonce,
 | 
						|
          },
 | 
						|
          backupEncKey,
 | 
						|
          wrappedSecrets
 | 
						|
        )
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      throw new lazy.BackupError("Unauthenticated", lazy.ERRORS.UNAUTHORIZED);
 | 
						|
    }
 | 
						|
 | 
						|
    let textDecoder = new TextDecoder();
 | 
						|
    let secrets = JSON.parse(textDecoder.decode(unwrappedSecrets));
 | 
						|
 | 
						|
    this.#privateKey = await crypto.subtle.importKey(
 | 
						|
      "jwk",
 | 
						|
      secrets.privateKey,
 | 
						|
      { name: "RSA-OAEP", hash: "SHA-256" },
 | 
						|
      true /* extractable */,
 | 
						|
      ["decrypt"]
 | 
						|
    );
 | 
						|
 | 
						|
    this.#_OSKeyStoreSecret = secrets.OSKeyStoreSecret;
 | 
						|
 | 
						|
    // Now use the private key to decrypt the wrappedArchiveKeyMaterial
 | 
						|
    let archiveKeyMaterial = await crypto.subtle.decrypt(
 | 
						|
      {
 | 
						|
        name: "RSA-OAEP",
 | 
						|
      },
 | 
						|
      this.#privateKey,
 | 
						|
      wrappedArchiveKeyMaterial
 | 
						|
    );
 | 
						|
 | 
						|
    let { archiveEncKey, authKey } = await ArchiveUtils.computeEncryptionKeys(
 | 
						|
      archiveKeyMaterial,
 | 
						|
      backupAuthKey
 | 
						|
    );
 | 
						|
 | 
						|
    this.#archiveEncKey = archiveEncKey;
 | 
						|
 | 
						|
    // Now ensure that the backup metadata has not been tampered with.
 | 
						|
    let textEncoder = new TextEncoder();
 | 
						|
    let jsonBlockBytes = textEncoder.encode(JSON.stringify(meta));
 | 
						|
    let verified = await crypto.subtle.verify(
 | 
						|
      "HMAC",
 | 
						|
      authKey,
 | 
						|
      confirmation,
 | 
						|
      jsonBlockBytes
 | 
						|
    );
 | 
						|
    if (!verified) {
 | 
						|
      this.#poisonSelf();
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Backup has been corrupted.",
 | 
						|
        lazy.ERRORS.CORRUPTED_ARCHIVE
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Decrypts a chunk from a backup archive. This will throw if the cipherText
 | 
						|
   * chunk appears to be too large (is greater than ARCHIVE_CHUNK_MAX)
 | 
						|
   *
 | 
						|
   * @param {Uint8Array} ciphertextChunk
 | 
						|
   *   The ciphertext chunk of bytes to decrypt.
 | 
						|
   * @param {boolean} [isLastChunk=false]
 | 
						|
   *   Callers should set this to true if the chunk being decrypted is the
 | 
						|
   *   last chunk. Once this is done, no additional chunks can be decrypted.
 | 
						|
   * @returns {Promise<Uint8Array>}
 | 
						|
   */
 | 
						|
  async decrypt(ciphertextChunk, isLastChunk = false) {
 | 
						|
    if (this.isDone()) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Cannot decrypt any more chunks with this ArchiveDecryptor.",
 | 
						|
        lazy.ERRORS.DECRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      ciphertextChunk.byteLength >
 | 
						|
      ArchiveUtils.ARCHIVE_CHUNK_MAX_BYTES_SIZE + ArchiveUtils.TAG_LENGTH_BYTES
 | 
						|
    ) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        `Chunk is too large to decrypt: ${ciphertextChunk.byteLength} bytes`,
 | 
						|
        lazy.ERRORS.DECRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      ciphertextChunk.byteLength !=
 | 
						|
        ArchiveUtils.ARCHIVE_CHUNK_MAX_BYTES_SIZE +
 | 
						|
          ArchiveUtils.TAG_LENGTH_BYTES &&
 | 
						|
      !isLastChunk
 | 
						|
    ) {
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Only last chunk can be smaller than the chunk max size",
 | 
						|
        lazy.ERRORS.DECRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (isLastChunk) {
 | 
						|
      NonceUtils.setLastChunkOnNonce(this.#nonce);
 | 
						|
    }
 | 
						|
 | 
						|
    let plaintextChunk;
 | 
						|
 | 
						|
    try {
 | 
						|
      plaintextChunk = await crypto.subtle.decrypt(
 | 
						|
        {
 | 
						|
          name: "AES-GCM",
 | 
						|
          // Take only the last 12 bytes of the nonce, since the WebCrypto API
 | 
						|
          // starts to behave differently when the IV is > 96 bits.
 | 
						|
          iv: this.#nonce.subarray(4),
 | 
						|
          tagLength: ArchiveUtils.TAG_LENGTH,
 | 
						|
        },
 | 
						|
        this.#archiveEncKey,
 | 
						|
        ciphertextChunk
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      this.#poisonSelf();
 | 
						|
      throw new lazy.BackupError(
 | 
						|
        "Failed to decrypt a chunk.",
 | 
						|
        lazy.ERRORS.DECRYPTION_FAILED
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    NonceUtils.incrementNonce(this.#nonce);
 | 
						|
 | 
						|
    return new Uint8Array(plaintextChunk);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Something has gone wrong during decryption. We want to make sure we cannot
 | 
						|
   * possibly decrypt anything further, so we blow away our internal state,
 | 
						|
   * effectively breaking this ArchiveDecryptor.
 | 
						|
   */
 | 
						|
  #poisonSelf() {
 | 
						|
    this.#privateKey = null;
 | 
						|
    this.#archiveEncKey = null;
 | 
						|
    this.#_OSKeyStoreSecret = null;
 | 
						|
    this.#nonce = null;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * True if the last chunk flag has been set on the nonce already. Once this
 | 
						|
   * returns true, no further chunks can be decrypted.
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   */
 | 
						|
  isDone() {
 | 
						|
    return NonceUtils.lastChunkSetOnNonce(this.#nonce);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initializes an ArchiveDecryptor using the recovery code and the JSON
 | 
						|
   * block that was extracted from the archive. The caller is expected to have
 | 
						|
   * already checked that the JSON block adheres to the ArchiveJSONBlock
 | 
						|
   * schema. The initialization may fail, and the Promise rejected, if the
 | 
						|
   * recovery code is not correct, or the meta data of the JSON block has
 | 
						|
   * changed since it was signed.
 | 
						|
   *
 | 
						|
   * @param {string} recoveryCode
 | 
						|
   *   The recovery code to attempt to begin decryption with.
 | 
						|
   * @param {object} jsonBlock
 | 
						|
   *   See the ArchiveJSONBlock schema for details.
 | 
						|
   * @returns {Promise<ArchiveDecryptor>}
 | 
						|
   */
 | 
						|
  static async initialize(recoveryCode, jsonBlock) {
 | 
						|
    ArchiveDecryptor.#isInternalConstructing = true;
 | 
						|
    let instance = new ArchiveDecryptor();
 | 
						|
    await instance.#initialize(recoveryCode, jsonBlock);
 | 
						|
    return instance;
 | 
						|
  }
 | 
						|
}
 |