import { randomBytes, createCipheriv, createDecipheriv, timingSafeEqual } from 'node:crypto'; import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs'; import { dirname } from 'path'; const ALGO = 'aes-256-gcm'; const IV_LEN = 12; const TAG_LEN = 16; const KEY_HEX_LEN = 64; // 32 bytes const ENV_VAR = 'MCP_ENCRYPTION_KEY'; /** * Read or auto-generate the MCP encryption key at `path`. * If the file exists: reads it (must be 32 bytes) and sets MCP_ENCRYPTION_KEY env var. * If the file does not exist: generates 32 random bytes, writes to file (mode 0600), * then sets the env var. Uses the same pattern as initMasterKey in src/crypto/sessions.ts. */ export function initMcpKeyFromFile(path: string): void { if (existsSync(path)) { const buf = readFileSync(path); if (buf.length !== 32) { throw new Error(`MCP key at ${path} is not 32 bytes (got ${buf.length})`); } process.env[ENV_VAR] = buf.toString('hex'); return; } mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); const key = randomBytes(32); writeFileSync(path, key, { mode: 0o600 }); chmodSync(path, 0o600); process.env[ENV_VAR] = key.toString('hex'); } export function isKeyConfigured(): boolean { const raw = process.env[ENV_VAR]; return typeof raw === 'string' && /^[0-9a-fA-F]{64}$/.test(raw); } export function loadKeyFromEnv(): Buffer { const raw = process.env[ENV_VAR]; if (!raw || !/^[0-9a-fA-F]{64}$/.test(raw)) { throw new Error( `${ENV_VAR} must be a 64-character hex string (32 bytes). MCP client features are disabled.`, ); } return Buffer.from(raw, 'hex'); } // Layout: [IV (12)] [TAG (16)] [CIPHERTEXT (n)] export function encrypt(plaintext: string, key: Buffer): Buffer { const iv = randomBytes(IV_LEN); const cipher = createCipheriv(ALGO, key, iv); const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return Buffer.concat([iv, tag, ct]); } export function decrypt(blob: Buffer, key: Buffer): string { if (blob.length < IV_LEN + TAG_LEN) throw new Error('ciphertext too short'); const iv = blob.subarray(0, IV_LEN); const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN); const ct = blob.subarray(IV_LEN + TAG_LEN); const decipher = createDecipheriv(ALGO, key, iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8'); } export function safeEqual(a: Buffer, b: Buffer): boolean { if (a.length !== b.length) return false; return timingSafeEqual(a, b); }