import { describe, expect, it } from 'vitest' import { overall, validateReport } from '@extant2000/evidence-record' import { checkClaims, formatClaims, type CryptoInventory } from '../src/index.js' /** * The shape this library is built around: a notes feature that really is * encrypted with a sound cipher, and is NOT end-to-end, because the server * holds the key. */ const notes: CryptoInventory = { claims: ['end-to-end-encrypted', 'encrypted-at-rest'], keys: [{ keyId: 'NOTES_KEY', heldBy: ['server', 'operator'], location: 'config/app.env', protects: 'note bodies', }], ciphers: [{ algorithm: 'AES', mode: 'GCM', keyBits: 256, purpose: 'at-rest', where: 'src/notes/store.ts', line: 88, }], } describe('custody is the check that matters', () => { it('sound cipher, false claim', () => { // Every cryptographic choice here is correct. The claim is still untrue, // and no amount of checking the algorithm would have caught it. const r = checkClaims(notes) const e2e = r.findings.find(f => f.id === 'end-to-end-encrypted')! const atRest = r.findings.find(f => f.id === 'encrypted-at-rest')! expect(e2e.determination).toBe('fail') expect(atRest.determination).toBe('pass') // the encryption really is fine expect(overall(r)).toBe('fail') }) it('names who holds the key, not just that the claim failed', () => { const text = formatClaims(notes, { evidence: 'full' }) expect(text).toContain('config/app.env') expect(text).toContain('server, operator') expect(text).toContain('this is encryption at rest, not end-to-end') }) it('ignores cipher strength entirely for a custody claim', () => { // AES-256-GCM does not make an operator-held key end-to-end. const stronger: CryptoInventory = { ...notes, ciphers: [{ ...notes.ciphers![0]!, keyBits: 512 }], } expect(checkClaims(stronger).findings[0]!.determination).toBe('fail') }) it('passes a custody claim when only the client holds the key', () => { const r = checkClaims({ claims: ['end-to-end-encrypted'], keys: [{ keyId: 'k', heldBy: ['client'], location: 'browser/keystore.ts', protects: 'messages' }], }) expect(overall(r)).toBe('pass') }) it('treats an HSM-held key as compatible with the claim', () => { // A key that never leaves an HSM is not readable by the operator, which // is the property the claim is about. const r = checkClaims({ claims: ['zero-knowledge'], keys: [{ keyId: 'k', heldBy: ['hsm'], location: 'kms://arn', protects: 'records' }], }) expect(overall(r)).toBe('pass') }) it('treats a third party as disqualifying', () => { // "End-to-end with a vendor in the middle" is the marketing sense of the // phrase, not the technical one. const r = checkClaims({ claims: ['end-to-end-encrypted'], keys: [{ keyId: 'k', heldBy: ['client', 'third-party'], location: 'vendor.md', protects: 'x' }], }) expect(overall(r)).toBe('fail') }) it('reports an undescribed custody as not-assessed, never a pass', () => { const r = checkClaims({ claims: ['end-to-end-encrypted'] }) expect(r.findings[0]!.determination).toBe('not-assessed') expect(overall(r)).toBe('not-assessed') expect(formatClaims({ claims: ['end-to-end-encrypted'] })).not.toContain('✓') }) }) describe('primitives', () => { const at = (algorithm: string, mode?: string): CryptoInventory => ({ claims: ['encrypted-at-rest'], ciphers: [{ algorithm, mode, purpose: 'at-rest', where: 'lib/crypto.ts', line: 4 }], }) it('fails a broken algorithm', () => { expect(checkClaims(at('3DES')).findings[0]!.determination).toBe('fail') expect(checkClaims(at('RC4')).findings[0]!.detail).toContain('RFC 7465') }) it('fails ECB regardless of the cipher', () => { const f = checkClaims(at('AES', 'ECB')).findings[0]! expect(f.determination).toBe('fail') expect(f.detail).toContain('identical blocks encrypt identically') }) it('accepts AES-GCM', () => { expect(checkClaims(at('AES', 'GCM')).findings[0]!.determination).toBe('pass') }) it('reports a broken primitive even when nothing was claimed about it', () => { // The absence of a marketing sentence is not a reason to leave MD5 in a // signature path. const r = checkClaims({ claims: [], ciphers: [{ algorithm: 'MD5', purpose: 'signature', where: 'lib/sign.ts', line: 12 }], }) expect(r.findings).toHaveLength(1) expect(r.findings[0]!.severity).toBe('critical') expect(r.findings[0]!.detail).toContain('collision-broken') }) }) describe('passwords', () => { const pw = (algorithm: string): CryptoInventory => ({ claims: ['passwords-hashed'], ciphers: [{ algorithm, purpose: 'password', where: 'server/api/auth.ts', line: 30 }], }) it('fails a fast hash — right for integrity, wrong for a password', () => { const f = checkClaims(pw('SHA-256')).findings[0]! expect(f.determination).toBe('fail') expect(f.detail).toContain('catastrophic for passwords') }) it('fails a reversible cipher, which is not hashing at all', () => { expect(checkClaims(pw('AES')).findings[0]!.determination).toBe('fail') }) it('accepts a real KDF', () => { for (const kdf of ['bcrypt', 'argon2id', 'scrypt', 'PBKDF2']) { expect(checkClaims(pw(kdf)).findings[0]!.determination).toBe('pass') } }) }) describe('transport', () => { it('fails a TLS floor below 1.2', () => { const f = checkClaims({ claims: ['encrypted-in-transit'], transport: [{ minTlsVersion: '1.0', where: 'config/tls.yml' }], }).findings[0]! expect(f.determination).toBe('fail') expect(f.detail).toContain('RFC 8996') }) it('accepts a 1.2 floor and a 1.3 floor', () => { for (const v of ['1.2', '1.3']) { expect(checkClaims({ claims: ['encrypted-in-transit'], transport: [{ minTlsVersion: v, where: 'config/tls.yml' }], }).findings[0]!.determination).toBe('pass') } }) it('reports an unpinned floor as not-assessed, not a pass', () => { // Without a floor the answer is the runtime default, which is not a // property of this system and can change under it. const f = checkClaims({ claims: ['encrypted-in-transit'], transport: [{ where: 'config/tls.yml' }], }).findings[0]! expect(f.determination).toBe('not-assessed') }) it('fails forward secrecy on a non-ephemeral suite', () => { const f = checkClaims({ claims: ['forward-secrecy'], transport: [{ where: 'config/tls.yml', cipherSuites: ['TLS_RSA_WITH_AES_128_CBC_SHA'] }], }).findings[0]! expect(f.determination).toBe('fail') expect(f.detail).toContain('retroactively decrypts') }) it('accepts ephemeral suites', () => { expect(checkClaims({ claims: ['forward-secrecy'], transport: [{ where: 't.yml', cipherSuites: ['TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'] }], }).findings[0]!.determination).toBe('pass') }) }) describe('conformance with the evidence-record standard', () => { it('every conclusion carries a citation', () => { expect(validateReport(checkClaims(notes))).toEqual([]) }) it('an empty inventory is not-assessed, never clean', () => { const r = checkClaims({ claims: [] }) expect(overall(r)).toBe('not-assessed') expect(formatClaims({ claims: [] })).not.toContain('✓') }) })