52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
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<typeof createCipheriv> & { 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<typeof createDecipheriv> & { setAuthTag(tag: Buffer): void }).setAuthTag(authTag);
|
|
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
|
}
|