import crypto from "node:crypto"; const ALGORITHM = "aes-256-gcm"; const IV_LENGTH = 16; const SALT_LENGTH = 16; const KEY_LENGTH = 32; const ITERATIONS = 100_000; function getKey(secret: Buffer, salt: Buffer): Buffer { return crypto.pbkdf2Sync(secret, salt, ITERATIONS, KEY_LENGTH, "sha256"); } function getEncryptionKey(): Buffer { const key = process.env.API_KEY_ENCRYPTION_KEY; if (!key || key.length < 32) { throw new Error( "API_KEY_ENCRYPTION_KEY environment variable must be set (minimum 32 characters)", ); } return Buffer.from(key, "utf8"); } export function encryptApiKey(plaintext: string): string { const secret = getEncryptionKey(); const salt = crypto.randomBytes(SALT_LENGTH); const iv = crypto.randomBytes(IV_LENGTH); const key = getKey(secret, salt); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); const authTag = cipher.getAuthTag(); // Format: salt:iv:authTag:ciphertext (all base64) return [ salt.toString("base64"), iv.toString("base64"), authTag.toString("base64"), encrypted.toString("base64"), ].join(":"); } export function decryptApiKey(encrypted: string): string { const secret = getEncryptionKey(); const parts = encrypted.split(":"); if (parts.length !== 4) { throw new Error("Invalid encrypted API key format"); } const salt = Buffer.from(parts[0]!, "base64"); const iv = Buffer.from(parts[1]!, "base64"); const authTag = Buffer.from(parts[2]!, "base64"); const ciphertext = Buffer.from(parts[3]!, "base64"); const key = getKey(secret, salt); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8"); }