feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user