import { describe, it, expect, beforeEach, vi } from "vitest"; import { Buffer } from "node:buffer"; import { encryptBytes, decryptBytes, extractKeyIdFromBytes, reencryptBytesToActive, encrypt, _resetCryptoCacheForTests, } from "../crypto"; /** * The binary "binary2" codec: AES-256-GCM over raw bytes with a key-id * header, no base64 detour. Same versioned-key discipline as the string * codec — fail-closed on unknown versions, malformed headers, and * unconfigured key ids. */ const KEY_V1 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const KEY_V2 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; function useSingleKey() { vi.unstubAllEnvs(); vi.stubEnv("ENCRYPTION_KEYS", ""); vi.stubEnv("ENCRYPTION_ACTIVE_KEY_ID", ""); vi.stubEnv("ENCRYPTION_KEY", KEY_V1); _resetCryptoCacheForTests(); } function useKeyMap(active: "v1" | "v2") { vi.unstubAllEnvs(); vi.stubEnv("ENCRYPTION_KEYS", JSON.stringify({ v1: KEY_V1, v2: KEY_V2 })); vi.stubEnv("ENCRYPTION_ACTIVE_KEY_ID", active); vi.stubEnv("ENCRYPTION_KEY", ""); _resetCryptoCacheForTests(); } describe("crypto binary codec (binary2)", () => { beforeEach(useSingleKey); it("round-trips raw binary payloads verbatim", () => { const payloads = [ Buffer.alloc(0), Buffer.from([0x00, 0x01, 0xff, 0xfe]), Buffer.from("%PDF-1.7 with ünïcode and NULs", "utf8"), Buffer.from(Array.from({ length: 100_000 }, (_, i) => i % 251)), ]; for (const plain of payloads) { const encrypted = encryptBytes(plain); expect(encrypted.equals(plain)).toBe(false); expect(decryptBytes(encrypted).equals(plain)).toBe(true); } }); it("writes the active key id into the header", () => { const payload = encryptBytes(Buffer.from("x")); expect(extractKeyIdFromBytes(payload)).toBe("v1"); }); it("costs a fixed small header, not the base64 +33 %", () => { const plain = Buffer.alloc(30_000, 7); const encrypted = encryptBytes(plain); // version(1) + idLen(1) + "v1"(2) + iv(12) + tag(16) = 32 bytes overhead. expect(encrypted.byteLength).toBe(plain.byteLength + 32); }); it("decrypts rows written under an older configured key after rotation", () => { const plain = Buffer.from("clinical letter bytes"); const oldPayload = encryptBytes(plain); useKeyMap("v2"); // v1 is still in the map — decrypt succeeds; re-encrypt moves to v2. expect(decryptBytes(oldPayload).equals(plain)).toBe(true); const rotated = reencryptBytesToActive(oldPayload); expect(extractKeyIdFromBytes(rotated)).toBe("v2"); expect(decryptBytes(rotated).equals(plain)).toBe(true); }); it("fails closed on an unconfigured key id", () => { const payload = encryptBytes(Buffer.from("secret")); vi.stubEnv("ENCRYPTION_KEY", KEY_V2); // v1 material replaced _resetCryptoCacheForTests(); // Header still says v1 but v1 now decodes to different key material → // GCM auth failure, never silent junk. expect(() => decryptBytes(payload)).toThrow(); }); it("fails closed on an unknown version byte", () => { const payload = encryptBytes(Buffer.from("secret")); const tampered = Buffer.from(payload); tampered[0] = 0x09; expect(() => decryptBytes(tampered)).toThrow(/version/i); }); it("fails closed on truncated payloads", () => { const payload = encryptBytes(Buffer.from("secret")); expect(() => decryptBytes(payload.subarray(0, 1))).toThrow(); expect(() => decryptBytes(payload.subarray(0, 10))).toThrow(); expect(() => decryptBytes(Buffer.alloc(0))).toThrow(); }); it("fails closed on a tampered ciphertext (GCM auth)", () => { const payload = encryptBytes(Buffer.from("secret")); const tampered = Buffer.from(payload); tampered[tampered.byteLength - 1] ^= 0x01; expect(() => decryptBytes(tampered)).toThrow(); }); it("extractKeyIdFromBytes returns null for garbage instead of throwing", () => { expect(extractKeyIdFromBytes(Buffer.alloc(0))).toBeNull(); expect(extractKeyIdFromBytes(Buffer.from([0x01, 0x02, 0x03]))).toBeNull(); // A legacy string-codec payload (UTF-8 of an encrypt() string) is not a // binary2 header either. expect( extractKeyIdFromBytes(Buffer.from(encrypt("hello"), "utf8")), ).toBeNull(); }); });