import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto"; const ENC_PREFIX = "enc:v1:"; function getEncryptionKey() { const secret = process.env.TOKEN_ENCRYPTION_KEY || process.env.NEXTAUTH_SECRET; if (!secret) { throw new Error("TOKEN_ENCRYPTION_KEY or NEXTAUTH_SECRET must be set to encrypt secrets"); } return createHash("sha256").update(secret).digest(); } /** Encrypt a secret for DB storage (AES-256-GCM). Idempotent if already encrypted. */ export function encryptSecret(plaintext: string): string { if (!plaintext) return plaintext; if (plaintext.startsWith(ENC_PREFIX)) return plaintext; const iv = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", getEncryptionKey(), iv); const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); const tag = cipher.getAuthTag(); return [ ENC_PREFIX, iv.toString("base64url"), ".", tag.toString("base64url"), ".", encrypted.toString("base64url"), ].join(""); } /** Decrypt a stored secret. Legacy plaintext values are returned as-is. */ export function decryptSecret(value: string): string { if (!value) return value; if (!value.startsWith(ENC_PREFIX)) return value; const payload = value.slice(ENC_PREFIX.length); const [ivB64, tagB64, dataB64] = payload.split("."); if (!ivB64 || !tagB64 || !dataB64) { throw new Error("Invalid encrypted secret format"); } const decipher = createDecipheriv( "aes-256-gcm", getEncryptionKey(), Buffer.from(ivB64, "base64url"), ); decipher.setAuthTag(Buffer.from(tagB64, "base64url")); const decrypted = Buffer.concat([ decipher.update(Buffer.from(dataB64, "base64url")), decipher.final(), ]); return decrypted.toString("utf8"); }