import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto"; import { readFileSync, writeFileSync, existsSync } from "node:fs"; // Standalone-mode crypto — see property-data's file-crypto.ts for the // shape and rationale. The two are duplicated rather than shared because // the MCPs are independent npm workspaces with no shared lib. const ALGORITHM = "aes-256-gcm"; const KEY_LENGTH = 32; const IV_LENGTH = 12; const AUTH_TAG_LENGTH = 16; function loadOrGenerateKey(keyFilePath: string, label: string): Buffer { if (existsSync(keyFilePath)) { const hex = readFileSync(keyFilePath, "utf-8").trim(); const buf = Buffer.from(hex, "hex"); if (buf.length !== KEY_LENGTH) { throw new Error( `[${label}] encryption key at ${keyFilePath} is ${buf.length} bytes, expected ${KEY_LENGTH}.`, ); } return buf; } const key = randomBytes(KEY_LENGTH); writeFileSync(keyFilePath, key.toString("hex"), { mode: 0o600 }); console.error(`[${label}] encryption key generated at ${keyFilePath}`); return key; } export function encryptWith(plaintext: string, keyFilePath: string, label: string): string { const key = loadOrGenerateKey(keyFilePath, label); const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }); let encrypted = cipher.update(plaintext, "utf-8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag().toString("hex"); return `${iv.toString("hex")}:${encrypted}:${authTag}`; } export function decryptWith(encryptedValue: string, keyFilePath: string, label: string): string { const parts = encryptedValue.split(":"); if (parts.length !== 3) { throw new Error(`[${label}] bad encrypted envelope (expected iv:ct:tag)`); } const key = loadOrGenerateKey(keyFilePath, label); const iv = Buffer.from(parts[0], "hex"); const ciphertext = parts[1]; const authTag = Buffer.from(parts[2], "hex"); const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }); decipher.setAuthTag(authTag); let plaintext = decipher.update(ciphertext, "hex", "utf-8"); plaintext += decipher.final("utf-8"); return plaintext; }