@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
JavaScript
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;