import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'; /** * Envelope for a tenant secret sealed with AES-256-GCM. Stored as base64 columns on * IiosProviderCredential; the plaintext (a provider config JSON string) never touches disk. */ export interface SealedSecret { cipherText: string; iv: string; authTag: string; keyVersion: number; } /** Bumped only when the platform master key rotates; lets old rows decrypt under an old key. */ export const SECRET_KEY_VERSION = 1; /** * Resolve the 32-byte platform master key from `IIOS_CRED_KEY` (base64). Returns null when * unset (so egress providers that need it stay unregistered / fail closed) but THROWS on a * present-but-malformed key — a wrong-length key is an operator error, not a "disabled" state. */ export function credKeyFromEnv(env: NodeJS.ProcessEnv = process.env): Buffer | null { const raw = env.IIOS_CRED_KEY; if (!raw) return null; const key = Buffer.from(raw, 'base64'); if (key.length !== 32) throw new Error('IIOS_CRED_KEY must decode to 32 bytes (base64-encoded 256-bit key)'); return key; } export function sealSecret(plaintext: string, key: Buffer): SealedSecret { const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', key, iv); const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); return { cipherText: enc.toString('base64'), iv: iv.toString('base64'), authTag: cipher.getAuthTag().toString('base64'), keyVersion: SECRET_KEY_VERSION, }; } export function openSecret(sealed: SealedSecret, key: Buffer): string { const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(sealed.iv, 'base64')); decipher.setAuthTag(Buffer.from(sealed.authTag, 'base64')); return Buffer.concat([decipher.update(Buffer.from(sealed.cipherText, 'base64')), decipher.final()]).toString('utf8'); }