Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
PatternEncryptor.tsโ€ข11.3 kB
/** * Pattern Encryption Service for Memory Security * * Part of Issue #1321 Phase 2: Memory Security Architecture * * PURPOSE: * Encrypts dangerous patterns extracted from FLAGGED memories using AES-256-GCM. * Ensures patterns are never stored in plain text while remaining accessible * for authorized security research outside LLM contexts. * * ARCHITECTURE: * - Uses AES-256-GCM for authenticated encryption * - Derives keys from DOLLHOUSE_ENCRYPTION_SECRET via PBKDF2 * - Generates unique IV for each encryption operation * - Provides GCM authentication tags for integrity verification * * SECURITY: * - Patterns encrypted at rest * - Decryption only outside LLM context * - All decryption attempts logged * - Key rotation supported * * @module PatternEncryptor */ import { randomBytes, createCipheriv, createDecipheriv, pbkdf2 } from 'node:crypto'; import { logger } from '../../utils/logger.js'; /** * Encrypted pattern structure * Contains ciphertext, algorithm metadata, and authentication data */ export interface EncryptedPattern { /** Base64-encoded encrypted data */ encryptedData: string; /** Encryption algorithm (always 'aes-256-gcm') */ algorithm: 'aes-256-gcm'; /** Base64-encoded initialization vector */ iv: string; /** Base64-encoded GCM authentication tag for integrity */ authTag: string; } /** * Configuration for pattern encryption */ export interface EncryptionConfig { /** Enable encryption (default: true in production) */ enabled: boolean; /** Encryption secret (from environment) */ secret?: string; /** PBKDF2 iterations (default: 100000) */ iterations: number; /** Salt for key derivation (default: 'dollhouse-pattern-encryption-v1') */ salt: string; } /** * Default encryption configuration */ const DEFAULT_CONFIG: EncryptionConfig = { enabled: process.env.NODE_ENV === 'production', secret: process.env.DOLLHOUSE_ENCRYPTION_SECRET, iterations: 100000, // SECURITY FIX: Configurable salt to prevent rainbow table attacks // Falls back to default only if not configured salt: process.env.DOLLHOUSE_ENCRYPTION_SALT || 'dollhouse-pattern-encryption-v1', }; /** * PatternEncryptor service * * Handles encryption and decryption of dangerous patterns using AES-256-GCM. * Provides authenticated encryption with integrity protection via GCM mode. */ export class PatternEncryptor { private static readonly ALGORITHM = 'aes-256-gcm'; private static readonly KEY_LENGTH = 32; // 256 bits private static readonly IV_LENGTH = 16; // 128 bits private static readonly AUTH_TAG_LENGTH = 16; // 128 bits private static config: EncryptionConfig = DEFAULT_CONFIG; private static encryptionKey?: Buffer; private static isInitialized: boolean = false; /** * Initialize encryption with configuration * * @param config - Optional configuration overrides * @throws Error if encryption secret is not provided when enabled */ static async initialize(config?: Partial<EncryptionConfig>): Promise<void> { this.config = { ...DEFAULT_CONFIG, ...config }; if (!this.config.enabled) { logger.info('Pattern encryption disabled'); this.isInitialized = true; return; } if (!this.config.secret) { throw new Error( 'DOLLHOUSE_ENCRYPTION_SECRET environment variable is required when encryption is enabled' ); } logger.info('Initializing pattern encryption', { algorithm: this.ALGORITHM, iterations: this.config.iterations, }); // Derive encryption key from secret this.encryptionKey = await this.deriveKey( this.config.secret, this.config.salt, this.config.iterations ); this.isInitialized = true; logger.info('Pattern encryption initialized successfully'); } /** * Encrypt a pattern using AES-256-GCM * * @param pattern - Plain text pattern to encrypt * @returns Encrypted pattern with metadata * @throws Error if encryption not initialized or pattern is empty */ static encrypt(pattern: string): EncryptedPattern { if (!this.isInitialized) { throw new Error('PatternEncryptor not initialized. Call initialize() first.'); } if (!this.config.enabled) { // Encryption disabled - return mock encrypted structure // This allows testing without encryption enabled logger.debug('Encryption disabled, returning mock structure'); return { encryptedData: Buffer.from(pattern).toString('base64'), algorithm: this.ALGORITHM, iv: randomBytes(this.IV_LENGTH).toString('base64'), authTag: randomBytes(this.AUTH_TAG_LENGTH).toString('base64'), }; } if (!pattern || pattern.length === 0) { throw new Error('Cannot encrypt empty pattern'); } if (!this.encryptionKey) { throw new Error('Encryption key not available'); } logger.debug('Encrypting pattern', { patternLength: pattern.length, }); try { // Generate random IV for this encryption const iv = randomBytes(this.IV_LENGTH); // Create cipher with AES-256-GCM const cipher = createCipheriv(this.ALGORITHM, this.encryptionKey, iv); // Encrypt the pattern let encrypted = cipher.update(pattern, 'utf8', 'base64'); encrypted += cipher.final('base64'); // Get authentication tag from GCM mode const authTag = cipher.getAuthTag(); logger.debug('Pattern encrypted successfully', { ivLength: iv.length, authTagLength: authTag.length, encryptedLength: encrypted.length, }); return { encryptedData: encrypted, algorithm: this.ALGORITHM, iv: iv.toString('base64'), authTag: authTag.toString('base64'), }; } catch (error) { logger.error('Failed to encrypt pattern', { error }); throw new Error(`Pattern encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Decrypt an encrypted pattern * * @param encrypted - Encrypted pattern structure * @returns Decrypted plain text pattern * @throws Error if decryption fails or authentication fails */ static decrypt(encrypted: EncryptedPattern): string { if (!this.isInitialized) { throw new Error('PatternEncryptor not initialized. Call initialize() first.'); } if (!this.config.enabled) { // Encryption disabled - decode the base64 mock data logger.debug('Encryption disabled, decoding mock structure'); return Buffer.from(encrypted.encryptedData, 'base64').toString('utf8'); } if (!this.encryptionKey) { throw new Error('Encryption key not available'); } if (!encrypted.encryptedData || !encrypted.iv || !encrypted.authTag) { throw new Error('Invalid encrypted pattern: missing required fields'); } logger.debug('Decrypting pattern', { algorithm: encrypted.algorithm, }); try { // Decode base64 components const iv = Buffer.from(encrypted.iv, 'base64'); const authTag = Buffer.from(encrypted.authTag, 'base64'); // Create decipher with AES-256-GCM const decipher = createDecipheriv(this.ALGORITHM, this.encryptionKey, iv); // Set authentication tag for verification decipher.setAuthTag(authTag); // Decrypt the pattern let decrypted = decipher.update(encrypted.encryptedData, 'base64', 'utf8'); decrypted += decipher.final('utf8'); logger.debug('Pattern decrypted successfully', { decryptedLength: decrypted.length, }); return decrypted; } catch (error) { logger.error('Failed to decrypt pattern', { error }); // Authentication failure means data was tampered with // Check for various GCM authentication error messages if (error instanceof Error) { const errorMsg = error.message.toLowerCase(); if ( errorMsg.includes('auth') || errorMsg.includes('decrypt') || errorMsg.includes('unsupported state') || errorMsg.includes('unable to authenticate') ) { throw new Error('Pattern decryption failed: Authentication tag mismatch (data may be corrupted or tampered)'); } } throw new Error(`Pattern decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Derive encryption key from secret using PBKDF2 * * @param secret - Master secret from environment * @param salt - Salt for key derivation * @param iterations - Number of PBKDF2 iterations * @returns Derived encryption key */ private static async deriveKey( secret: string, salt: string, iterations: number ): Promise<Buffer> { return new Promise((resolve, reject) => { pbkdf2( secret, salt, iterations, this.KEY_LENGTH, 'sha256', (err, derivedKey) => { if (err) { logger.error('Key derivation failed', { error: err }); reject(new Error(`Failed to derive encryption key: ${err.message}`)); } else { logger.debug('Encryption key derived successfully', { keyLength: derivedKey.length, iterations, }); resolve(derivedKey); } } ); }); } /** * Check if encryption is enabled * * @returns true if encryption is enabled and initialized */ static isEnabled(): boolean { return this.config.enabled && this.isInitialized; } /** * Get encryption configuration status * * @returns Configuration status (without exposing secrets) */ static getStatus() { return { enabled: this.config.enabled, initialized: this.isInitialized, algorithm: this.ALGORITHM, keyLength: this.KEY_LENGTH, iterations: this.config.iterations, hasSecret: !!this.config.secret, }; } /** * Securely clear encryption key from memory * SECURITY FIX: Overwrites key buffer with zeros before releasing * * This prevents key recovery from memory dumps or process inspection. * Should be called when encryption is no longer needed. */ private static secureKeyClear(): void { if (this.encryptionKey) { // Overwrite key material with zeros this.encryptionKey.fill(0); this.encryptionKey = undefined; logger.debug('Encryption key securely cleared from memory'); } } /** * Reset the encryptor (useful for testing) * SECURITY FIX: Now performs secure key clearing * WARNING: This will securely clear the encryption key from memory */ static reset(): void { this.secureKeyClear(); this.isInitialized = false; this.config = DEFAULT_CONFIG; logger.debug('PatternEncryptor reset'); } /** * Securely reset the encryptor and clear all sensitive data * SECURITY: Explicitly clears encryption keys from memory * * Use this when: * - Shutting down the application * - Rotating encryption keys * - Responding to security incidents */ static secureReset(): void { this.secureKeyClear(); this.isInitialized = false; this.config = DEFAULT_CONFIG; logger.info('PatternEncryptor securely reset - all sensitive data cleared'); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server