import { createCipheriv, createDecipheriv, randomBytes, hkdfSync, } from "crypto"; const ALGORITHM = "aes-256-gcm"; // Static salt — committed to source so it is stable across deployments. // It does NOT need to be secret; its purpose is domain-separation and to // prevent the raw secret from being used directly as a key. const HKDF_SALT = Buffer.from("epicure-encryption-v1-salt", "utf8"); const HKDF_INFO = Buffer.from("epicure-aes-256-gcm-key", "utf8"); function getKey(): Buffer { // Prefer a dedicated encryption secret; fall back to BETTER_AUTH_SECRET so // existing deployments keep working without an immediate migration. const secret = process.env["ENCRYPTION_SECRET"] ?? process.env["BETTER_AUTH_SECRET"]; if (!secret) throw new Error("ENCRYPTION_SECRET (or BETTER_AUTH_SECRET) is required"); // HKDF-SHA256 provides proper key derivation with domain separation. // This replaces the previous raw SHA-256 hash which offered no stretching // or salt, making brute-force against leaked ciphertexts trivial. return Buffer.from( hkdfSync("sha256", secret, HKDF_SALT, HKDF_INFO, 32) ); } export function encrypt(plaintext: string): string { const key = getKey(); const iv = randomBytes(12); const cipher = createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); const authTag = (cipher as ReturnType & { getAuthTag(): Buffer }).getAuthTag(); return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`; } export function decrypt(ciphertext: string): string { const parts = ciphertext.split(":"); if (parts.length !== 3) throw new Error("Invalid ciphertext format"); const [ivHex, authTagHex, encryptedHex] = parts as [string, string, string]; const key = getKey(); const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); const encrypted = Buffer.from(encryptedHex, "hex"); const decipher = createDecipheriv(ALGORITHM, key, iv); (decipher as ReturnType & { setAuthTag(tag: Buffer): void }).setAuthTag(authTag); return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8"); }