tor-browser

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

ArchiveEncryptionState.sys.mjs (11019B)


      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 lazy = {};
      6 
      7 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
      8  return console.createInstance({
      9    prefix: "BackupService::ArchiveEncryption",
     10    maxLogLevel: Services.prefs.getBoolPref("browser.backup.log", false)
     11      ? "Debug"
     12      : "Warn",
     13  });
     14 });
     15 
     16 ChromeUtils.defineESModuleGetters(lazy, {
     17  ArchiveUtils: "resource:///modules/backup/ArchiveUtils.sys.mjs",
     18  OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
     19  BackupError: "resource:///modules/backup/BackupError.mjs",
     20  ERRORS: "chrome://browser/content/backup/backup-constants.mjs",
     21 });
     22 
     23 /**
     24 * ArchiveEncryptionState encapsulates key primitives and wrapped secrets that
     25 * can be safely serialized to the filesystem. An ArchiveEncryptionState is
     26 * used to compute the necessary keys for encrypting a backup archive.
     27 */
     28 export class ArchiveEncryptionState {
     29  /**
     30   * A hack that lets us ensure that an ArchiveEncryptionState cannot be
     31   * constructed except via the ArchiveEncryptionState.initialize static
     32   * method.
     33   *
     34   * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties#simulating_private_constructors
     35   */
     36  static #isInternalConstructing = false;
     37 
     38  /**
     39   * A reference to an object holding the current state of the
     40   * ArchiveEncryptionState instance. When this reference is null, encryption
     41   * is not considered enabled.
     42   */
     43  #state = null;
     44 
     45  /**
     46   * The current version number of the ArchiveEncryptionState. This is encoded
     47   * in the serialized state, and is also used during calculation of the salt
     48   * in enable().
     49   *
     50   * @type {number}
     51   */
     52  static get VERSION() {
     53    return 1;
     54  }
     55 
     56  /**
     57   * The number of characters to generate with a CSRNG (crypto.getRandomValues)
     58   * if no recovery code is passed in to enable();
     59   *
     60   * @type {number}
     61   */
     62  static get GENERATED_RECOVERY_CODE_LENGTH() {
     63    return 14;
     64  }
     65 
     66  /**
     67   * The RSA-OAEP public key that will be used to derive keys for encrypting
     68   * backups.
     69   *
     70   * @type {CryptoKey}
     71   */
     72  get publicKey() {
     73    return this.#state.publicKey;
     74  }
     75 
     76  /**
     77   * The AES-GCM key that will be used to authenticate the owner of the backup.
     78   *
     79   * @type {CryptoKey}
     80   */
     81  get backupAuthKey() {
     82    return this.#state.backupAuthKey;
     83  }
     84 
     85  /**
     86   * A salt computed for the PBKDF2 stretching of the recovery code.
     87   *
     88   * @type {Uint8Array}
     89   */
     90  get salt() {
     91    return this.#state.salt;
     92  }
     93 
     94  /**
     95   * A nonce computed when wrapping the private key and OSKeyStore secret.
     96   *
     97   * @type {Uint8Array}
     98   */
     99  get nonce() {
    100    return this.#state.nonce;
    101  }
    102 
    103  /**
    104   * The wrapped static secrets, including the RSA-OAEP private key, and the
    105   * OSKeyStore secret.
    106   *
    107   * @type {Uint8Array}
    108   */
    109  get wrappedSecrets() {
    110    return this.#state.wrappedSecrets;
    111  }
    112 
    113  constructor() {
    114    if (!ArchiveEncryptionState.#isInternalConstructing) {
    115      throw new lazy.BackupError(
    116        "ArchiveEncryptionState is not constructable.",
    117        lazy.ERRORS.UNKNOWN
    118      );
    119    }
    120    ArchiveEncryptionState.#isInternalConstructing = false;
    121  }
    122 
    123  /**
    124   * Calculates various encryption keys and other information necessary to
    125   * encrypt backups, based on the passed in recoveryCode.
    126   *
    127   * This will throw if encryption is already enabled for this
    128   * ArchiveEncryptionState.
    129   *
    130   * @throws {Exception}
    131   * @param {string} [recoveryCode=null]
    132   *   A recovery code that will be used to drive the various encryption keys
    133   *   and data for backup encryption. If not supplied by the caller, a
    134   *   recovery code will be generated.
    135   * @returns {Promise<string>}
    136   *   Resolves with the recovery code string. If callers did not pass the
    137   *   recovery code in as an argument, they should not store it. They should
    138   *   instead display this string to the user, and then forget it altogether.
    139   */
    140  async #enable(recoveryCode = null) {
    141    lazy.logConsole.debug("Creating new enabled ArchiveEncryptionState");
    142 
    143    lazy.logConsole.debug("Generating an RSA-OEAP keyPair");
    144    let keyPair = await crypto.subtle.generateKey(
    145      {
    146        name: "RSA-OAEP",
    147        modulusLength: 2048,
    148        publicExponent: new Uint8Array([1, 0, 1]),
    149        hash: { name: "SHA-256" },
    150      },
    151      true /* extractable */,
    152      ["encrypt", "decrypt"]
    153    );
    154 
    155    if (!recoveryCode) {
    156      // A recovery code wasn't provided, so we'll generate one using
    157      // getRandomValues, and make sure it's GENERATED_RECOVERY_CODE_LENGTH
    158      // characters long.
    159      recoveryCode = "";
    160      // We've intentionally replaced some lookalike characters (O, o, 0, l, I,
    161      // 1) with symbols.
    162      const charset =
    163        "ABCDEFGH#JKLMN@PQRSTUVWXYZabcdefgh=jklmn+pqrstuvwxyz%!23456789";
    164      // getRandomValues will return a value between 0-255. In order to not
    165      // gain a bias on any particular character (due to wrap-around), we'll
    166      // ensure that we only consider random values that are less than or
    167      // equal to the highest multiple of charset.length that is less than
    168      // 255.
    169      let highestMultiple =
    170        Math.floor((255 /* upper limit */ - 1) / charset.length) *
    171        charset.length;
    172 
    173      while (
    174        recoveryCode.length <
    175        ArchiveEncryptionState.GENERATED_RECOVERY_CODE_LENGTH
    176      ) {
    177        let randomValue = new Uint8Array(1);
    178        crypto.getRandomValues(randomValue);
    179        // If the random value is higher than highestMultiple, try again.
    180        if (randomValue > highestMultiple) {
    181          continue;
    182        }
    183        // Otherwise, we're within the highest multiple, meaning we can mod
    184        // the generated number to choose a character from charset.
    185        let randomIndex = randomValue % charset.length;
    186        recoveryCode += charset[randomIndex];
    187      }
    188    }
    189 
    190    // Next, we generate a 32-byte salt, and then concatenate a static suffix
    191    // to it, including the version number.
    192    lazy.logConsole.debug("Creating salt");
    193    let textEncoder = new TextEncoder();
    194    const SALT_SUFFIX = textEncoder.encode(
    195      "backupkey-v" + ArchiveEncryptionState.VERSION
    196    );
    197    let saltPrefix = new Uint8Array(32);
    198    crypto.getRandomValues(saltPrefix);
    199 
    200    let salt = new Uint8Array(saltPrefix.length + SALT_SUFFIX.length);
    201    salt.set(saltPrefix);
    202    salt.set(SALT_SUFFIX, saltPrefix.length);
    203 
    204    let { backupAuthKey, backupEncKey } =
    205      await lazy.ArchiveUtils.computeBackupKeys(recoveryCode, salt);
    206 
    207    lazy.logConsole.debug("Encrypting secrets with encKey");
    208    const NONCE_SIZE = 96;
    209    let nonce = crypto.getRandomValues(new Uint8Array(NONCE_SIZE));
    210 
    211    let secrets = JSON.stringify({
    212      privateKey: await crypto.subtle.exportKey("jwk", keyPair.privateKey),
    213      OSKeyStoreSecret: await lazy.OSKeyStore.exportRecoveryPhrase(),
    214    });
    215    let secretsBytes = textEncoder.encode(secrets);
    216 
    217    let wrappedSecrets = new Uint8Array(
    218      await crypto.subtle.encrypt(
    219        {
    220          name: "AES-GCM",
    221          iv: nonce,
    222        },
    223        backupEncKey,
    224        secretsBytes
    225      )
    226    );
    227 
    228    this.#state = {
    229      publicKey: keyPair.publicKey,
    230      salt,
    231      backupAuthKey,
    232      nonce,
    233      wrappedSecrets,
    234    };
    235 
    236    return recoveryCode;
    237  }
    238 
    239  /**
    240   * Serializes an ArchiveEncryptionState instance into an object that can be
    241   * safely persisted to disk.
    242   *
    243   * @returns {Promise<object>}
    244   */
    245  async serialize() {
    246    let publicKey = await crypto.subtle.exportKey("jwk", this.#state.publicKey);
    247    let salt = lazy.ArchiveUtils.arrayToBase64(this.#state.salt);
    248    let backupAuthKey = lazy.ArchiveUtils.arrayToBase64(
    249      this.#state.backupAuthKey
    250    );
    251    let nonce = lazy.ArchiveUtils.arrayToBase64(this.#state.nonce);
    252    let wrappedSecrets = lazy.ArchiveUtils.arrayToBase64(
    253      this.#state.wrappedSecrets
    254    );
    255    let result = {
    256      publicKey,
    257      salt,
    258      backupAuthKey,
    259      nonce,
    260      wrappedSecrets,
    261      version: ArchiveEncryptionState.VERSION,
    262    };
    263 
    264    return result;
    265  }
    266 
    267  /**
    268   * Deserializes an object created via serialize() and updates its internal
    269   * state to match the deserialization.
    270   *
    271   * @param {object} stateData
    272   *   The object generated via serialize()
    273   * @returns {Promise<undefined>}
    274   */
    275  async #deserialize(stateData) {
    276    lazy.logConsole.debug(
    277      "Deserializing from state with version ",
    278      stateData.version
    279    );
    280 
    281    // If we ever need to do a migration from one ArchiveEncryptionState
    282    // version to another, this is where we might do it. We don't currently
    283    // have any need to do migrations just yet though, so any version that
    284    // doesn't match the one that we can accept is rejected.
    285    if (stateData.version != ArchiveEncryptionState.VERSION) {
    286      throw new lazy.BackupError(
    287        "The ArchiveEncryptionState version is from a newer version.",
    288        lazy.ERRORS.UNSUPPORTED_BACKUP_VERSION
    289      );
    290    }
    291 
    292    let publicKey = await crypto.subtle.importKey(
    293      "jwk",
    294      stateData.publicKey,
    295      { name: "RSA-OAEP", hash: "SHA-256" },
    296      true /* extractable */,
    297      ["encrypt"]
    298    );
    299    let backupAuthKey = lazy.ArchiveUtils.stringToArray(
    300      stateData.backupAuthKey
    301    );
    302    let salt = lazy.ArchiveUtils.stringToArray(stateData.salt);
    303    let nonce = lazy.ArchiveUtils.stringToArray(stateData.nonce);
    304    let wrappedSecrets = lazy.ArchiveUtils.stringToArray(
    305      stateData.wrappedSecrets
    306    );
    307 
    308    this.#state = {
    309      publicKey,
    310      backupAuthKey,
    311      salt,
    312      nonce,
    313      wrappedSecrets,
    314    };
    315  }
    316 
    317  /**
    318   * @typedef {object} InitializationResult
    319   * @property {string|undefined} recoveryCode
    320   *   The generated recovery code if the initialization happened without
    321   *   deserialization.
    322   * @property {ArchiveEncryptionState} instance
    323   *   The constructed ArchiveEncryptionState.
    324   */
    325 
    326  /**
    327   * Constructs a new ArchiveEncryptionState. If a stateData object is passed,
    328   * the ArchiveEncryptionState will attempt to be deserialized from it -
    329   * otherwise, new state data will be generated automatically. This might
    330   * reject if the user is prompted to authenticate to their OSKeyStore, and
    331   * they cancel the authentication.
    332   *
    333   * @param {object|string|undefined} stateDataOrRecoveryCode
    334   *   Either the object generated via serialize(), a recovery code to be
    335   *   used to generate the state, or undefined.
    336   * @returns {Promise<InitializationResult>}
    337   */
    338  static async initialize(stateDataOrRecoveryCode) {
    339    ArchiveEncryptionState.#isInternalConstructing = true;
    340    let instance = new ArchiveEncryptionState();
    341    if (typeof stateDataOrRecoveryCode == "object") {
    342      await instance.#deserialize(stateDataOrRecoveryCode);
    343      return { instance };
    344    }
    345    let recoveryCode = await instance.#enable(stateDataOrRecoveryCode);
    346    return { instance, recoveryCode };
    347  }
    348 }