refactor: isolate passkey wrapping crypto

This commit is contained in:
2026-08-22 16:54:48 +02:00
parent acfab83438
commit 058b10ddf1
3 changed files with 373 additions and 23 deletions
+12 -23
View File
@@ -16,15 +16,11 @@ const WRAP_KEY_HKDF_INFO = 'sattle-passkey-wrap-v1'
export const derivePasskeyWrapKey = async (
prfOutput: Uint8Array,
hkdfSalt: Uint8Array
hkdfSalt: Uint8Array,
): Promise<CryptoKey> => {
const baseKey = await crypto.subtle.importKey(
'raw',
new Uint8Array(prfOutput),
'HKDF',
false,
['deriveKey']
)
const baseKey = await crypto.subtle.importKey('raw', new Uint8Array(prfOutput), 'HKDF', false, [
'deriveKey',
])
return crypto.subtle.deriveKey(
// the copies pin the TS type to Uint8Array<ArrayBuffer> - hexToBytes
// returns Uint8Array<ArrayBufferLike>, which BufferSource rejects
@@ -32,33 +28,29 @@ export const derivePasskeyWrapKey = async (
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(hkdfSalt),
info: new Uint8Array(utf8ToBytes(WRAP_KEY_HKDF_INFO))
info: new Uint8Array(utf8ToBytes(WRAP_KEY_HKDF_INFO)),
},
baseKey,
{name: 'AES-GCM', length: 256},
false,
['encrypt', 'decrypt']
['encrypt', 'decrypt'],
)
}
export const wrapLinkingKeyWithPrf = async (
prfOutput: Uint8Array,
linkingKey: Uint8Array
linkingKey: Uint8Array,
): Promise<PasskeyWrap> => {
const hkdfSalt = crypto.getRandomValues(new Uint8Array(16))
const iv = crypto.getRandomValues(new Uint8Array(12))
const wrapKey = await derivePasskeyWrapKey(prfOutput, hkdfSalt)
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt(
{name: 'AES-GCM', iv},
wrapKey,
new Uint8Array(linkingKey)
)
await crypto.subtle.encrypt({name: 'AES-GCM', iv}, wrapKey, new Uint8Array(linkingKey)),
)
return {
hkdfSalt: bytesToHex(hkdfSalt),
iv: bytesToHex(iv),
wrappedKey: bytesToHex(ciphertext)
wrappedKey: bytesToHex(ciphertext),
}
}
@@ -66,16 +58,13 @@ export const wrapLinkingKeyWithPrf = async (
// i.e. a different passkey than the one that created the slot
export const unwrapLinkingKeyWithPrf = async (
prfOutput: Uint8Array,
wrap: PasskeyWrap
wrap: PasskeyWrap,
): Promise<Uint8Array> => {
const wrapKey = await derivePasskeyWrapKey(
prfOutput,
hexToBytes(wrap.hkdfSalt)
)
const wrapKey = await derivePasskeyWrapKey(prfOutput, hexToBytes(wrap.hkdfSalt))
const plaintext = await crypto.subtle.decrypt(
{name: 'AES-GCM', iv: new Uint8Array(hexToBytes(wrap.iv))},
wrapKey,
new Uint8Array(hexToBytes(wrap.wrappedKey))
new Uint8Array(hexToBytes(wrap.wrappedKey)),
)
return new Uint8Array(plaintext)
}