113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
|
|
import {
|
|
certFingerprint,
|
|
generatePin,
|
|
constantTimeEqual,
|
|
mintToken,
|
|
PairingWindow,
|
|
PeerStore,
|
|
} from '../../src/sharing/pairing.js'
|
|
|
|
describe('certFingerprint', () => {
|
|
it('is a deterministic 64-char hex digest', () => {
|
|
const fp = certFingerprint('cert-bytes')
|
|
expect(fp).toMatch(/^[0-9a-f]{64}$/)
|
|
expect(certFingerprint('cert-bytes')).toBe(fp)
|
|
})
|
|
it('differs for different certs', () => {
|
|
expect(certFingerprint('a')).not.toBe(certFingerprint('b'))
|
|
})
|
|
})
|
|
|
|
describe('generatePin', () => {
|
|
it('is always 6 digits', () => {
|
|
for (let i = 0; i < 200; i++) expect(generatePin()).toMatch(/^\d{6}$/)
|
|
})
|
|
it('varies', () => {
|
|
const pins = new Set(Array.from({ length: 50 }, () => generatePin()))
|
|
expect(pins.size).toBeGreaterThan(1)
|
|
})
|
|
})
|
|
|
|
describe('constantTimeEqual', () => {
|
|
it('matches equal strings and rejects different ones', () => {
|
|
expect(constantTimeEqual('abc', 'abc')).toBe(true)
|
|
expect(constantTimeEqual('abc', 'abd')).toBe(false)
|
|
expect(constantTimeEqual('abc', 'abcd')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('mintToken', () => {
|
|
it('is url-safe and unique', () => {
|
|
const a = mintToken()
|
|
const b = mintToken()
|
|
expect(a).toMatch(/^[A-Za-z0-9_-]+$/)
|
|
expect(a).not.toBe(b)
|
|
})
|
|
})
|
|
|
|
describe('PairingWindow', () => {
|
|
it('accepts the correct PIN within the window', () => {
|
|
const w = new PairingWindow(1000, 1000, '123456')
|
|
expect(w.verify('123456', 1500)).toBe(true)
|
|
})
|
|
it('rejects a wrong PIN', () => {
|
|
const w = new PairingWindow(1000, 1000, '123456')
|
|
expect(w.verify('000000', 1200)).toBe(false)
|
|
})
|
|
it('rejects after the window expires', () => {
|
|
const w = new PairingWindow(1000, 1000, '123456')
|
|
expect(w.isOpen(3000)).toBe(false)
|
|
expect(w.verify('123456', 3000)).toBe(false)
|
|
})
|
|
it('is one-time: a consumed PIN cannot be reused', () => {
|
|
const w = new PairingWindow(10_000, 1000, '123456')
|
|
expect(w.verify('123456', 1100)).toBe(true)
|
|
expect(w.verify('123456', 1200)).toBe(false)
|
|
})
|
|
it('closes after too many wrong guesses (no brute force within the window)', () => {
|
|
const w = new PairingWindow(10_000, 1000, '123456', 5)
|
|
for (let i = 0; i < 5; i++) expect(w.verify('000000', 1000 + i)).toBe(false)
|
|
// window is now locked even though the TTL has not expired
|
|
expect(w.isOpen(1100)).toBe(false)
|
|
// and the correct PIN no longer works
|
|
expect(w.verify('123456', 1100)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('PeerStore', () => {
|
|
it('authorizes only when token AND fingerprint both match the same peer', () => {
|
|
const store = new PeerStore()
|
|
const a = store.pair('fp-aaa', 'MacBook')
|
|
const b = store.pair('fp-bbb', 'Mac Studio')
|
|
|
|
// correct pairing
|
|
expect(store.authorize(a.token, 'fp-aaa')).toBe(true)
|
|
// right token, wrong device fingerprint -> denied (stolen-token defense)
|
|
expect(store.authorize(a.token, 'fp-bbb')).toBe(false)
|
|
// wrong token on the right device -> denied
|
|
expect(store.authorize('not-the-token', 'fp-aaa')).toBe(false)
|
|
// unknown device -> denied
|
|
expect(store.authorize(a.token, 'fp-ccc')).toBe(false)
|
|
expect(store.authorize(b.token, 'fp-bbb')).toBe(true)
|
|
})
|
|
|
|
it('revokes a peer on unpair', () => {
|
|
const store = new PeerStore()
|
|
const p = store.pair('fp-x', 'Laptop')
|
|
expect(store.authorize(p.token, 'fp-x')).toBe(true)
|
|
expect(store.unpair('fp-x')).toBe(true)
|
|
expect(store.authorize(p.token, 'fp-x')).toBe(false)
|
|
expect(store.list()).toHaveLength(0)
|
|
})
|
|
|
|
it('round-trips through serializable peer records', () => {
|
|
const store = new PeerStore()
|
|
store.pair('fp-1', 'A')
|
|
const restored = new PeerStore(store.list())
|
|
const peer = restored.list()[0]!
|
|
expect(restored.authorize(peer.token, 'fp-1')).toBe(true)
|
|
})
|
|
})
|