UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

808 lines (699 loc) 22.1 kB
/** * Enterprise Secrets Management System * Implements encrypted configuration storage with key rotation and access control */ import crypto from 'crypto'; import fs from 'fs/promises'; import path from 'path'; import { logger } from '../logger.js'; export interface SecretConfig { name: string; value: string; description?: string; tags?: string[]; expiresAt?: Date; createdAt: Date; lastAccessed?: Date; accessCount: number; } export interface EncryptedSecret { name: string; encryptedData: string; encryptedValue: string; iv: string; salt: string; authTag: string; algorithm: string; keyDerivation: string; metadata: { description?: string; tags?: string[]; expiresAt?: string; createdAt: string; lastAccessed?: string; accessCount: number; }; } export interface KeyRotationConfig { enabled: boolean; intervalDays: number; retainOldKeys: number; autoRotate: boolean; } export interface SecretsManagerConfig { storePath: string; masterKeyPath: string; keyRotation: KeyRotationConfig; encryption: { algorithm: string; keyLength: number; ivLength: number; saltLength: number; iterations: number; }; access: { auditLog: boolean; maxAccessAttempts: number; requireAuthentication: boolean; }; } export class SecretsManager { private config: SecretsManagerConfig; private masterKey: Buffer | null = null; private secrets = new Map<string, SecretConfig>(); private accessLog: Array<{ secret: string; timestamp: Date; user?: string; success: boolean }> = []; private keyRotationTimer?: NodeJS.Timeout; constructor(config: Partial<SecretsManagerConfig> = {}) { this.config = { storePath: process.env.SECRETS_STORE_PATH || './secrets', masterKeyPath: process.env.MASTER_KEY_PATH || './master.key', keyRotation: { enabled: true, intervalDays: 90, retainOldKeys: 3, autoRotate: false, }, encryption: { algorithm: 'aes-256-gcm', keyLength: 32, ivLength: 16, saltLength: 32, iterations: 100000, }, access: { auditLog: true, maxAccessAttempts: 5, requireAuthentication: true, }, ...config, }; } /** * Initialize secrets manager */ async initialize(masterPassword?: string): Promise<void> { try { // Ensure secrets directory exists await fs.mkdir(this.config.storePath, { recursive: true }); // Load or generate master key await this.loadOrGenerateMasterKey(masterPassword); // Load existing secrets await this.loadSecrets(); // Start key rotation timer if enabled if (this.config.keyRotation.enabled && this.config.keyRotation.autoRotate) { this.startKeyRotationTimer(); } logger.info('Secrets manager initialized', { storePath: this.config.storePath, secretsCount: this.secrets.size, }); } catch (error) { logger.error('Failed to initialize secrets manager', error as Error); throw error; } } /** * Store a secret securely */ async storeSecret( name: string, value: string, options: { description?: string; tags?: string[]; expiresAt?: Date; } = {} ): Promise<void> { try { if (!this.masterKey) { throw new Error('Secrets manager not initialized'); } // Validate secret name this.validateSecretName(name); // Create secret config const secret: SecretConfig = { name, value, description: options.description, tags: options.tags || [], expiresAt: options.expiresAt, createdAt: new Date(), accessCount: 0, }; // Encrypt and store const encrypted = await this.encryptSecretInternal(secret); await this.saveEncryptedSecret(encrypted); // Update in-memory cache this.secrets.set(name, secret); logger.info('Secret stored', { name, hasExpiration: !!options.expiresAt, tags: options.tags, }); } catch (error) { logger.error('Failed to store secret', error as Error, { name }); throw error; } } /** * Encrypt a secret and return encrypted data (for testing purposes) */ async encryptSecret(name: string, value: string): Promise<EncryptedSecret> { try { if (!this.masterKey) { await this.initialize(); } // Validate secret name this.validateSecretName(name); // Create secret config const secret: SecretConfig = { name, value, createdAt: new Date(), accessCount: 0, }; // Store in memory for decryption this.secrets.set(name, secret); // Encrypt and return the encrypted secret return await this.encryptSecretInternal(secret); } catch (error) { logger.error('Failed to encrypt secret', error as Error, { name }); throw error; } } /** * Decrypt a secret (for testing purposes) */ async decryptSecret(name: string): Promise<string> { const secret = await this.getSecret(name); if (!secret) { throw new Error('Secret not found'); } return secret; } /** * Retrieve a secret */ async getSecret(name: string, userId?: string): Promise<string | null> { try { if (!this.masterKey) { throw new Error('Secrets manager not initialized'); } // Check if secret exists const secret = this.secrets.get(name); if (!secret) { this.logAccess(name, false, userId); return null; } // Check expiration if (secret.expiresAt && secret.expiresAt < new Date()) { logger.warn('Attempted access to expired secret', { name, expiresAt: secret.expiresAt }); this.logAccess(name, false, userId, 'expired'); return null; } // Update access tracking secret.lastAccessed = new Date(); secret.accessCount++; // Log access this.logAccess(name, true, userId); logger.debug('Secret accessed', { name, accessCount: secret.accessCount, userId, }); return secret.value; } catch (error) { logger.error('Failed to retrieve secret', error as Error, { name, userId }); this.logAccess(name, false, userId, 'error'); throw error; } } /** * Update a secret */ async updateSecret( name: string, newValue: string, options: { description?: string; tags?: string[]; expiresAt?: Date; } = {} ): Promise<void> { try { const existingSecret = this.secrets.get(name); if (!existingSecret) { throw new Error(`Secret '${name}' not found`); } // Create updated secret const updatedSecret: SecretConfig = { ...existingSecret, value: newValue, description: options.description ?? existingSecret.description, tags: options.tags ?? existingSecret.tags, expiresAt: options.expiresAt ?? existingSecret.expiresAt, }; // Encrypt and store const encrypted = await this.encryptSecretInternal(updatedSecret); await this.saveEncryptedSecret(encrypted); // Update in-memory cache this.secrets.set(name, updatedSecret); logger.info('Secret updated', { name }); } catch (error) { logger.error('Failed to update secret', error as Error, { name }); throw error; } } /** * Delete a secret */ async deleteSecret(name: string): Promise<boolean> { try { if (!this.secrets.has(name)) { return false; } // Remove from filesystem const filePath = path.join(this.config.storePath, `${name}.json`); await fs.unlink(filePath); // Remove from memory this.secrets.delete(name); logger.info('Secret deleted', { name }); return true; } catch (error) { logger.error('Failed to delete secret', error as Error, { name }); throw error; } } /** * List all secret names (not values) */ async listSecrets(tags?: string[]): Promise< Array<{ name: string; description?: string; tags?: string[]; expiresAt?: Date; createdAt: Date; lastAccessed?: Date; accessCount: number; }> > { try { let secrets = Array.from(this.secrets.values()); // Filter by tags if specified if (tags && tags.length > 0) { secrets = secrets.filter(secret => secret.tags?.some(tag => tags.includes(tag))); } // Return metadata only (no values) return secrets.map(secret => ({ name: secret.name, description: secret.description, tags: secret.tags, expiresAt: secret.expiresAt, createdAt: secret.createdAt, lastAccessed: secret.lastAccessed, accessCount: secret.accessCount, })); } catch (error) { logger.error('Failed to list secrets', error as Error); throw error; } } /** * Rotate master key */ async rotateMasterKey(newPassword?: string): Promise<void> { try { logger.info('Starting master key rotation'); // Backup current secrets const secretsBackup = new Map(this.secrets); // Generate new master key const oldMasterKey = this.masterKey; await this.generateMasterKey(newPassword); // Re-encrypt all secrets with new key for (const [name, secret] of secretsBackup.entries()) { const encrypted = await this.encryptSecretInternal(secret); await this.saveEncryptedSecret(encrypted); } // Archive old master key if configured if (this.config.keyRotation.retainOldKeys > 0) { await this.archiveMasterKey(oldMasterKey!); } logger.info('Master key rotation completed', { secretsReencrypted: secretsBackup.size, }); } catch (error) { logger.error('Master key rotation failed', error as Error); throw error; } } /** * Export secrets (encrypted) for backup */ async exportSecrets(): Promise<string> { try { const exportData = { version: '1.0', timestamp: new Date().toISOString(), secrets: await this.getAllEncryptedSecrets(), }; return JSON.stringify(exportData, null, 2); } catch (error) { logger.error('Failed to export secrets', error as Error); throw error; } } /** * Import secrets from backup */ async importSecrets(exportData: string): Promise<void> { try { const data = JSON.parse(exportData); if (data.version !== '1.0') { throw new Error('Unsupported export format version'); } let importCount = 0; for (const encrypted of data.secrets) { await this.saveEncryptedSecret(encrypted); // Decrypt and add to memory cache const secret = await this.decryptSecretInternal(encrypted); this.secrets.set(secret.name, secret); importCount++; } logger.info('Secrets imported successfully', { importCount, totalSecrets: this.secrets.size, }); } catch (error) { logger.error('Failed to import secrets', error as Error); throw error; } } /** * Get access audit log */ getAccessLog(hours: number = 24): Array<{ secret: string; timestamp: Date; user?: string; success: boolean; reason?: string; }> { const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000); return this.accessLog.filter(entry => entry.timestamp >= cutoff); } /** * Encrypt a secret (internal method) */ private async encryptSecretInternal(secret: SecretConfig): Promise<EncryptedSecret> { const salt = crypto.randomBytes(this.config.encryption.saltLength); const iv = crypto.randomBytes(this.config.encryption.ivLength); // Derive encryption key from master key and salt const key = crypto.pbkdf2Sync( this.masterKey!, salt, this.config.encryption.iterations, this.config.encryption.keyLength, 'sha256' ); // Encrypt the secret value using GCM for authenticated encryption const cipher = crypto.createCipheriv(this.config.encryption.algorithm, key, iv); let encrypted = cipher.update(secret.value, 'utf8', 'base64'); encrypted += cipher.final('base64'); const authTag = (cipher as any).getAuthTag(); return { name: secret.name, encryptedData: encrypted, encryptedValue: encrypted, iv: iv.toString('base64'), salt: salt.toString('base64'), authTag: authTag.toString('base64'), algorithm: this.config.encryption.algorithm, keyDerivation: 'pbkdf2', metadata: { description: secret.description, tags: secret.tags, expiresAt: secret.expiresAt?.toISOString(), createdAt: secret.createdAt.toISOString(), lastAccessed: secret.lastAccessed?.toISOString(), accessCount: secret.accessCount, }, }; } /** * Decrypt a secret (internal method) */ private async decryptSecretInternal(encrypted: EncryptedSecret): Promise<SecretConfig> { const salt = Buffer.from(encrypted.salt, 'base64'); const iv = Buffer.from(encrypted.iv, 'base64'); const authTag = Buffer.from(encrypted.authTag, 'base64'); // Derive decryption key const key = crypto.pbkdf2Sync( this.masterKey!, salt, this.config.encryption.iterations, this.config.encryption.keyLength, 'sha256' ); // Decrypt the value using GCM for authenticated decryption const decipher = crypto.createDecipheriv(encrypted.algorithm, key, iv); (decipher as any).setAuthTag(authTag); let decrypted = decipher.update(encrypted.encryptedValue, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return { name: encrypted.name, value: decrypted, description: encrypted.metadata.description, tags: encrypted.metadata.tags || [], expiresAt: encrypted.metadata.expiresAt ? new Date(encrypted.metadata.expiresAt) : undefined, createdAt: new Date(encrypted.metadata.createdAt), lastAccessed: encrypted.metadata.lastAccessed ? new Date(encrypted.metadata.lastAccessed) : undefined, accessCount: encrypted.metadata.accessCount, }; } /** * Load or generate master key */ private async loadOrGenerateMasterKey(password?: string): Promise<void> { try { // Try to load existing master key const keyExists = await fs .access(this.config.masterKeyPath) .then(() => true) .catch(() => false); if (keyExists) { await this.loadMasterKey(password); } else { await this.generateMasterKey(password); } } catch (error) { logger.error('Failed to load or generate master key', error as Error); throw error; } } /** * Load master key from file */ private async loadMasterKey(password?: string): Promise<void> { const keyData = await fs.readFile(this.config.masterKeyPath, 'utf8'); if (password) { // Decrypt master key with password const [encryptedKey, salt, iv] = keyData.split(':'); const derivedKey = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); const decipher = crypto.createDecipheriv('aes-256-cbc', derivedKey, Buffer.from(iv, 'hex')); let decrypted = decipher.update(encryptedKey, 'hex', 'utf8'); decrypted += decipher.final('utf8'); this.masterKey = Buffer.from(decrypted, 'hex'); } else { // Use key directly (for development only) this.masterKey = Buffer.from(keyData, 'hex'); } } /** * Generate new master key */ private async generateMasterKey(password?: string): Promise<void> { this.masterKey = crypto.randomBytes(32); let keyData: string; if (password) { // Encrypt master key with password const salt = crypto.randomBytes(16).toString('hex'); const iv = crypto.randomBytes(16); const derivedKey = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); const cipher = crypto.createCipheriv('aes-256-cbc', derivedKey, iv); let encrypted = cipher.update(this.masterKey.toString('hex'), 'utf8', 'hex'); encrypted += cipher.final('hex'); keyData = `${encrypted}:${salt}:${iv.toString('hex')}`; } else { // Store key directly (for development only) keyData = this.masterKey.toString('hex'); } await fs.writeFile(this.config.masterKeyPath, keyData, { mode: 0o600 }); logger.info('New master key generated', { keyPath: this.config.masterKeyPath, encrypted: !!password, }); } /** * Load secrets from storage */ private async loadSecrets(): Promise<void> { try { const files = await fs.readdir(this.config.storePath); const secretFiles = files.filter(file => file.endsWith('.json')); for (const file of secretFiles) { try { const filePath = path.join(this.config.storePath, file); const encryptedData = await fs.readFile(filePath, 'utf8'); const encrypted: EncryptedSecret = JSON.parse(encryptedData); const secret = await this.decryptSecretInternal(encrypted); this.secrets.set(secret.name, secret); } catch (error) { logger.error('Failed to load secret file', error as Error, { file }); } } logger.info('Secrets loaded from storage', { count: this.secrets.size, }); } catch (error) { logger.error('Failed to load secrets', error as Error); throw error; } } /** * Save encrypted secret to file */ private async saveEncryptedSecret(encrypted: EncryptedSecret): Promise<void> { const filePath = path.join(this.config.storePath, `${encrypted.name}.json`); await fs.writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 }); } /** * Get all encrypted secrets */ private async getAllEncryptedSecrets(): Promise<EncryptedSecret[]> { const encryptedSecrets: EncryptedSecret[] = []; for (const secret of this.secrets.values()) { const encrypted = await this.encryptSecretInternal(secret); encryptedSecrets.push(encrypted); } return encryptedSecrets; } /** * Validate secret name */ private validateSecretName(name: string): void { if (!name || typeof name !== 'string') { throw new Error('Secret name must be a non-empty string'); } if (!/^[a-zA-Z0-9_-]+$/.test(name)) { throw new Error( 'Secret name can only contain alphanumeric characters, underscores, and hyphens' ); } if (name.length > 100) { throw new Error('Secret name cannot exceed 100 characters'); } } /** * Log secret access */ private logAccess(secret: string, success: boolean, userId?: string, reason?: string): void { if (this.config.access.auditLog) { this.accessLog.push({ secret, timestamp: new Date(), user: userId, success, ...(reason && { reason }), }); // Trim access log if too large if (this.accessLog.length > 10000) { this.accessLog.splice(0, 1000); } } } /** * Archive old master key */ private async archiveMasterKey(oldKey: Buffer): Promise<void> { const archivePath = path.join(this.config.storePath, 'archived-keys'); await fs.mkdir(archivePath, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const archiveFile = path.join(archivePath, `master-key-${timestamp}.bak`); await fs.writeFile(archiveFile, oldKey.toString('hex'), { mode: 0o600 }); logger.info('Old master key archived', { archiveFile }); } /** * Start key rotation timer */ private startKeyRotationTimer(): void { const intervalMs = this.config.keyRotation.intervalDays * 24 * 60 * 60 * 1000; this.keyRotationTimer = setInterval(async () => { // TODO: Store interval ID and call clearInterval in cleanup try { logger.info('Starting automatic key rotation'); await this.rotateMasterKey(); } catch (error) { logger.error('Automatic key rotation failed', error as Error); } }, intervalMs); } /** * Stop and cleanup */ async stop(): Promise<void> { if (this.keyRotationTimer) { clearInterval(this.keyRotationTimer); } // Clear sensitive data from memory this.masterKey?.fill(0); this.masterKey = null; this.secrets.clear(); logger.info('Secrets manager stopped'); } // Test helper methods async deleteTestSecret(name: string): Promise<boolean> { const exists = this.secrets.has(name); if (exists) { this.secrets.delete(name); // Also try to delete from storage if it exists try { const secretPath = path.join(this.config.storePath, `${name}.json`); await fs.unlink(secretPath); } catch { // Ignore if file doesn't exist } } return exists; } async rotateEncryptionKey(): Promise<void> { // Generate new master key const newMasterKey = crypto.randomBytes(32); const oldMasterKey = this.masterKey; // Set new master key this.masterKey = newMasterKey; // Re-encrypt all secrets with new key for (const [name, secret] of this.secrets.entries()) { const encrypted = await this.encryptSecretInternal(secret); await this.saveEncryptedSecret(encrypted); } // Clear old key from memory if (oldMasterKey) { oldMasterKey.fill(0); } logger.info('Encryption key rotated successfully'); } setEncryptionKey(key: Buffer): void { if (key.length < 32) { throw new Error('Encryption key must be at least 32 bytes'); } this.masterKey = key; } get secretStorage() { // For testing access to internal storage return this.secrets; } }