import crypto from "node:crypto"; const encryptedSecretPrefix = "ttenc:v1:"; let warnedAboutFallbackKey = false; let cachedKey: Buffer | null = null; function getEncryptionMaterial() { const configuredKey = process.env.SECRET_ENCRYPTION_KEY ?? process.env.TICKETTRACKER_SECRET_KEY; if (configuredKey) { return configuredKey; } if (!process.env.DATABASE_URL) { throw new Error("SECRET_ENCRYPTION_KEY is required to encrypt database secrets"); } if (!warnedAboutFallbackKey) { console.warn("SECRET_ENCRYPTION_KEY is not set; deriving database secret encryption from DATABASE_URL."); warnedAboutFallbackKey = true; } return process.env.DATABASE_URL; } function getEncryptionKey() { if (!cachedKey) { cachedKey = crypto.createHash("sha256").update(getEncryptionMaterial()).digest(); } return cachedKey; } export function isEncryptedSecret(value: string | null | undefined) { return typeof value === "string" && value.startsWith(encryptedSecretPrefix); } export function encryptSecret(value: string) { if (isEncryptedSecret(value)) { return value; } const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv("aes-256-gcm", getEncryptionKey(), iv); const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]); const tag = cipher.getAuthTag(); return `${encryptedSecretPrefix}${iv.toString("base64url")}.${tag.toString("base64url")}.${encrypted.toString("base64url")}`; } export function decryptSecret(value: string | null | undefined) { if (!value) { return null; } if (!isEncryptedSecret(value)) { return value; } const parts = value.slice(encryptedSecretPrefix.length).split("."); if (parts.length !== 3) { throw new Error("Encrypted database secret has an invalid format"); } const [ivText, tagText, encryptedText] = parts; const decipher = crypto.createDecipheriv("aes-256-gcm", getEncryptionKey(), Buffer.from(ivText, "base64url")); decipher.setAuthTag(Buffer.from(tagText, "base64url")); return Buffer.concat([decipher.update(Buffer.from(encryptedText, "base64url")), decipher.final()]).toString("utf8"); }