1
21import {
22 createCipheriv,
23 createDecipheriv,
24 createHash,
25 createHmac,
26 randomBytes,
27 timingSafeEqual
28} from "node:crypto";
29
30const ALGO = "aes-256-gcm";
31const IV_LEN = 12;
32const TAG_LEN = 16;
33
34
35export function keyFromSecret(secret: string): Buffer {
36 if (secret.length === 64 && /^[0-9a-fA-F]{64}$/.test(secret)) {
37 return Buffer.from(secret, "hex");
38 }
39 return createHash("sha256").update(secret, "utf8").digest();
40}
41
42
43export function aes256gcmEncrypt(plaintext: string, key: string | Buffer): string {
44 const k = typeof key === "string" ? keyFromSecret(key) : key;
45 if (k.length !== 32) throw new Error("AES-256 key must be 32 bytes");
46 const iv = randomBytes(IV_LEN);
47 const cipher = createCipheriv(ALGO, k, iv);
48 const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
49 const tag = cipher.getAuthTag();
50 return Buffer.concat([iv, tag, ct]).toString("base64");
51}
52
53
57export function aes256gcmDecrypt(payload: string, key: string | Buffer): string {
58 const k = typeof key === "string" ? keyFromSecret(key) : key;
59 if (k.length !== 32) throw new Error("AES-256 key must be 32 bytes");
60 const raw = Buffer.from(payload, "base64");
61 if (raw.length < IV_LEN + TAG_LEN) throw new Error("ciphertext too short");
62 const iv = raw.subarray(0, IV_LEN);
63 const tag = raw.subarray(IV_LEN, IV_LEN + TAG_LEN);
64 const ct = raw.subarray(IV_LEN + TAG_LEN);
65 const decipher = createDecipheriv(ALGO, k, iv);
66 decipher.setAuthTag(tag);
67 return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
68}
69
70
71export function sha256hex(value: string): string {
72 return createHash("sha256").update(value, "utf8").digest("hex");
73}
74
75
76export function hmacSha256hex(key: string, message: string): string {
77 return createHmac("sha256", key).update(message, "utf8").digest("hex");
78}
79
80
81export function randomToken(bytes = 32): string {
82 return randomBytes(bytes)
83 .toString("base64")
84 .replace(/\+/g, "-")
85 .replace(/\
86 .replace(/=+$/, "");
87}
88
89
93export function safeEqual(a: string, b: string): boolean {
94 const ba = Buffer.from(a, "utf8");
95 const bb = Buffer.from(b, "utf8");
96 if (ba.length !== bb.length) return false;
97 return timingSafeEqual(ba, bb);
98}
99