import { createCipheriv, createDecipheriv, createHash, randomBytes, scrypt, } from 'crypto'; const ALGORITHM = 'aes-256-gcm'; const KEY_LEN = 32; const IV_LEN = 16; const SALT_LEN = 16; const TAG_LEN = 16; function getMasterPassphrase(): string { const master = process.env.MASTER_ENCRYPTION_KEY; if (!master || master.length < 32) { throw new Error('MASTER_ENCRYPTION_KEY must be set (minimum 32 characters)'); } return master; } async function deriveKey(salt: Buffer): Promise { return new Promise((resolve, reject) => { scrypt(getMasterPassphrase(), salt, KEY_LEN, (err, key) => { if (err) reject(err); else resolve(key); }); }); } export async function encryptApiKey(plaintext: string): Promise { const salt = randomBytes(SALT_LEN); const iv = randomBytes(IV_LEN); const key = await deriveKey(salt); const cipher = createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([ cipher.update(plaintext, 'utf8'), cipher.final(), ]); const tag = cipher.getAuthTag(); return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); } export async function decryptApiKey(payload: string): Promise { const buf = Buffer.from(payload, 'base64'); if (buf.length < SALT_LEN + IV_LEN + TAG_LEN + 1) { throw new Error('Invalid encrypted key payload'); } const salt = buf.subarray(0, SALT_LEN); const iv = buf.subarray(SALT_LEN, SALT_LEN + IV_LEN); const tag = buf.subarray(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN); const ciphertext = buf.subarray(SALT_LEN + IV_LEN + TAG_LEN); const key = await deriveKey(salt); const decipher = createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(tag); return Buffer.concat([ decipher.update(ciphertext), decipher.final(), ]).toString('utf8'); } export function hashApiKey(plaintext: string): string { return createHash('sha256').update(plaintext, 'utf8').digest('hex'); }