import crypto from "crypto" // AES-256-GCM encryption for secrets at rest (OAuth tokens). The key is derived // from ACCOUNTING_ENCRYPTION_KEY, falling back to BETTER_AUTH_SECRET, via SHA-256 // so no additional configuration is required. function getKey(): Buffer { const secret = process.env.ACCOUNTING_ENCRYPTION_KEY || process.env.BETTER_AUTH_SECRET if (!secret) throw new Error("No encryption key configured (BETTER_AUTH_SECRET)") return crypto.createHash("sha256").update(secret).digest() } /** Encrypt a UTF-8 string → "iv:tag:ciphertext" (all base64). */ export function encrypt(plaintext: string): string { const iv = crypto.randomBytes(12) const cipher = crypto.createCipheriv("aes-256-gcm", getKey(), iv) const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]) const tag = cipher.getAuthTag() return [iv.toString("base64"), tag.toString("base64"), enc.toString("base64")].join(":") } /** Decrypt a value produced by encrypt(). */ export function decrypt(payload: string): string { const [ivB64, tagB64, dataB64] = payload.split(":") const decipher = crypto.createDecipheriv("aes-256-gcm", getKey(), Buffer.from(ivB64, "base64")) decipher.setAuthTag(Buffer.from(tagB64, "base64")) return Buffer.concat([decipher.update(Buffer.from(dataB64, "base64")), decipher.final()]).toString("utf8") }