UNPKG

@escher-dbai/rag-module

Version:

Enterprise RAG module with chat context storage, vector search, and session management. Complete chat history retrieval and streaming content extraction for Electron apps.

407 lines (341 loc) 12.3 kB
const crypto = require('crypto'); const keytar = require('keytar'); /** * EncryptionService - AES-256-GCM encryption with OS Keychain integration * Per business architecture specifications */ class EncryptionService { constructor() { this.algorithm = 'aes-256-gcm'; this.keyLength = 32; // 256 bits this.ivLength = 16; // 128 bits this.tagLength = 16; // 128 bits this.serviceName = 'cloudops-storage-service'; this.keyName = 'encryption-key'; this.encryptionKey = null; this.initialized = false; } /** * Initialize encryption service - get or create key from OS Keychain */ async initialize() { try { this.encryptionKey = await this.getOrCreateKey(); this.initialized = true; console.log('✅ EncryptionService initialized with OS Keychain integration'); return true; } catch (error) { console.error('❌ Failed to initialize EncryptionService:', error.message); throw new Error(`Failed to initialize encryption service: ${error.message}`); } } /** * Get existing key from OS Keychain or create new one * @returns {Buffer} 32-byte encryption key */ async getOrCreateKey() { try { // Try to get existing key from OS Keychain const existingKey = await keytar.getPassword(this.serviceName, this.keyName); if (existingKey) { // Convert base64 back to buffer const keyBuffer = Buffer.from(existingKey, 'base64'); if (keyBuffer.length === this.keyLength) { console.log('🔑 Retrieved existing encryption key from OS Keychain'); return keyBuffer; } } // Create new key if none exists or invalid const newKey = crypto.randomBytes(this.keyLength); await keytar.setPassword(this.serviceName, this.keyName, newKey.toString('base64')); console.log('🔑 Created new encryption key and stored in OS Keychain'); return newKey; } catch (error) { console.error('❌ Keychain access failed:', error.message); // Fallback: Generate session key (not persisted - for development) console.warn('⚠️ Using session-only encryption key (not persisted)'); return crypto.randomBytes(this.keyLength); } } /** * Encrypt data using AES-256-GCM (Business Architecture API) * @param {string} plaintext - Data to encrypt * @returns {string} Base64 encoded encrypted data (IV + encrypted + tag) */ encrypt(plaintext) { if (!this.encryptionKey) { throw new Error('EncryptionService not initialized'); } try { // Generate random IV for each encryption const iv = crypto.randomBytes(this.ivLength); // Create cipher using modern API const cipher = crypto.createCipheriv(this.algorithm, this.encryptionKey, iv); // Encrypt the data let encrypted = cipher.update(plaintext, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); // Get authentication tag const tag = cipher.getAuthTag(); // Combine IV + encrypted data + tag const combined = Buffer.concat([iv, encrypted, tag]); // Return as base64 return combined.toString('base64'); } catch (error) { console.error('❌ Encryption failed:', error.message); throw new Error(`Encryption failed: ${error.message}`); } } /** * Decrypt data using AES-256-GCM (Business Architecture API) * @param {string} encryptedData - Base64 encoded encrypted data * @returns {string} Decrypted plaintext */ decrypt(encryptedData) { if (!this.encryptionKey) { throw new Error('EncryptionService not initialized'); } try { // Decode from base64 const combined = Buffer.from(encryptedData, 'base64'); if (combined.length < this.ivLength + this.tagLength) { throw new Error('Invalid encrypted data format'); } // Extract components const iv = combined.subarray(0, this.ivLength); const tag = combined.subarray(-this.tagLength); const encrypted = combined.subarray(this.ivLength, -this.tagLength); // Create decipher using modern API const decipher = crypto.createDecipheriv(this.algorithm, this.encryptionKey, iv); decipher.setAuthTag(tag); // Decrypt the data let decrypted = decipher.update(encrypted, null, 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { console.error('❌ Decryption failed:', error.message); throw new Error(`Decryption failed: ${error.message}`); } } /** * Legacy method for backward compatibility */ async encryptContent(data) { const plaintext = Buffer.isBuffer(data) ? data.toString('utf8') : String(data); return this.encrypt(plaintext); } /** * Legacy method for backward compatibility */ async decryptContent(encryptedData) { return this.decrypt(encryptedData); } /** * Encrypt embedding vectors * @param {number[]} embedding - Embedding vector array * @returns {Promise<string>} - Base64 encoded encrypted embedding */ async encryptEmbedding(embedding) { this._ensureInitialized(); // Convert embedding array to JSON then to buffer const embeddingJson = JSON.stringify({ data: embedding, dimension: embedding.length, type: 'float32', algorithm: this.algorithm, timestamp: new Date().toISOString() }); const plaintext = Buffer.from(embeddingJson, 'utf8'); return this._encrypt(plaintext, this.embeddingKey); } /** * Decrypt embedding vectors * @param {string} encryptedEmbedding - Base64 encoded encrypted embedding * @returns {Promise<number[]>} - Decrypted embedding vector array */ async decryptEmbedding(encryptedEmbedding) { this._ensureInitialized(); const decrypted = this._decrypt(encryptedEmbedding, this.embeddingKey); const embeddingData = JSON.parse(decrypted.toString('utf8')); return embeddingData.data; } /** * Encrypt data for sync (backup) operations * @param {string|Buffer|Object} data - Data to encrypt * @returns {Promise<string>} - Base64 encoded encrypted data */ async encryptForSync(data) { this._ensureInitialized(); let plaintext; if (typeof data === 'object' && !Buffer.isBuffer(data)) { plaintext = Buffer.from(JSON.stringify(data), 'utf8'); } else if (Buffer.isBuffer(data)) { plaintext = data; } else { plaintext = Buffer.from(String(data), 'utf8'); } return this._encrypt(plaintext, this.syncKey); } /** * Decrypt data from sync operations * @param {string} encryptedData - Base64 encoded encrypted data * @returns {Promise<string>} - Decrypted data */ async decryptFromSync(encryptedData) { this._ensureInitialized(); const decrypted = this._decrypt(encryptedData, this.syncKey); return decrypted.toString('utf8'); } /** * Generate a new encryption key * @returns {Buffer} - 256-bit encryption key */ generateKey() { return crypto.randomBytes(this.keyLength); } /** * Derive key from password using PBKDF2 * @param {string} password - Password string * @param {string} [salt] - Salt (generates random if not provided) * @returns {Promise<{key: Buffer, salt: string}>} */ async deriveKeyFromPassword(password, salt) { const saltBuffer = salt ? Buffer.from(salt, 'hex') : crypto.randomBytes(16); return new Promise((resolve, reject) => { crypto.pbkdf2(password, saltBuffer, 100000, this.keyLength, 'sha256', (err, derivedKey) => { if (err) reject(err); else resolve({ key: derivedKey, salt: saltBuffer.toString('hex') }); }); }); } /** * Hash data using SHA-256 * @param {string|Buffer} data - Data to hash * @returns {string} - Hex encoded hash */ hash(data) { const hash = crypto.createHash('sha256'); hash.update(Buffer.isBuffer(data) ? data : Buffer.from(String(data), 'utf8')); return hash.digest('hex'); } /** * Generate secure random ID * @param {number} [length=32] - ID length * @returns {string} - Hex encoded random ID */ generateSecureId(length = 32) { return crypto.randomBytes(length).toString('hex'); } /** * Encrypt and encode file * @param {string} filePath - Path to file * @returns {Promise<string>} - Base64 encoded encrypted file */ async encryptFile(filePath) { this._ensureInitialized(); const fileData = await fs.readFile(filePath); return this.encryptForSync(fileData); } /** * Decrypt and save file * @param {string} encryptedData - Base64 encoded encrypted data * @param {string} outputPath - Path to save decrypted file * @returns {Promise<boolean>} */ async decryptFile(encryptedData, outputPath) { this._ensureInitialized(); const decryptedData = await this.decryptFromSync(encryptedData); await fs.writeFile(outputPath, decryptedData); return true; } // ============ PRIVATE METHODS ============ /** * Core encryption function using AES-256-GCM * @param {Buffer} plaintext - Data to encrypt * @param {Buffer} key - Encryption key * @returns {string} - Base64 encoded encrypted data */ _encrypt(plaintext, key) { // Generate random IV const iv = crypto.randomBytes(this.ivLength); // Create cipher with modern API (Node.js v22 compatible) const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); // Encrypt data let encrypted = cipher.update(plaintext); cipher.final(); // Get authentication tag const tag = cipher.getAuthTag(); // Combine IV + encrypted data + tag const combined = Buffer.concat([iv, encrypted, tag]); return combined.toString(this.encoding); } /** * Core decryption function using AES-256-GCM * @param {string} encryptedData - Base64 encoded encrypted data * @param {Buffer} key - Encryption key * @returns {Buffer} - Decrypted data */ _decrypt(encryptedData, key) { // Decode from base64 const combined = Buffer.from(encryptedData, this.encoding); // Extract components const iv = combined.subarray(0, this.ivLength); const tag = combined.subarray(-this.tagLength); const encrypted = combined.subarray(this.ivLength, -this.tagLength); // Create decipher with modern API (Node.js v22 compatible) const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(tag); // Decrypt data let decrypted = decipher.update(encrypted); decipher.final(); return decrypted; } /** * Load existing key or generate new one * @param {string} keyFileName - Key file name * @returns {Promise<Buffer>} - Encryption key */ async _loadOrGenerateKey(keyFileName) { const keyPath = path.join(this.basePath, 'keys', keyFileName); try { // Try to load existing key if (await fs.pathExists(keyPath)) { const keyData = await fs.readFile(keyPath, 'utf8'); return Buffer.from(keyData.trim(), this.encoding); } // Generate new key const newKey = this.generateKey(); await fs.writeFile(keyPath, newKey.toString(this.encoding), { mode: 0o600 }); return newKey; } catch (error) { throw new Error(`Failed to load or generate key ${keyFileName}: ${error.message}`); } } /** * Check if encryption is properly initialized * @returns {boolean} */ isInitialized() { return this.encryptionKey !== null && this.initialized; } /** * Get encryption status for health checks * @returns {Object} Encryption status */ getStatus() { return { initialized: this.isInitialized(), algorithm: this.algorithm, keyLength: this.keyLength, keychainService: this.serviceName }; } _ensureInitialized() { if (!this.initialized) { throw new Error('Encryption service must be initialized before use'); } } } module.exports = EncryptionService;