/**
* Performs a key rotation for a user's wrapped Data Encryption Key.
* @param {BufferSource} oldSalt - The salt for the key to be retired.
* @param {BufferSource} newSalt - The salt for the new key.
* @param {ArrayBuffer} oldWrappedDEKBlob - The encrypted DEK blob from the server,
* which contains both the IV and the ciphertext.
* @returns {Promise<{newWrappedDEKBlob: ArrayBuffer}>} - The new encrypted DEK blob.
*/
async function rotateKeyEncryptionKey(oldSalt, newSalt, oldEncryptedDEK) {
// 1. Request PRF secrets for BOTH the old and new salts in one transaction.
const getOptions = {
publicKey: { /* ... challenge, rpId, etc. ... */,
extensions: { prf: { eval: { first: oldSalt, second: newSalt } } }
}
};
const assertion = await navigator.credentials.get(getOptions);
const prfResults = assertion.getClientExtensionResults().prf.results;
const oldPrfSecret = prfResults.first;
const newPrfSecret = prfResults.second;
// 2. Derive both the old and new Key Encryption Keys (KEKs) using HKDF.
const oldMasterKey = await crypto.subtle.importKey('raw', oldPrfSecret, 'HKDF', false, ['deriveKey']);
const oldKEK = await crypto.subtle.deriveKey(
{ name: 'HKDF', salt: new Uint8Array(), hash: 'SHA-256', info: new TextEncoder().encode('DEK Wrapping Key V1') },
oldMasterKey, { name: 'AES-GCM', length: 256 }, false, ['unwrapKey']
);
const newMasterKey = await crypto.subtle.importKey('raw', newPrfSecret, 'HKDF', false, ['deriveKey']);
const newKEK = await crypto.subtle.deriveKey(
{ name: 'HKDF', salt: new Uint8Array(), hash: 'SHA-256', info: new TextEncoder().encode('DEK Wrapping Key V2') },
newMasterKey, { name: 'AES-GCM', length: 256 }, false, ['wrapKey']
);
// 3. Use the old KEK to decrypt (unwrap) the DEK.
// Assumes a structure of [12-byte IV][Ciphertext]
const oldIv = oldWrappedDEKBlob.slice(0, 12);
const oldCiphertext = oldWrappedDEKBlob.slice(12);
const plaintextDEK = await crypto.subtle.unwrapKey(
'raw',
oldCiphertext, // Use the parsed ciphertext
oldKEK,
{ name: 'AES-GCM', iv: oldIv } // Use the parsed IV
);
// 4. Immediately use the new KEK to re-encrypt (wrap) the DEK.
const newIv = crypto.getRandomValues(new Uint8Array(12));
const newWrappedDEK = await crypto.subtle.wrapKey(
'raw',
plaintextDEK,
newKEK,
{ name: 'AES-GCM', iv: newIv }
);
// --- Step 5: Combine the new IV and new ciphertext into a single blob ---
const newWrappedDEKBlob = new Uint8Array(newIv.byteLength + newWrappedDEK.byteLength);
newWrappedDEKBlob.set(new Uint8Array(newIv), 0);
newWrappedDEKBlob.set(new Uint8Array(newWrappedDEK), newIv.byteLength);
// 6. Return the new wrapped DEK blob to the server.
return { newWrappedDEKBlob: newWrappedDEKBlob.buffer };
}