mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
fix(passkeys): bind slots to wallet material
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user