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