/** * Broker: AES-GCM app-password encryption * * Tests the encryptPassword / decryptPassword helpers in src/broker/crypto.ts. * Key behaviors verified (T-03-01, T-03-03): * - Lossless roundtrip * - Unique IVs per encryption (never reuse IV) * - GCM auth tag tamper detection * - Stored payload shape */ import { describe, it, expect, beforeAll } from 'vitest'; // Set a fixed 32-byte (64-char hex) key before importing the module const TEST_KEY = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2'; beforeAll(() => { process.env.APP_PASSWORD_ENCRYPTION_KEY = TEST_KEY; }); // Dynamic import so env is set before module-level KEY evaluation async function getCrypto() { return import('../../src/broker/crypto.js'); } describe('encryptPassword / decryptPassword', () => { it('roundtrip: decrypt(encrypt(plaintext)) === plaintext', async () => { const { encryptPassword, decryptPassword } = await getCrypto(); const plaintext = 'my-fastmail-app-password-abc123'; const encrypted = encryptPassword(plaintext); expect(decryptPassword(encrypted)).toBe(plaintext); }); it('different IVs produce different ciphertext for the same plaintext', async () => { const { encryptPassword } = await getCrypto(); const plaintext = 'same-password'; const enc1 = encryptPassword(plaintext); const enc2 = encryptPassword(plaintext); // The JSON payloads must differ (different IVs → different ciphertext) expect(enc1).not.toBe(enc2); // And the IVs themselves must differ const p1 = JSON.parse(enc1); const p2 = JSON.parse(enc2); expect(p1.iv).not.toBe(p2.iv); }); it('decrypting with a tampered authTag throws', async () => { const { encryptPassword, decryptPassword } = await getCrypto(); const encrypted = encryptPassword('secret'); const payload = JSON.parse(encrypted); // Flip first byte of authTag payload.authTag = 'ff' + payload.authTag.slice(2); expect(() => decryptPassword(JSON.stringify(payload))).toThrow(); }); it('decrypting with a tampered ciphertext throws', async () => { const { encryptPassword, decryptPassword } = await getCrypto(); const encrypted = encryptPassword('secret'); const payload = JSON.parse(encrypted); // Flip first byte of ciphertext payload.ciphertext = 'ff' + payload.ciphertext.slice(2); expect(() => decryptPassword(JSON.stringify(payload))).toThrow(); }); it('stored payload is valid JSON with iv, authTag, ciphertext fields', async () => { const { encryptPassword } = await getCrypto(); const encrypted = encryptPassword('test-password'); const payload = JSON.parse(encrypted); expect(payload).toHaveProperty('iv'); expect(payload).toHaveProperty('authTag'); expect(payload).toHaveProperty('ciphertext'); // All values are non-empty hex strings expect(typeof payload.iv).toBe('string'); expect(payload.iv.length).toBeGreaterThan(0); expect(typeof payload.authTag).toBe('string'); expect(payload.authTag.length).toBeGreaterThan(0); expect(typeof payload.ciphertext).toBe('string'); expect(payload.ciphertext.length).toBeGreaterThan(0); }); });