accounts
Version:
Tempo Accounts SDK
172 lines (152 loc) • 5.92 kB
text/typescript
import { Address, Hex, Json, P256, PublicKey } from 'ox'
import { SignatureEnvelope } from 'ox/tempo'
import { KeyAuthorizationManager } from 'viem/tempo'
import { describe, expect, test, vi } from 'vp/test'
import { accounts } from '../../test/config.js'
import * as Keystore from './Keystore.js'
const rootAddress = accounts[0]!.address
function toAccountContext() {
return {
access: rootAddress,
keyAuthorizationManager: KeyAuthorizationManager.memory(),
}
}
describe('webCryptoP256', () => {
test('default: creates a non-extractable key with a structured-clone handle', async () => {
const keystore = Keystore.webCryptoP256()
expect(keystore.requiresStructuredClone).toBe(true)
const key = await keystore.createKey()
expect(key.publicKey).toMatch(/^0x[0-9a-f]+$/i)
const handle = key.handle as { kind: string; keyPair?: { privateKey: CryptoKey } }
expect(handle.kind).toBe('webcrypto-p256')
expect(handle.keyPair?.privateKey.extractable).toBe(false)
const account = await keystore.toAccount(
{ handle: key.handle, keyType: 'p256', publicKey: key.publicKey },
toAccountContext(),
)
expect(account.accessKeyAddress).toBe(
Address.fromPublicKey(PublicKey.fromHex(key.publicKey)).toLowerCase(),
)
})
test('behavior: extractable handles survive JSON and verify against the public key', async () => {
const keystore = Keystore.webCryptoP256({ extractable: true })
expect(keystore.requiresStructuredClone).toBe(false)
const key = await keystore.createKey()
expect(JSON.parse(JSON.stringify(key.handle))).toEqual(key.handle)
const account = await keystore.toAccount(
{
handle: JSON.parse(JSON.stringify(key.handle)),
keyType: 'p256',
publicKey: key.publicKey,
},
toAccountContext(),
)
expect(account.accessKeyAddress).toBe(
Address.fromPublicKey(PublicKey.fromHex(key.publicKey)).toLowerCase(),
)
// Signature verifies against the persisted public key.
const payload = Hex.random(32)
const signature = await account.sign({ hash: payload, raw: true })
const envelope = SignatureEnvelope.deserialize(signature)
if (envelope.type !== 'p256') throw new Error('expected p256 envelope')
expect(
P256.verify({
hash: envelope.prehash,
payload,
publicKey: PublicKey.fromHex(key.publicKey),
signature: envelope.signature,
}),
).toBe(true)
})
test('behavior: a live handle mangled by JSON storage is permanently unavailable', async () => {
const keystore = Keystore.webCryptoP256()
const key = await keystore.createKey()
// Simulate a structured-clone handle squeezed through a string-based
// storage adapter: the CryptoKey degrades to a plain object.
const mangled = Json.parse(Json.stringify(key.handle))
await expect(
keystore.toAccount(
{ handle: mangled, keyType: 'p256', publicKey: key.publicKey },
toAccountContext(),
),
).rejects.toBeInstanceOf(Keystore.KeyUnavailableError)
})
test('error: corrupt key material is permanently unavailable', async () => {
const keystore = Keystore.webCryptoP256({ extractable: true })
const key = await keystore.createKey()
await expect(
keystore.toAccount(
{
handle: { jwk: { kty: 'EC' }, kind: 'webcrypto-p256' },
keyType: 'p256',
publicKey: key.publicKey,
},
toAccountContext(),
),
).rejects.toBeInstanceOf(Keystore.KeyUnavailableError)
})
test('error: rejects handles written by another keystore', async () => {
const keystore = Keystore.webCryptoP256()
const key = await keystore.createKey()
const toAccount = keystore.toAccount(
{
handle: { keyTag: 'app.example.key', kind: 'secure-enclave' },
keyType: 'p256',
publicKey: key.publicKey,
},
toAccountContext(),
)
await expect(toAccount).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Unrecognized \`webCryptoP256\` keystore handle.]`,
)
await expect(toAccount).rejects.not.toBeInstanceOf(Keystore.KeyUnavailableError)
})
test('error: fails loudly when WebCrypto is unavailable', async () => {
const keystore = Keystore.webCryptoP256()
vi.stubGlobal('crypto', {})
try {
await expect(keystore.createKey()).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: \`webCryptoP256\` keystore requires WebCrypto (\`crypto.subtle\`) support.]`,
)
} finally {
vi.unstubAllGlobals()
}
})
})
describe('p256 / secp256k1', () => {
test.each([
{ factory: Keystore.p256, kind: 'p256' },
{ factory: Keystore.secp256k1, kind: 'secp256k1' },
] as const)('default: $kind handles survive JSON and rehydrate', async ({ factory, kind }) => {
const keystore = factory()
expect(keystore.requiresStructuredClone).toBeFalsy()
const key = await keystore.createKey()
expect(key.handle).toMatchObject({ kind })
expect(JSON.parse(JSON.stringify(key.handle))).toEqual(key.handle)
const account = await keystore.toAccount(
{
handle: JSON.parse(JSON.stringify(key.handle)),
keyType: kind,
publicKey: key.publicKey,
},
toAccountContext(),
)
expect(account.accessKeyAddress).toBe(
Address.fromPublicKey(PublicKey.fromHex(key.publicKey)).toLowerCase(),
)
})
test('error: rejects handles written by another keystore', async () => {
const keystore = Keystore.p256()
const key = await keystore.createKey()
expect(() =>
keystore.toAccount(
{
handle: { kind: 'secp256k1', privateKey: '0x01' },
keyType: 'p256',
publicKey: key.publicKey,
},
toAccountContext(),
),
).toThrowErrorMatchingInlineSnapshot(`[Error: Unrecognized \`p256\` keystore handle.]`)
})
})