Formatting

This commit is contained in:
2026-09-05 18:30:35 +02:00
parent 454a9db1d5
commit 4caa075052
133 changed files with 8260 additions and 8181 deletions
+75 -70
View File
@@ -1,129 +1,134 @@
import {hmac} from '@noble/hashes/hmac.js'
import {sha256} from '@noble/hashes/sha2.js'
import {bytesToHex} from '@noble/hashes/utils.js'
import {beforeEach, describe, expect, it} from 'vitest'
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { bytesToHex } from '@noble/hashes/utils.js';
import { beforeEach, describe, expect, it } from 'vitest';
import {deriveWalletMaterial, saveWalletMaterial} from './keys'
import { deriveWalletMaterial, saveWalletMaterial } from './keys';
import {
getPasskeyPrfOutput,
registerPasskey,
rewrapAllSlots,
unlockWalletMaterialWithPasskey,
} from './passkeys'
import type {CeremonyCredential, PasskeyCredentials} from './passkeys'
import {stubLocalStorage} from './test-utils'
} from './passkeys';
import type { CeremonyCredential, PasskeyCredentials } from './passkeys';
import { stubLocalStorage } from './test-utils';
const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const OTHER_MNEMONIC = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
const MATERIAL = {...deriveWalletMaterial(MNEMONIC), linkingKeyHex: '07'.repeat(32)}
const OTHER_MATERIAL = deriveWalletMaterial(OTHER_MNEMONIC)
const ALTERED_ROOT = {...MATERIAL, cashRootHex: OTHER_MATERIAL.cashRootHex}
const SLOT_STORAGE_KEY = 'sattle_passkey_slots'
const MNEMONIC =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const OTHER_MNEMONIC =
'legal winner thank year wave sausage worth useful legal winner thank yellow';
const MATERIAL = { ...deriveWalletMaterial(MNEMONIC), linkingKeyHex: '07'.repeat(32) };
const OTHER_MATERIAL = deriveWalletMaterial(OTHER_MNEMONIC);
const ALTERED_ROOT = { ...MATERIAL, cashRootHex: OTHER_MATERIAL.cashRootHex };
const SLOT_STORAGE_KEY = 'sattle_passkey_slots';
const toBytes = (source: BufferSource): Uint8Array =>
source instanceof ArrayBuffer
? new Uint8Array(source)
: new Uint8Array(source.buffer, source.byteOffset, source.byteLength)
: new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
class FakeAuthenticator implements PasskeyCredentials {
private readonly credentialId = new Uint8Array(16).fill(12)
private readonly secret = new Uint8Array(32).fill(13)
onCreate?: () => Promise<void>
onGet?: () => Promise<void>
private readonly credentialId = new Uint8Array(16).fill(12);
private readonly secret = new Uint8Array(32).fill(13);
onCreate?: () => Promise<void>;
onGet?: () => Promise<void>;
create = async (options?: CredentialCreationOptions): Promise<CeremonyCredential> => {
await this.onCreate?.()
return this.credential(options?.publicKey?.extensions?.prf?.eval?.first)
}
await this.onCreate?.();
return this.credential(options?.publicKey?.extensions?.prf?.eval?.first);
};
get = async (options?: CredentialRequestOptions): Promise<CeremonyCredential | null> => {
await this.onGet?.()
const allowed = options?.publicKey?.allowCredentials ?? []
await this.onGet?.();
const allowed = options?.publicKey?.allowCredentials ?? [];
return allowed.some(
(descriptor) => bytesToHex(toBytes(descriptor.id)) === bytesToHex(this.credentialId),
)
? this.credential(options?.publicKey?.extensions?.prf?.eval?.first)
: null
}
: null;
};
private credential = (salt: BufferSource | undefined): CeremonyCredential => ({
type: 'public-key',
rawId: this.credentialId,
getClientExtensionResults: () => ({
prf: salt
? {enabled: true, results: {first: new Uint8Array(hmac(sha256, this.secret, toBytes(salt)))}}
? {
enabled: true,
results: { first: new Uint8Array(hmac(sha256, this.secret, toBytes(salt))) },
}
: {},
}),
})
});
}
const rawSlots = (): string => localStorage.getItem(SLOT_STORAGE_KEY) ?? ''
const rawSlots = (): string => localStorage.getItem(SLOT_STORAGE_KEY) ?? '';
beforeEach(async () => {
stubLocalStorage()
await saveWalletMaterial(MATERIAL)
})
stubLocalStorage();
await saveWalletMaterial(MATERIAL);
});
describe('exact wallet material passkey commitment', () => {
it('rejects a wrong old PRF before changing any slot bytes', async () => {
const authenticator = new FakeAuthenticator()
const slot = await registerPasskey(MATERIAL, {credentials: authenticator})
const before = rawSlots()
const wrongPrf = new Uint8Array(32).fill(99)
const authenticator = new FakeAuthenticator();
const slot = await registerPasskey(MATERIAL, { credentials: authenticator });
const before = rawSlots();
const wrongPrf = new Uint8Array(32).fill(99);
await expect(
rewrapAllSlots(MATERIAL, new Map([[slot.credentialId, wrongPrf]])),
).rejects.toThrow()
expect(rawSlots()).toBe(before)
})
).rejects.toThrow();
expect(rawSlots()).toBe(before);
});
it('rejects same-owner altered-root registration against encrypted saved material', async () => {
const authenticator = new FakeAuthenticator()
await saveWalletMaterial(MATERIAL, 'password')
const authenticator = new FakeAuthenticator();
await saveWalletMaterial(MATERIAL, 'password');
await expect(registerPasskey(ALTERED_ROOT, {credentials: authenticator})).rejects.toThrow(
await expect(registerPasskey(ALTERED_ROOT, { credentials: authenticator })).rejects.toThrow(
'saved wallet material',
)
expect(rawSlots()).toBe('')
})
);
expect(rawSlots()).toBe('');
});
it('rejects registration when same-owner saved material changes during the ceremony', async () => {
const authenticator = new FakeAuthenticator()
authenticator.onCreate = () => saveWalletMaterial(ALTERED_ROOT)
const authenticator = new FakeAuthenticator();
authenticator.onCreate = () => saveWalletMaterial(ALTERED_ROOT);
await expect(registerPasskey(MATERIAL, {credentials: authenticator})).rejects.toThrow(
await expect(registerPasskey(MATERIAL, { credentials: authenticator })).rejects.toThrow(
'saved wallet material',
)
expect(rawSlots()).toBe('')
})
);
expect(rawSlots()).toBe('');
});
it('rejects unlock after same-owner saved material was replaced', async () => {
const authenticator = new FakeAuthenticator()
await registerPasskey(MATERIAL, {credentials: authenticator})
await saveWalletMaterial(ALTERED_ROOT, 'password')
const authenticator = new FakeAuthenticator();
await registerPasskey(MATERIAL, { credentials: authenticator });
await saveWalletMaterial(ALTERED_ROOT, 'password');
await expect(unlockWalletMaterialWithPasskey({credentials: authenticator})).rejects.toThrow()
})
await expect(unlockWalletMaterialWithPasskey({ credentials: authenticator })).rejects.toThrow();
});
it('rejects same-owner saved-material replacement during the unlock ceremony', async () => {
const authenticator = new FakeAuthenticator()
await registerPasskey(MATERIAL, {credentials: authenticator})
authenticator.onGet = () => saveWalletMaterial(ALTERED_ROOT)
const authenticator = new FakeAuthenticator();
await registerPasskey(MATERIAL, { credentials: authenticator });
authenticator.onGet = () => saveWalletMaterial(ALTERED_ROOT);
await expect(unlockWalletMaterialWithPasskey({credentials: authenticator})).rejects.toThrow(
await expect(unlockWalletMaterialWithPasskey({ credentials: authenticator })).rejects.toThrow(
'changed',
)
})
);
});
it('keeps a valid slot usable when its old PRF proves the existing wrap', async () => {
const authenticator = new FakeAuthenticator()
const slot = await registerPasskey(MATERIAL, {credentials: authenticator})
const oldPrf = await getPasskeyPrfOutput(slot.credentialId, {credentials: authenticator})
const authenticator = new FakeAuthenticator();
const slot = await registerPasskey(MATERIAL, { credentials: authenticator });
const oldPrf = await getPasskeyPrfOutput(slot.credentialId, { credentials: authenticator });
await rewrapAllSlots(MATERIAL, new Map([[slot.credentialId, oldPrf]]))
await rewrapAllSlots(MATERIAL, new Map([[slot.credentialId, oldPrf]]));
await expect(unlockWalletMaterialWithPasskey({credentials: authenticator})).resolves.toEqual(
await expect(unlockWalletMaterialWithPasskey({ credentials: authenticator })).resolves.toEqual(
MATERIAL,
)
})
})
);
});
});