import { randomBytes } from 'node:crypto'; import { SessionDecryptError, decrypt, encrypt } from './session-crypto'; const KEY = randomBytes(32); const OTHER_KEY = randomBytes(32); const PLAINTEXT = JSON.stringify({ user: { oid: 'abc', tid: 'def' }, createdAt: 1714000000000 }); describe('session-crypto', () => { it('round-trips a payload', () => { const ciphertext = encrypt(PLAINTEXT, KEY); expect(decrypt(ciphertext, KEY)).toBe(PLAINTEXT); }); it('produces the v1 envelope shape', () => { const ciphertext = encrypt(PLAINTEXT, KEY); const parts = ciphertext.split('.'); expect(parts).toHaveLength(4); expect(parts[0]).toBe('v1'); }); it('uses a fresh IV per encryption', () => { const a = encrypt(PLAINTEXT, KEY); const b = encrypt(PLAINTEXT, KEY); expect(a).not.toBe(b); // Same plaintext + key + different IV ⇒ different ciphertexts but // both decrypt back to the same value. expect(decrypt(a, KEY)).toBe(PLAINTEXT); expect(decrypt(b, KEY)).toBe(PLAINTEXT); }); it('rejects ciphertext encrypted under a different key', () => { const ciphertext = encrypt(PLAINTEXT, KEY); expect(() => decrypt(ciphertext, OTHER_KEY)).toThrow(SessionDecryptError); }); it('rejects tampered ciphertext (auth tag verification)', () => { const ciphertext = encrypt(PLAINTEXT, KEY); const parts = ciphertext.split('.'); // Flip a byte in the ciphertext segment. const ct = Buffer.from(parts[3] as string, 'base64url'); ct[0] = (ct[0] ?? 0) ^ 0xff; parts[3] = ct.toString('base64url'); const tampered = parts.join('.'); expect(() => decrypt(tampered, KEY)).toThrow(SessionDecryptError); }); it('rejects a tampered auth tag', () => { const ciphertext = encrypt(PLAINTEXT, KEY); const parts = ciphertext.split('.'); const tag = Buffer.from(parts[2] as string, 'base64url'); tag[0] = (tag[0] ?? 0) ^ 0xff; parts[2] = tag.toString('base64url'); const tampered = parts.join('.'); expect(() => decrypt(tampered, KEY)).toThrow(SessionDecryptError); }); it('rejects an unknown version', () => { const ciphertext = encrypt(PLAINTEXT, KEY); const parts = ciphertext.split('.'); parts[0] = 'v999'; expect(() => decrypt(parts.join('.'), KEY)).toThrow(/unsupported version "v999"/); }); it('rejects a malformed envelope (wrong segment count)', () => { expect(() => decrypt('not-even-close', KEY)).toThrow(/malformed envelope/); expect(() => decrypt('v1.a.b', KEY)).toThrow(/malformed envelope/); }); it('rejects an IV of the wrong length', () => { const ciphertext = encrypt(PLAINTEXT, KEY); const parts = ciphertext.split('.'); parts[1] = Buffer.alloc(8).toString('base64url'); expect(() => decrypt(parts.join('.'), KEY)).toThrow(/iv length 8/); }); it('rejects a tag of the wrong length', () => { const ciphertext = encrypt(PLAINTEXT, KEY); const parts = ciphertext.split('.'); parts[2] = Buffer.alloc(8).toString('base64url'); expect(() => decrypt(parts.join('.'), KEY)).toThrow(/tag length 8/); }); });