import { describe, expect, it } from "vitest"; import { decrypt, deriveAuditHmacKey, encrypt, isEncrypted, } from "../../../apps/api/src/lib/encryption.js"; const PREFIX_LEN = "$ENC$".length; describe("encryption", () => { const testKey = "a".repeat(64); // 32 bytes hex-encoded it("encrypts and decrypts a value", async () => { const plaintext = "my-secret-oidc-client-secret"; const encrypted = await encrypt(plaintext, testKey); expect(encrypted).not.toBe(plaintext); expect(isEncrypted(encrypted)).toBe(true); const decrypted = await decrypt(encrypted, testKey); expect(decrypted).toBe(plaintext); }); it("produces different ciphertext for same plaintext (random IV)", async () => { const plaintext = "same-value"; const a = await encrypt(plaintext, testKey); const b = await encrypt(plaintext, testKey); expect(a).not.toBe(b); }); it("isEncrypted returns false for plaintext", () => { expect(isEncrypted("just-a-normal-value")).toBe(false); expect(isEncrypted("")).toBe(false); }); it("decrypt returns null for wrong key", async () => { const encrypted = await encrypt("secret", testKey); const wrongKey = "b".repeat(64); const result = await decrypt(encrypted, wrongKey); expect(result).toBeNull(); }); it("decrypt tries previous key on failure", async () => { const oldKey = "c".repeat(64); const newKey = "d".repeat(64); const encrypted = await encrypt("secret", oldKey); const result = await decrypt(encrypted, newKey, oldKey); expect(result).toBe("secret"); }); it("decrypt passes through non-encrypted values", async () => { const result = await decrypt("plain-text-value", testKey); expect(result).toBe("plain-text-value"); }); it("deriveAuditHmacKey produces a 32-byte buffer", async () => { const key = await deriveAuditHmacKey(testKey); expect(key).toBeInstanceOf(Buffer); expect(key.length).toBe(32); }); it("encrypts and decrypts an empty string", async () => { const encrypted = await encrypt("", testKey); expect(isEncrypted(encrypted)).toBe(true); const decrypted = await decrypt(encrypted, testKey); expect(decrypted).toBe(""); }); it("encrypts and decrypts a very long input", async () => { const plaintext = "x".repeat(10_000); const encrypted = await encrypt(plaintext, testKey); const decrypted = await decrypt(encrypted, testKey); expect(decrypted).toBe(plaintext); }); it("encrypts and decrypts unicode and emoji characters", async () => { const plaintext = "\u{1F9A6}\u{1F30A} Otter says: éàüß 你好 АБВ"; const encrypted = await encrypt(plaintext, testKey); const decrypted = await decrypt(encrypted, testKey); expect(decrypted).toBe(plaintext); }); it("encrypts and decrypts special characters (newlines, tabs, null bytes)", async () => { const plaintext = "line1\nline2\ttab\0null\r\nwindows"; const encrypted = await encrypt(plaintext, testKey); const decrypted = await decrypt(encrypted, testKey); expect(decrypted).toBe(plaintext); }); it("isEncrypted returns true for $ENC$ prefix with invalid base64", () => { expect(isEncrypted("$ENC$!!!not-valid-base64%%%")).toBe(true); }); it("decrypt returns null for $ENC$ with truncated data", async () => { const result = await decrypt("$ENC$AQID", testKey); expect(result).toBeNull(); }); it("decrypt returns null for $ENC$ with corrupted ciphertext", async () => { const encrypted = await encrypt("hello", testKey); const corrupted = `${encrypted.slice(0, PREFIX_LEN + 10)}AAAA${encrypted.slice(PREFIX_LEN + 14)}`; const result = await decrypt(corrupted, testKey); expect(result).toBeNull(); }); it("deriveAuditHmacKey produces different keys for different master keys", async () => { const keyA = await deriveAuditHmacKey("a".repeat(64)); const keyB = await deriveAuditHmacKey("b".repeat(64)); expect(keyA.equals(keyB)).toBe(false); }); it("deriveAuditHmacKey is deterministic", async () => { const first = await deriveAuditHmacKey(testKey); const second = await deriveAuditHmacKey(testKey); expect(first.equals(second)).toBe(true); }); it("two encryptions of the same plaintext decrypt to the same value", async () => { const plaintext = "roundtrip-check"; const encA = await encrypt(plaintext, testKey); const encB = await encrypt(plaintext, testKey); expect(encA).not.toBe(encB); expect(await decrypt(encA, testKey)).toBe(plaintext); expect(await decrypt(encB, testKey)).toBe(plaintext); }); });