import test from 'brittle' import crypto from 'crypto' import { deriveMacKey, signCapability, verifyCapability, createAdminProof, verifyAdminProof, classifyConnectionInput, encodePeardockInvite, decodePeardockInvite, safeEqual, } from '../shared/crypto-auth.js' import { Roles } from '../shared/protocol.js' const SEED = 'ab'.repeat(32) const SEED2 = 'cd'.repeat(32) const PEER = '11'.repeat(32) const SERVER_PK = '22'.repeat(32) test('deriveMacKey is deterministic and differs by seed', (t) => { const a = deriveMacKey(SEED) const b = deriveMacKey(SEED) const c = deriveMacKey(SEED2) t.is(a.length, 32) t.ok(safeEqual(a, b)) t.absent(safeEqual(a, c)) }) test('sign and verify capability', (t) => { const { token, payload } = signCapability(SEED, { role: Roles.operator, ttlMs: 3600_000, }) t.ok(token.includes('.')) t.is(payload.role, Roles.operator) const res = verifyCapability(SEED, token, { peerId: PEER }) t.ok(res.ok) t.is(res.payload.role, Roles.operator) t.ok(res.payload.jti) }) test('tampered capability fails', (t) => { const { token } = signCapability(SEED, { role: Roles.admin, ttlMs: 3600_000 }) const [body, mac] = token.split('.') // Flip last char of body (base64url) const flipped = body.slice(0, -1) + (body.endsWith('A') ? 'B' : 'A') + '.' + mac const res = verifyCapability(SEED, flipped) t.absent(res.ok) t.is(res.code, 'CAPABILITY_INVALID') }) test('wrong seed fails verify', (t) => { const { token } = signCapability(SEED, { role: Roles.operator, ttlMs: 3600_000 }) const res = verifyCapability(SEED2, token) t.absent(res.ok) t.is(res.code, 'CAPABILITY_INVALID') }) test('expired capability fails', (t) => { const { token } = signCapability(SEED, { role: Roles.viewer, ttlMs: 60_000 }) const res = verifyCapability(SEED, token, { now: Date.now() + 120_000 }) t.absent(res.ok) t.is(res.code, 'CAPABILITY_EXPIRED') }) test('persistent capability has null exp and never expires', (t) => { const { token, payload } = signCapability(SEED, { role: Roles.operator, forever: true, }) t.is(payload.exp, null) const res = verifyCapability(SEED, token, { now: Date.now() + 100 * 365 * 24 * 3600 * 1000 }) t.ok(res.ok) t.is(res.payload.exp, null) }) test('peer-bound capability mismatch', (t) => { const { token } = signCapability(SEED, { role: Roles.operator, ttlMs: 3600_000, peerId: PEER, }) const bad = verifyCapability(SEED, token, { peerId: '33'.repeat(32) }) t.absent(bad.ok) t.is(bad.code, 'CAPABILITY_PEER_MISMATCH') const good = verifyCapability(SEED, token, { peerId: PEER }) t.ok(good.ok) }) test('spent jti check', (t) => { const { token, payload } = signCapability(SEED, { role: Roles.operator, ttlMs: 3600_000 }) const spent = new Set([payload.jti]) const res = verifyCapability(SEED, token, { allowSpentCheck: (jti) => !spent.has(jti), }) t.absent(res.ok) t.is(res.code, 'CAPABILITY_SPENT') }) test('admin proof round-trip', (t) => { const proof = createAdminProof(SEED, { peerId: PEER, serverPublicKeyHex: SERVER_PK, }) t.ok(proof.nonce) t.ok(proof.mac) const ok = verifyAdminProof(SEED, proof, { peerId: PEER, serverPublicKeyHex: SERVER_PK, }) t.ok(ok.ok) const bad = verifyAdminProof(SEED2, proof, { peerId: PEER, serverPublicKeyHex: SERVER_PK, }) t.absent(bad.ok) t.is(bad.code, 'ADMIN_PROOF_FAILED') }) test('classifyConnectionInput', (t) => { t.is(classifyConnectionInput(SERVER_PK), 'publicKey') t.is(classifyConnectionInput('aa'.repeat(24)), 'legacyInvite') t.is(classifyConnectionInput('ynr' + 'a'.repeat(100)), 'legacyAutopassInvite') t.is(classifyConnectionInput('not-valid'), 'unknown') t.is(classifyConnectionInput(''), 'unknown') }) test('peardock pd1 invite encodes and decodes package', (t) => { const { token } = signCapability(SEED, { role: Roles.operator, forever: true }) const share = encodePeardockInvite({ publicKeyHex: SERVER_PK, capability: token, role: Roles.operator, jti: 'abc', }) t.ok(share.startsWith('pd1.')) t.is(classifyConnectionInput(share), 'peardockInvite') const pkg = decodePeardockInvite(share) t.ok(pkg) t.is(pkg.publicKeyHex, SERVER_PK) t.is(pkg.capability, token) t.is(pkg.role, Roles.operator) }) test('safeEqual length mismatch', (t) => { t.absent(safeEqual(Buffer.from('ab'), Buffer.from('abc'))) t.ok(safeEqual(crypto.randomBytes(0), Buffer.alloc(0))) })