import { base32Decode, base32Encode, base64UrlDecode, base64UrlEncode, bytesToHex, computeJwkThumbprint, generateKeyPair, hexToBytes, importPrivateKey, sign, } from '@pdsjs/core/crypto'; import { describe, expect, test } from 'vitest'; describe('Base32 Encoding', () => { test('encodes bytes to base32lower', () => { const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]); const encoded = base32Encode(bytes); expect(typeof encoded).toBe('string'); expect(encoded).toMatch(/^[a-z2-7]+$/); }); test('base32 encode/decode roundtrip', () => { const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]); const encoded = base32Encode(original); const decoded = base32Decode(encoded); expect(decoded).toEqual(original); }); }); describe('P-256 Signing', () => { test('generates key pair with correct sizes', async () => { const kp = await generateKeyPair(); expect(kp.privateKey.length).toBe(32); expect(kp.publicKey.length).toBe(33); // compressed expect(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03).toBe(true); }); test('can sign data with generated key', async () => { const kp = await generateKeyPair(); const key = await importPrivateKey(kp.privateKey); const data = new TextEncoder().encode('test message'); const sig = await sign(key, data); expect(sig.length).toBe(64); // r (32) + s (32) }); test('different messages produce different signatures', async () => { const kp = await generateKeyPair(); const key = await importPrivateKey(kp.privateKey); const sig1 = await sign(key, new TextEncoder().encode('message 1')); const sig2 = await sign(key, new TextEncoder().encode('message 2')); expect(sig1).not.toEqual(sig2); }); test('bytesToHex and hexToBytes roundtrip', () => { const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]); const hex = bytesToHex(original); const back = hexToBytes(hex); expect(hex).toBe('000ff0ffabcd'); expect(back).toEqual(original); }); test('importPrivateKey rejects invalid key lengths', async () => { // Too short await expect(() => importPrivateKey(new Uint8Array(31))).rejects.toThrow( /expected 32 bytes, got 31/, ); // Too long await expect(() => importPrivateKey(new Uint8Array(33))).rejects.toThrow( /expected 32 bytes, got 33/, ); // Empty await expect(() => importPrivateKey(new Uint8Array(0))).rejects.toThrow( /expected 32 bytes, got 0/, ); }); test('importPrivateKey rejects non-Uint8Array input', async () => { // Arrays have .length but aren't Uint8Array // @ts-expect-error - Testing runtime validation with invalid type await expect(() => importPrivateKey([1, 2, 3])).rejects.toThrow( /Invalid private key/, ); // Strings don't work either // @ts-expect-error - Testing runtime validation with invalid type await expect(() => importPrivateKey('not bytes')).rejects.toThrow( /Invalid private key/, ); // null/undefined // @ts-expect-error - Testing runtime validation with invalid type await expect(() => importPrivateKey(null)).rejects.toThrow( /Invalid private key/, ); }); }); describe('JWT Base64URL', () => { test('base64UrlEncode encodes bytes correctly', () => { const input = new TextEncoder().encode('hello world'); const encoded = base64UrlEncode(input); expect(encoded).toBe('aGVsbG8gd29ybGQ'); expect(encoded.includes('+')).toBe(false); expect(encoded.includes('/')).toBe(false); expect(encoded.includes('=')).toBe(false); }); test('base64UrlDecode decodes string correctly', () => { const decoded = base64UrlDecode('aGVsbG8gd29ybGQ'); const str = new TextDecoder().decode(decoded); expect(str).toBe('hello world'); }); test('base64url roundtrip', () => { const original = new Uint8Array([0, 1, 2, 255, 254, 253]); const encoded = base64UrlEncode(original); const decoded = base64UrlDecode(encoded); expect(decoded).toEqual(original); }); }); describe('JWK Thumbprint', () => { test('computes deterministic thumbprint for EC key', async () => { // Test vector: known JWK and its expected thumbprint const jwk = { kty: 'EC', crv: 'P-256', x: 'WbbCfHGZ9QtKsVuMdPZ8hBbP2949N_CSLG3LVV0nnKY', y: 'eSgPlDj0RVMw8t8u4MvCYG4j_JfDwvrMUUwEEHVLmqQ', }; const jkt1 = await computeJwkThumbprint(jwk); const jkt2 = await computeJwkThumbprint(jwk); // Thumbprint must be deterministic expect(jkt1).toBe(jkt2); // Must be base64url-encoded SHA-256 (43 chars) expect(jkt1.length).toBe(43); // Must only contain base64url characters expect(jkt1).toMatch(/^[A-Za-z0-9_-]+$/); }); test('produces different thumbprints for different keys', async () => { const jwk1 = { kty: 'EC', crv: 'P-256', x: 'WbbCfHGZ9QtKsVuMdPZ8hBbP2949N_CSLG3LVV0nnKY', y: 'eSgPlDj0RVMw8t8u4MvCYG4j_JfDwvrMUUwEEHVLmqQ', }; const jwk2 = { kty: 'EC', crv: 'P-256', x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', }; const jkt1 = await computeJwkThumbprint(jwk1); const jkt2 = await computeJwkThumbprint(jwk2); expect(jkt1).not.toBe(jkt2); }); });