import { describe, expect, it } from 'vitest'; import { bytesEqual, COMMIT_MAC_TYPE, COMMIT_VERSION, computeCommitMac, computeRecordMac, formatMacHeader, formatWantMacHeader, hkdfSha256, parseMacHeader, parseWantMacHeader, RECORD_MAC_TYPE, SpaceMacError, timingSafeEqual, uint16be, verifyCommitMac, verifyRecordMac, } from '../src/mac.js'; import { fromHex, referenceCommit, toHex, vectors } from './helpers/vectors.js'; const secret = fromHex(vectors.commit.secretHex); describe('commit MAC', () => { it('reproduces the reference MAC byte for byte', async () => { expect(toHex(await computeCommitMac(secret, referenceCommit()))).toBe( vectors.commit.macHex, ); }); it('verifies the reference MAC and refuses others', async () => { const mac = fromHex(vectors.commit.macHex); expect(await verifyCommitMac(secret, referenceCommit(), mac)).toBe(true); expect( await verifyCommitMac(secret, referenceCommit(), mac.subarray(0, 31)), ).toBe(false); expect( await verifyCommitMac( secret, { ...referenceCommit(), repo: 'did:plc:cccc' }, mac, ), ).toBe(false); }); it('rejects a field longer than uint16', () => { expect(() => uint16be(0x10000)).toThrow(); }); }); describe('record MAC', () => { it('reproduces the reference MAC byte for byte', async () => { const { uri, cid, macHex } = vectors.record; expect(toHex(await computeRecordMac(secret, uri, cid))).toBe(macHex); expect(await verifyRecordMac(secret, uri, cid, fromHex(macHex))).toBe(true); expect(await verifyRecordMac(secret, `${uri}x`, cid, fromHex(macHex))).toBe( false, ); }); }); describe('headers', () => { it('formats the response header as the reference does', () => { expect( formatMacHeader(COMMIT_MAC_TYPE, fromHex(vectors.commit.macHex)), ).toBe(vectors.commit.macHeader); expect( formatMacHeader(RECORD_MAC_TYPE, fromHex(vectors.record.macHex)), ).toBe(vectors.record.macHeader); }); it('formats the request header as the reference does', () => { expect(formatWantMacHeader(COMMIT_MAC_TYPE, vectors.kem.keyShare)).toBe( vectors.kem.wantMacHeader, ); }); it('parses both back', () => { expect( toHex(parseMacHeader(vectors.commit.macHeader, COMMIT_MAC_TYPE)), ).toBe(vectors.commit.macHex); expect(parseWantMacHeader(vectors.kem.wantMacHeader, COMMIT_MAC_TYPE)).toBe( vectors.kem.keyShare, ); }); it('refuses the wrong type, version or shape', () => { for (const value of [ null, '', 'type="atproto-space-repo", ver=2, mac=:AAAA:', 'type="atproto-space-record", ver=1, mac=:AAAA:', 'type="atproto-space-repo", ver=1, mac="AAAA"', 'type="atproto-space-repo", ver=1, mac=(:AAAA:)', 'type=atproto-space-repo, ver=1, mac=:AAAA:', '(((', ]) { expect( () => parseMacHeader(value, COMMIT_MAC_TYPE), String(value), ).toThrow(SpaceMacError); } expect(() => parseWantMacHeader('type="atproto-space-repo", ver=1', COMMIT_MAC_TYPE), ).toThrow(SpaceMacError); expect(() => parseWantMacHeader( 'type="atproto-space-repo", ver=1, key_share=:AAAA:', COMMIT_MAC_TYPE, ), ).toThrow(SpaceMacError); }); it('carries the BadSpaceMac code', () => { expect(new SpaceMacError('x').code).toBe('BadSpaceMac'); }); }); describe('hkdfSha256', () => { // RFC 5869 test case 1. it('matches RFC 5869', async () => { const okm = await hkdfSha256( fromHex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), fromHex('000102030405060708090a0b0c'), fromHex('f0f1f2f3f4f5f6f7f8f9'), 42, ); expect(toHex(okm)).toBe( '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865', ); }); // RFC 5869 test case 3: an empty salt. it('treats an empty salt as zeros', async () => { const okm = await hkdfSha256( fromHex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), new Uint8Array(0), new Uint8Array(0), 42, ); expect(toHex(okm)).toBe( '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8', ); }); }); describe('byte helpers', () => { it('compare by length and content', () => { expect(bytesEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2]))).toBe( true, ); expect(bytesEqual(new Uint8Array([1, 2]), new Uint8Array([1, 3]))).toBe( false, ); expect(bytesEqual(new Uint8Array([1]), new Uint8Array([1, 2]))).toBe(false); expect(timingSafeEqual(new Uint8Array([1]), new Uint8Array([1, 2]))).toBe( false, ); }); it('COMMIT_VERSION is 1', () => { expect(COMMIT_VERSION).toBe(1); }); });