Bug 1900892 - Part 1: Factor out computeBackupKeys to ArchiveUtils. r=djackson,backup-reviewers,kpatenio
Factoring this out, as computing these keys is something that we need to do both when generating the ArchiveEncryptionState, as well as when performing a decryption. This also renames "authKey" and "encKey" in ArchiveEncryptionState to use "backupAuthKey" and "backupEncKey", as these are more in-line with what the encryption design document uses (and because there are "authKeys" and "encKeys" that will be used by the encryption mechanism that are distinct from the backupAuthKey and backupEncKey). Differential Revision: https://phabricator.services.mozilla.com/D212858
This commit is contained in:
@@ -76,8 +76,8 @@ export class ArchiveEncryptionState {
|
|||||||
*
|
*
|
||||||
* @type {CryptoKey}
|
* @type {CryptoKey}
|
||||||
*/
|
*/
|
||||||
get authKey() {
|
get backupAuthKey() {
|
||||||
return this.#state.authKey;
|
return this.#state.backupAuthKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,23 +182,10 @@ export class ArchiveEncryptionState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let textEncoder = new TextEncoder();
|
|
||||||
let recoveryCodeBytes = textEncoder.encode(recoveryCode);
|
|
||||||
|
|
||||||
// Next, we turn the recoveryCode into some key material that we can use
|
|
||||||
// to derive a key from.
|
|
||||||
lazy.logConsole.debug("Creating keyMaterial from recovery code");
|
|
||||||
let keyMaterial = await crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
recoveryCodeBytes,
|
|
||||||
"PBKDF2",
|
|
||||||
false /* extractable */,
|
|
||||||
["deriveBits"]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Next, we generate a 32-byte salt, and then concatenate a static suffix
|
// Next, we generate a 32-byte salt, and then concatenate a static suffix
|
||||||
// to it, including the version number.
|
// to it, including the version number.
|
||||||
lazy.logConsole.debug("Creating salt");
|
lazy.logConsole.debug("Creating salt");
|
||||||
|
let textEncoder = new TextEncoder();
|
||||||
const SALT_SUFFIX = textEncoder.encode(
|
const SALT_SUFFIX = textEncoder.encode(
|
||||||
"backupkey-v" + ArchiveEncryptionState.VERSION
|
"backupkey-v" + ArchiveEncryptionState.VERSION
|
||||||
);
|
);
|
||||||
@@ -209,67 +196,8 @@ export class ArchiveEncryptionState {
|
|||||||
salt.set(saltPrefix);
|
salt.set(saltPrefix);
|
||||||
salt.set(SALT_SUFFIX, saltPrefix.length);
|
salt.set(SALT_SUFFIX, saltPrefix.length);
|
||||||
|
|
||||||
// Then we derive the "backup key", using
|
let { backupAuthKey, backupEncKey } =
|
||||||
// PBKDF2(recoveryCode, saltPrefix || SALT_SUFFIX, SHA-256, 600,000)
|
await lazy.ArchiveUtils.computeBackupKeys(recoveryCode, salt);
|
||||||
const ITERATIONS = 600_000;
|
|
||||||
|
|
||||||
// We actually use the backup key as bits to derive other information,
|
|
||||||
// like the HKDF authKey and encKey.
|
|
||||||
lazy.logConsole.debug("Deriving backupKeyBits");
|
|
||||||
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"]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Derive BackupAuthKey as HKDF(backupKey, “backupkey-auth”, salt=None)
|
|
||||||
lazy.logConsole.debug("Deriving authKey from backupKey");
|
|
||||||
let authKey = new Uint8Array(
|
|
||||||
await crypto.subtle.deriveBits(
|
|
||||||
{
|
|
||||||
name: "HKDF",
|
|
||||||
salt: new Uint8Array(0), // no salt
|
|
||||||
info: textEncoder.encode("backupkey-auth"),
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
backupKeyHKDF,
|
|
||||||
256
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Derive BackupEncKey as HKDF(BackupKey, info=“backupkey-enc-key”, salt=None)
|
|
||||||
lazy.logConsole.debug("Deriving backupEncKey");
|
|
||||||
let encKey = 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 },
|
|
||||||
true /* extractable */,
|
|
||||||
["encrypt", "wrapKey"]
|
|
||||||
);
|
|
||||||
|
|
||||||
lazy.logConsole.debug("Encrypting secrets with encKey");
|
lazy.logConsole.debug("Encrypting secrets with encKey");
|
||||||
const NONCE_SIZE = 96;
|
const NONCE_SIZE = 96;
|
||||||
@@ -287,7 +215,7 @@ export class ArchiveEncryptionState {
|
|||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
iv: nonce,
|
iv: nonce,
|
||||||
},
|
},
|
||||||
encKey,
|
backupEncKey,
|
||||||
secretsBytes
|
secretsBytes
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -295,7 +223,7 @@ export class ArchiveEncryptionState {
|
|||||||
this.#state = {
|
this.#state = {
|
||||||
publicKey: keyPair.publicKey,
|
publicKey: keyPair.publicKey,
|
||||||
salt,
|
salt,
|
||||||
authKey,
|
backupAuthKey,
|
||||||
nonce,
|
nonce,
|
||||||
wrappedSecrets,
|
wrappedSecrets,
|
||||||
};
|
};
|
||||||
@@ -312,7 +240,9 @@ export class ArchiveEncryptionState {
|
|||||||
async serialize() {
|
async serialize() {
|
||||||
let publicKey = await crypto.subtle.exportKey("jwk", this.#state.publicKey);
|
let publicKey = await crypto.subtle.exportKey("jwk", this.#state.publicKey);
|
||||||
let salt = lazy.ArchiveUtils.arrayToBase64(this.#state.salt);
|
let salt = lazy.ArchiveUtils.arrayToBase64(this.#state.salt);
|
||||||
let authKey = lazy.ArchiveUtils.arrayToBase64(this.#state.authKey);
|
let backupAuthKey = lazy.ArchiveUtils.arrayToBase64(
|
||||||
|
this.#state.backupAuthKey
|
||||||
|
);
|
||||||
let nonce = lazy.ArchiveUtils.arrayToBase64(this.#state.nonce);
|
let nonce = lazy.ArchiveUtils.arrayToBase64(this.#state.nonce);
|
||||||
let wrappedSecrets = lazy.ArchiveUtils.arrayToBase64(
|
let wrappedSecrets = lazy.ArchiveUtils.arrayToBase64(
|
||||||
this.#state.wrappedSecrets
|
this.#state.wrappedSecrets
|
||||||
@@ -320,7 +250,7 @@ export class ArchiveEncryptionState {
|
|||||||
let result = {
|
let result = {
|
||||||
publicKey,
|
publicKey,
|
||||||
salt,
|
salt,
|
||||||
authKey,
|
backupAuthKey,
|
||||||
nonce,
|
nonce,
|
||||||
wrappedSecrets,
|
wrappedSecrets,
|
||||||
version: ArchiveEncryptionState.VERSION,
|
version: ArchiveEncryptionState.VERSION,
|
||||||
@@ -360,7 +290,9 @@ export class ArchiveEncryptionState {
|
|||||||
true /* extractable */,
|
true /* extractable */,
|
||||||
["encrypt"]
|
["encrypt"]
|
||||||
);
|
);
|
||||||
let authKey = lazy.ArchiveUtils.stringToArray(stateData.authKey);
|
let backupAuthKey = lazy.ArchiveUtils.stringToArray(
|
||||||
|
stateData.backupAuthKey
|
||||||
|
);
|
||||||
let salt = lazy.ArchiveUtils.stringToArray(stateData.salt);
|
let salt = lazy.ArchiveUtils.stringToArray(stateData.salt);
|
||||||
let nonce = lazy.ArchiveUtils.stringToArray(stateData.nonce);
|
let nonce = lazy.ArchiveUtils.stringToArray(stateData.nonce);
|
||||||
let wrappedSecrets = lazy.ArchiveUtils.stringToArray(
|
let wrappedSecrets = lazy.ArchiveUtils.stringToArray(
|
||||||
@@ -369,7 +301,7 @@ export class ArchiveEncryptionState {
|
|||||||
|
|
||||||
this.#state = {
|
this.#state = {
|
||||||
publicKey,
|
publicKey,
|
||||||
authKey,
|
backupAuthKey,
|
||||||
salt,
|
salt,
|
||||||
nonce,
|
nonce,
|
||||||
wrappedSecrets,
|
wrappedSecrets,
|
||||||
|
|||||||
@@ -88,4 +88,96 @@ export const ArchiveUtils = {
|
|||||||
get ARCHIVE_CHUNK_MAX_BYTES_SIZE() {
|
get ARCHIVE_CHUNK_MAX_BYTES_SIZE() {
|
||||||
return 1048576; // 2 ^ 20 bytes, per guidance from security engineering.
|
return 1048576; // 2 ^ 20 bytes, per guidance from security engineering.
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 },
|
||||||
|
true /* extractable */,
|
||||||
|
["encrypt", "decrypt", "wrapKey"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { backupAuthKey, backupEncKey };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1007,7 +1007,7 @@ export class BackupService extends EventTarget {
|
|||||||
? {
|
? {
|
||||||
publicKey: encState.publicKey,
|
publicKey: encState.publicKey,
|
||||||
salt: encState.salt,
|
salt: encState.salt,
|
||||||
authKey: encState.authKey,
|
backupAuthKey: encState.backupAuthKey,
|
||||||
wrappedSecrets: encState.wrappedSecrets,
|
wrappedSecrets: encState.wrappedSecrets,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ add_task(async function test_ArchiveEncryptionState_enable() {
|
|||||||
|
|
||||||
Assert.equal(recoveryCode, TEST_RECOVERY_CODE, "Got back recovery code.");
|
Assert.equal(recoveryCode, TEST_RECOVERY_CODE, "Got back recovery code.");
|
||||||
Assert.ok(encState.publicKey, "A public key was computed.");
|
Assert.ok(encState.publicKey, "A public key was computed.");
|
||||||
Assert.ok(encState.authKey, "An auth key was computed.");
|
Assert.ok(encState.backupAuthKey, "An auth key was computed.");
|
||||||
Assert.ok(encState.salt, "A salt was computed.");
|
Assert.ok(encState.salt, "A salt was computed.");
|
||||||
Assert.ok(encState.nonce, "A nonce was computed.");
|
Assert.ok(encState.nonce, "A nonce was computed.");
|
||||||
Assert.ok(encState.wrappedSecrets, "Wrapped secrets were computed.");
|
Assert.ok(encState.wrappedSecrets, "Wrapped secrets were computed.");
|
||||||
@@ -66,8 +66,8 @@ add_task(
|
|||||||
"Public keys match"
|
"Public keys match"
|
||||||
);
|
);
|
||||||
Assert.deepEqual(
|
Assert.deepEqual(
|
||||||
encState.authKey,
|
encState.backupAuthKey,
|
||||||
recoveredState.authKey,
|
recoveredState.backupAuthKey,
|
||||||
"Auth keys match"
|
"Auth keys match"
|
||||||
);
|
);
|
||||||
Assert.deepEqual(encState.salt, recoveredState.salt, "Salts match");
|
Assert.deepEqual(encState.salt, recoveredState.salt, "Salts match");
|
||||||
|
|||||||
Reference in New Issue
Block a user