Skip to content

Commit

Permalink
Add function rotateMainKey
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Jul 23, 2024
1 parent b8e78c8 commit b0ab37d
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/services/keystore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,73 @@ describe("The keystore", () => {
assert.strictEqual(newPrivateData.prfKeys.length, 2);
assert.strictEqual(toBase64(newPrivateData.prfKeys[1].credentialId), toBase64(newCredentialId));
});

it("can rotate the mainKey of a container encrypted with a password key and multiple PRF keys.", async () => {
const privateData: keystore.AsymmetricEncryptedContainer = jsonParseTaggedBinary('{"mainKey":{"publicKey":{"importKey":{"format":"raw","keyData":{"$b64u":"BNunIeN4Js_iBvCDJEVOz8869zluTVLGhIGoXTdAq3inUGOsDGnQh5jlN_PHGqGEdAOfCbbfgFGFGGiSTMzr1-k"},"algorithm":{"name":"ECDH","namedCurve":"P-256"}}},"unwrapKey":{"format":"raw","unwrapAlgo":"AES-KW","unwrappedKeyAlgo":{"name":"AES-GCM","length":256}}},"passwordKey":{"pbkdf2Params":{"name":"PBKDF2","hash":"SHA-256","iterations":1000,"salt":{"$b64u":"u8SbZmq3whLjddp0bZguopZmoXvwyQoXcEGClemCp8I"}},"algorithm":{"name":"AES-GCM","length":256},"keypair":{"publicKey":{"importKey":{"format":"raw","keyData":{"$b64u":"BOFaP7mYez0NKiBQU2dDpN-t05AadNKfjvJ7-T0cXVlMjj21ehNNMv6j3jk2lZnu4F3W9xNXBdIVS2haA0aKnpA"},"algorithm":{"name":"ECDH","namedCurve":"P-256"}}},"privateKey":{"unwrapKey":{"format":"jwk","wrappedKey":{"$b64u":"qWDkpdWnlTleshhlM3CxAsSFua0vQYniF3e3dmT5_-eaqArvhZyItUEWU3dHgenL1R7j5wh4dPy3oudC1xNvfCBHDrOy_YoOrOx1cbHhehge2ITHDo2frgMBuhm1s62_MUp56b38QfQIOTTLrmKQBYL2sWprPFMp2I8S2a-d3MR1jXdPnvupS_vgRTJ2fXbVB62DfE7U1OxADSSFprUt0bxbbCC7GS4hEkVV9nYf5BRB_q19DDpb1HMwz7_nYfLJidqt6Nx_WUag5BZddw99-dSh6mp0bASQGtCA6zpwFIV91U8"},"unwrapAlgo":{"name":"AES-GCM","iv":{"$b64u":"F8E9s7NxPhAtzbnE"}},"unwrappedKeyAlgo":{"name":"ECDH","namedCurve":"P-256"}}}},"unwrapKey":{"wrappedKey":{"$b64u":"gEM0ic_pgCIZGlheJfHho08vWOngHWuSMDOJJPa0cv3une2aIGpuDA"},"unwrappingKey":{"deriveKey":{"algorithm":{"name":"ECDH"},"derivedKeyAlgorithm":{"name":"AES-KW","length":256}}}}},"prfKeys":[{"credentialId":{"$b64u":"0MM_YI2iaGiTFuXdH8yZZO61DvthovXBPbFkz8UHzeEeO-xFfUkEbUSp7MkjuJOy"},"prfSalt":{"$b64u":"Gb1xFiZySV1Capojuk7Daf-O11e0G-kyhAoYEc_eKvM"},"hkdfSalt":{"$b64u":"m8CFJBtxAaUFvrdHjnoU8mxmX62CKula6IIrHPVEp7E"},"hkdfInfo":{"$b64u":"ZURpcGxvbWFzIFBSRg"},"algorithm":{"name":"AES-GCM","length":256},"keypair":{"publicKey":{"importKey":{"format":"raw","keyData":{"$b64u":"BFBM0vAWx2NAvslZq3alSvO4pZ18Mcvo_NibPFEQtautmS1vBSNpODG5ayxoG3p2JzV1MpjXJpAHrz8JKnE2QQo"},"algorithm":{"name":"ECDH","namedCurve":"P-256"}}},"privateKey":{"unwrapKey":{"format":"jwk","wrappedKey":{"$b64u":"aw7WMubFXy8PJJX3NkSWGfOjgWORbUzUEcIjlaZFI1iqjsIBA32lWKoeI-yKzOfYA6Bhir6IFUIazuZNL1DGitHxrSjG70X5dQwEM5Kkp2QCqRw5FaAGleRy7jT52n2gG_f-qHqYgdG0o-E6kSuB-B57AJiXeaDmu7OKeXKmSiMwADroRzHc3T4UpgA_RW4YQRcjs0iJm0Qs5KZpgAyRkqRbIYPXqWJGj1w_M1WqNLKqdiNkQpdZpgnrTxQ8OzZDkUZd-rT5jIXVjfdNZ0BrtTwKEFtx6XwMTBd4r3rcZvM80MI"},"unwrapAlgo":{"name":"AES-GCM","iv":{"$b64u":"gBnENnHFsW_xAiZu"}},"unwrappedKeyAlgo":{"name":"ECDH","namedCurve":"P-256"}}}},"unwrapKey":{"wrappedKey":{"$b64u":"c6wEMBDuQFSAyExsuUL5ieSZOvk1cYgRSlJOOKATEvLhlTDUOmlj6w"},"unwrappingKey":{"deriveKey":{"algorithm":{"name":"ECDH"},"derivedKeyAlgorithm":{"name":"AES-KW","length":256}}}}},{"credentialId":{"$b64u":"U1ZjVB_LEZqOBm10kf39OVwBTdJkLjHikpUftRR5wL_tD08GC_0QHkzcxKNYY-eO"},"prfSalt":{"$b64u":"6MRed82OBXJ1WQ9aPnZMSQ_CGehNZKr9ABa0ACMqIcY"},"hkdfSalt":{"$b64u":"xfKHBNmx7EJ239xbvBE0HCIuWqsB3H28VNoXbQI7CZg"},"hkdfInfo":{"$b64u":"ZURpcGxvbWFzIFBSRg"},"algorithm":{"name":"AES-GCM","length":256},"keypair":{"publicKey":{"importKey":{"format":"raw","keyData":{"$b64u":"BPXK4Q-vU4safIHDJEsmM6cZqquBRzeTJAr_lxKXoYC4lkz02WFvyFJkWIA9jrim6ok1yHSppSJBa2l6Hb9lQB8"},"algorithm":{"name":"ECDH","namedCurve":"P-256"}}},"privateKey":{"unwrapKey":{"format":"jwk","wrappedKey":{"$b64u":"pr8DoqE8Nz9S_2OS-Vu2RZNK-M9JxLWib-R6aDALkBYIxssChWrkN-vEAnpTgOe0lQGkTAEx07ENmg4LvCGAZkHEz581WBlD6MOn4Hqh8OP_CuvajmtM6hNH72qozHsu2QnvUtMUxtIxSrY8Y4sZVlGLtCyjtnmm9MLNSlFcsTpIk_pWi_BzTv-mgos6r-InAxeIgyJir7muOj0OxR4P-gKKGYC6lMp5qK_Ixgc3x_z9w9-tUwJ2TPUjCLX-MsBn00wunsz36pCRjM2RuB90IbmdSEHDRXZ0NMFaHRlktRZsYZ8"},"unwrapAlgo":{"name":"AES-GCM","iv":{"$b64u":"KBOix07YMFCJEg5I"}},"unwrappedKeyAlgo":{"name":"ECDH","namedCurve":"P-256"}}}},"unwrapKey":{"wrappedKey":{"$b64u":"n9lD77nkjpI5iBZX4gP8hZdQRhrucg3IvWcSfUlCGpYsrnEWiVVGYw"},"unwrappingKey":{"deriveKey":{"algorithm":{"name":"ECDH"},"derivedKeyAlgorithm":{"name":"AES-KW","length":256}}}}}],"jwe":"eyJhbGciOiJBMjU2R0NNS1ciLCJlbmMiOiJBMjU2R0NNIiwiaXYiOiJuRDhmTWdZVXdSUF9nU3VGIiwidGFnIjoiaVdSRnFsb0FneTdEaElVRDBqTVBmdyJ9.-XD8oZjK6o1VMHlpe-AazEyC9Sij2UIkyUizlAc2u3w.SSwBLZhoAc1KyA_0.GJSciNkM99lF0oguOizZ1TtzrAkFBDelPSKtZVTOE0AUl1566Hmh8SHmvS0_jKtJjayFBhxoHk-44292gPIXzViEY8DCndwLMjG9BivHr19vOy1B8TxGl_4pUPjDRjKQWovyo_Z4vfUkKEWg7BEbLNw5kQ783AyvixQo1dh6GHC5dt-LIDMREMCw7ZRtE2jM6PhTMfFBKJ54QP16T6no6ULTm-DWfKVdmwPLnz9esNY9-sDC49GmZce9H-AoDLa0RFTE1KH70YBfaFrOpYkrSxMOJyjfpvbOLIuYc7IGLmjnQKLuH7jb-fKZDvGZiHh010qrOFgeINIoG7KzThbe0EHgCukmIG8KlTcILZJj29fYFM6QQPxHTOe4KqyzAAVtcMzCRMSKOZdounGPDJlRsrnedhqDviV7bqoGgSkH2YJXXcNfS2MfAxm0fGKdcuVJ7xfCo9e5c8CKOXw73suzxsTHHl6YjZhqhdNYgWQLCjDaF83wlowb-x2MxZr2pJ4Zrf-1jHgFQbp-8vI8yoGghyslZmSOTMg6dZj_ueBtgCP--dUVB7kprbspJ3oNmuEO1dsw4lsUrdwaHFDaVYDkYjLZBdfzDzdcx7SJ8sQQANKt3vtNmgveRnjEaZ0aHrIbEy6OYIllApTbaGqrRETZohXHdwrBIudO0MScjESOcEvAuXYc8xPFUynXKvn3xl6Iuhsk_bt-gZeOoV-S1SVfEvV4zK9BfjsrRqPzQGzDM8048-_eraKiCWjZppe4XU3m-RSZS5fyt1Z5XsPsw5ZQov2GM-aKNqkho27NkhaXuN4L-xMoc56WUShgCk20vcO2r36yHjZYf2OONrsDVnJZNDSl3SxmXB5uGW_l0YgyPrm6MNnFN3bvVmSUy9M3VH15t2vFfDKCwoHJzHcMdFy5TOv8ZmQNai2Xi4eNUs4723QXlIy3intYb4HEAaMDsE85nrtMjRCmjXOc28eB-2mRXs1m-3umAo2rqLVO88ZdJuTXkbwW_eU6gXEdGtmJ06RJ6N6BhHhm1GYFjHl4c1h7Jqr_ELZexor3Dd0DA8EcTV8F0dIAz1dVzraXMK0KNft6gDsRXXCZ88--syLAo2ayyqO39m8NZca_rH6h72e2J2UZ8h1qPoseHCuoSa95YpkrcP0FMWAMYVNeW0T69R2pNusByoc1FlTayo8Ms25HlS-vax3ylJHHOkU82dmHcGeqRJHT6gnk9LRc3B8E3t_GjeVPao0qzOGX-hqB6yFS_K3lHFN1yQ73PDji8GvJq3oBPdvFO7NvfH_X19HTl2rxXqgwvy-mfsd4CP0AphI9NO1erFKXNfUkgBVibO5emZqakkWmFknog2T8QyFEzCDR2awqUTlcRfvd1HkG4qBPKdIV9f-d8iw9PNcTwk2IFLjy4Lx2dVen6_eWBnJJu-5wdYp1f3UKQapr28hFVwdqGXIbpNYXsMPf7XYmLGOjkLnwtamS4pKwn7UYkPUKfmwQELDOJgLHWVUc9POuF4GUqUZNhRh0y0GxinZ_w-tbAtJjysrniqED8pHsxHSTCZ85wR5Lha41c_yvlchybgjLQ_IgM4TzIk_ZolKbdR5XUKoj2yGEU8-XkAhutDbHCJwFglbYTY7Y8ZwO7QVGFlbHFqWSTnwCB_3lw8V1PUxyxymcRgBhSSjsJxx8LIR1Lx5qKYTZZqPeWpLKGrtJVyB_nRarVFtHEg.QBUeKjmFep5cwbdOM8Ikug"}');

const prfOutput1 = fromBase64("Pt83hCqtFqYOmZ0EUt4wG8ptn4N6rphOn1ujw83FbA4=");
const prfOutput2 = fromBase64("GbdCV4TxlvI10cSAyK61zDxe1WGpiTJUAVVXScZlxKE=");

const [passwordKey, passwordKeyInfo] = await keystore.getPasswordKey(
privateData,
"Asdf123!",
) as [CryptoKey, keystore.AsymmetricPasswordKeyInfo];

const mockCredential1 = mockPrfCredential({
id: privateData.prfKeys[0].credentialId,
prfOutput: prfOutput1,
});
const [prfKey1, prfKeyInfo1,] = await keystore.getPrfKey(
privateData,
mockCredential1,
"localhost",
async () => false,
) as [CryptoKey, keystore.WebauthnPrfEncryptionKeyInfoV2, any];

const mockCredential2 = mockPrfCredential({
id: privateData.prfKeys[1].credentialId,
prfOutput: prfOutput2,
});
const [prfKey2, prfKeyInfo2,] = await keystore.getPrfKey(
privateData,
mockCredential2,
"localhost",
async () => false,
) as [CryptoKey, keystore.WebauthnPrfEncryptionKeyInfoV2, any];

const oldMainKeys = [
await keystore.unwrapKey(passwordKey, privateData.mainKey, passwordKeyInfo, false),
await keystore.unwrapKey(prfKey1, privateData.mainKey, prfKeyInfo1, false),
await keystore.unwrapKey(prfKey2, privateData.mainKey, prfKeyInfo2, false),
];

for (const oldMainKey of oldMainKeys) {
const unlocked = await keystore.unlock(oldMainKey, privateData);
assert.isNotNull(unlocked, "Expected to be able to unlock keystore with existing key");
}

const newPrivateData = await keystore.rotateMainKey(privateData, [prfKey1, prfKeyInfo1]);

for (const oldMainKey of oldMainKeys) {
await asyncAssertThrows(
() => keystore.unlock(oldMainKey, newPrivateData),
"Expected failure to unlock keystore with old key",
);
}

assert.strictEqual(privateData.passwordKey.keypair, newPrivateData.passwordKey.keypair);
assert.strictEqual(privateData.prfKeys[0].keypair, newPrivateData.prfKeys[0].keypair);
assert.strictEqual(privateData.prfKeys[1].keypair, newPrivateData.prfKeys[1].keypair);
assert.strictEqual(privateData.prfKeys[0].credentialId, newPrivateData.prfKeys[0].credentialId);
assert.strictEqual(privateData.prfKeys[1].credentialId, newPrivateData.prfKeys[1].credentialId);

for (const [unlocked, newPrivateData2] of [
await keystore.unlockPassword(newPrivateData, "Asdf123!"),
await keystore.unlockPrf(newPrivateData, mockCredential1, "localhost", async () => false),
await keystore.unlockPrf(newPrivateData, mockCredential2, "localhost", async () => false),
]) {
assert.isNotNull(unlocked, "Expected to be able to unlock new keystore with new key");
assert.isNull(newPrivateData2, "Expected no update to privateData on unlock");
}
});
});
52 changes: 52 additions & 0 deletions src/services/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ type StaticEncapsulationInfo = {
}


function isAsymmetricEncryptedContainer(privateData: EncryptedContainer): privateData is AsymmetricEncryptedContainer {
return (
(privateData.passwordKey
? isAsymmetricPasswordKeyInfo(privateData.passwordKey)
: true)
&& (privateData.prfKeys
? privateData.prfKeys.every(isPrfKeyV2)
: true)
);
}

// Values from OWASP password guidelines https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
const pbkdfHash: HashAlgorithmIdentifier = "SHA-256";
const pbkdfIterations: number = 600000;
Expand Down Expand Up @@ -199,6 +210,47 @@ async function createAsymmetricMainKey(currentMainKey?: CryptoKey): Promise<{ ke
};
}

export async function rotateMainKey(
privateData: AsymmetricEncryptedContainer,
[unwrappingKey, keyInfo]: [CryptoKey, WrappedKeyInfo],
): Promise<AsymmetricEncryptedContainer> {
if (!isAsymmetricEncryptedContainer(privateData)) {
throw new Error("EncryptedContainer is not fully asymmetric-encrypted");
}

const currentMainKey = await unwrapKey(unwrappingKey, privateData.mainKey, keyInfo, false);
const {
keyInfo: newMainPublicKeyInfo,
mainKey: newMainKey,
privateKey: newMainPrivateKey,
} = await createAsymmetricMainKey();

return {
mainKey: newMainPublicKeyInfo,
jwe: await reencryptPrivateData(privateData.jwe, currentMainKey, newMainKey),
passwordKey: privateData.passwordKey && {
...privateData.passwordKey,
...await encapsulateKey(
newMainPrivateKey,
privateData.passwordKey.keypair.publicKey,
privateData.passwordKey.keypair,
newMainKey,
),
},
prfKeys: privateData.prfKeys && await Promise.all(
privateData.prfKeys.map(async keyInfo => ({
...keyInfo,
...await encapsulateKey(
newMainPrivateKey,
keyInfo.keypair.publicKey,
keyInfo.keypair,
newMainKey,
),
}))
),
};
}

async function createSessionKey(): Promise<[CryptoKey, ArrayBuffer]> {
const sessionKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
Expand Down

0 comments on commit b0ab37d

Please sign in to comment.