chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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);
});
});