import { describe, expect, it } from "vitest"; import { aes256gcmDecrypt, aes256gcmEncrypt, keyFromSecret, randomToken, safeEqual, sha256hex } from "../crypto.js"; describe("crypto", () => { const KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; // 64 hex it("aes256gcm roundtrip", () => { const plaintext = "hello webhook secret — ünïcode ✓"; const ct = aes256gcmEncrypt(plaintext, KEY); expect(ct).not.toContain(plaintext); // envelope: base64 of iv(12) + tag(16) + ct const raw = Buffer.from(ct, "base64"); expect(raw.length).toBeGreaterThan(28); expect(aes256gcmDecrypt(ct, KEY)).toBe(plaintext); }); it("each encryption uses a fresh IV (ciphertexts differ)", () => { const a = aes256gcmEncrypt("same input", KEY); const b = aes256gcmEncrypt("same input", KEY); expect(a).not.toBe(b); expect(aes256gcmDecrypt(b, KEY)).toBe("same input"); }); it("tampering the ciphertext is detected (auth tag)", () => { const ct = aes256gcmEncrypt("secret data", KEY); const raw = Buffer.from(ct, "base64"); raw[raw.length - 1] ^= 0xff; // flip a byte of the ciphertext expect(() => aes256gcmDecrypt(raw.toString("base64"), KEY)).toThrow(); }); it("wrong key fails to decrypt", () => { const ct = aes256gcmEncrypt("secret data", KEY); const other = "ffff0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd"; expect(() => aes256gcmDecrypt(ct, other)).toThrow(); }); it("rejects malformed payloads", () => { expect(() => aes256gcmDecrypt("short", KEY)).toThrow(); // A non-32-byte RAW buffer key is rejected; a short STRING secret is // normalized (sha256 → 32 bytes) per keyFromSecret, so it must NOT throw. expect(() => aes256gcmEncrypt("x", Buffer.alloc(16))).toThrow(); expect(() => aes256gcmEncrypt("x", "too-short")).not.toThrow(); }); it("keyFromSecret: 64-hex key is used raw, anything else is sha256'd", () => { expect(keyFromSecret(KEY).toString("hex")).toBe(KEY); expect(keyFromSecret("short-key").toString("hex")).toBe(sha256hex("short-key")); expect(keyFromSecret("any 32+ char string").length).toBe(32); }); it("sha256hex known vectors", () => { expect(sha256hex("")).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); expect(sha256hex("abc")).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); }); it("randomToken: unique, url-safe, length ≈ 4*bytes/3", () => { const a = randomToken(32); const b = randomToken(32); expect(a).not.toBe(b); expect(a).toMatch(/^[A-Za-z0-9_-]+$/); // base64 (no padding) length = ceil(bytes*8/6); 32 bytes → 43 chars. expect(a.length).toBe(Math.ceil((32 * 8) / 6)); }); it("safeEqual is constant-shape and correct", () => { expect(safeEqual("abc", "abc")).toBe(true); expect(safeEqual("abc", "abd")).toBe(false); expect(safeEqual("abc", "ab")).toBe(false); }); });