fix(passkeys): bind slots to wallet material

This commit is contained in:
2026-09-04 21:27:14 +02:00
parent b7c0a33a69
commit 9374a87d0d
15 changed files with 631 additions and 116 deletions
+66
View File
@@ -0,0 +1,66 @@
// Rewrap proves every existing slot before replacing any bytes. A supplied PRF
// is therefore both authority for the old wrap and input for its fresh wrap;
// one wrong credential aborts the complete in-memory batch before persistence.
import type {WalletMaterialV2} from './storage/storedSecret'
import {serializeWalletMaterial, walletMaterialHash} from './storage/storedSecret'
import type {PasskeySlot} from './storage/passkeySlots'
import {
PASSKEY_SLOTS_STORAGE_KEY,
passkeySlotsEqual,
readPasskeySlots,
requireCurrentPasskeyMaterialOwner,
writePasskeySlots,
} from './storage/passkeySlots'
import {withStorageLock} from './storageLock'
import {unwrapWalletMaterialWithPrf, wrapWalletMaterialWithPrf} from './passkeyWrap'
type ProvenSlot = {
readonly slot: PasskeySlot
readonly prfOutput: Uint8Array
}
export const rewrapAllSlots = async (
material: WalletMaterialV2,
prfOutputs: ReadonlyMap<string, Uint8Array>,
): Promise<void> => {
const ownerId = requireCurrentPasskeyMaterialOwner(material)
const materialHash = walletMaterialHash(material)
const serialized = serializeWalletMaterial(material)
await withStorageLock(PASSKEY_SLOTS_STORAGE_KEY, async () => {
const provenSlots: ProvenSlot[] = readPasskeySlots().map((slot) => {
const prfOutput = prfOutputs.get(slot.credentialId)
if (!prfOutput) {
throw new Error('Missing old PRF output for a passkey slot - refusing a partial re-wrap.')
}
return {slot, prfOutput}
})
for (const {slot, prfOutput} of provenSlots) {
if (slot.materialHash !== materialHash) {
throw new Error('Passkey slot does not match the saved wallet material.')
}
const current = await unwrapWalletMaterialWithPrf(prfOutput, slot)
if (serializeWalletMaterial(current) !== serialized) {
throw new Error('Passkey slot contains different wallet material.')
}
}
const rewrapped: PasskeySlot[] = []
for (const {slot, prfOutput} of provenSlots) {
rewrapped.push({...slot, ...(await wrapWalletMaterialWithPrf(prfOutput, material))})
}
if (requireCurrentPasskeyMaterialOwner(material) !== ownerId) {
throw new Error('Saved wallet material changed during passkey re-wrap.')
}
const currentSlots = readPasskeySlots()
if (
currentSlots.length !== provenSlots.length ||
currentSlots.some((current, index) => {
const original = provenSlots[index]?.slot
return original === undefined || !passkeySlotsEqual(original, current)
})
) {
throw new Error('Passkey slots changed during re-wrap.')
}
writePasskeySlots(ownerId, rewrapped)
})
}