1import { describe, expect, it } from "vitest";
2import {
3 aes256gcmDecrypt,
4 aes256gcmEncrypt,
5 keyFromSecret,
6 randomToken,
7 safeEqual,
8 sha256hex
9} from "../crypto.js";
10
11describe("crypto", () => {
12 const KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
13
14 it("aes256gcm roundtrip", () => {
15 const plaintext = "hello webhook secret — ünïcode ✓";
16 const ct = aes256gcmEncrypt(plaintext, KEY);
17 expect(ct).not.toContain(plaintext);
18
19 const raw = Buffer.from(ct, "base64");
20 expect(raw.length).toBeGreaterThan(28);
21 expect(aes256gcmDecrypt(ct, KEY)).toBe(plaintext);
22 });
23
24 it("each encryption uses a fresh IV (ciphertexts differ)", () => {
25 const a = aes256gcmEncrypt("same input", KEY);
26 const b = aes256gcmEncrypt("same input", KEY);
27 expect(a).not.toBe(b);
28 expect(aes256gcmDecrypt(b, KEY)).toBe("same input");
29 });
30
31 it("tampering the ciphertext is detected (auth tag)", () => {
32 const ct = aes256gcmEncrypt("secret data", KEY);
33 const raw = Buffer.from(ct, "base64");
34 raw[raw.length - 1] ^= 0xff;
35 expect(() => aes256gcmDecrypt(raw.toString("base64"), KEY)).toThrow();
36 });
37
38 it("wrong key fails to decrypt", () => {
39 const ct = aes256gcmEncrypt("secret data", KEY);
40 const other = "ffff0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd";
41 expect(() => aes256gcmDecrypt(ct, other)).toThrow();
42 });
43
44 it("rejects malformed payloads", () => {
45 expect(() => aes256gcmDecrypt("short", KEY)).toThrow();
46
47
48 expect(() => aes256gcmEncrypt("x", Buffer.alloc(16))).toThrow();
49 expect(() => aes256gcmEncrypt("x", "too-short")).not.toThrow();
50 });
51
52 it("keyFromSecret: 64-hex key is used raw, anything else is sha256'd", () => {
53 expect(keyFromSecret(KEY).toString("hex")).toBe(KEY);
54 expect(keyFromSecret("short-key").toString("hex")).toBe(sha256hex("short-key"));
55 expect(keyFromSecret("any 32+ char string").length).toBe(32);
56 });
57
58 it("sha256hex known vectors", () => {
59 expect(sha256hex("")).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
60 expect(sha256hex("abc")).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
61 });
62
63 it("randomToken: unique, url-safe, length ≈ 4*bytes/3", () => {
64 const a = randomToken(32);
65 const b = randomToken(32);
66 expect(a).not.toBe(b);
67 expect(a).toMatch(/^[A-Za-z0-9_-]+$/);
68
69 expect(a.length).toBe(Math.ceil((32 * 8) / 6));
70 });
71
72 it("safeEqual is constant-shape and correct", () => {
73 expect(safeEqual("abc", "abc")).toBe(true);
74 expect(safeEqual("abc", "abd")).toBe(false);
75 expect(safeEqual("abc", "ab")).toBe(false);
76 });
77});
78