/** * Encryption utilities for VC-SYS CLI - TypeScript port from frontend * Uses AES-256-GCM for secure token storage */ import crypto from 'crypto'; import fs from 'fs-extra'; import path from 'path'; import os from 'os'; import { Logger } from '../core/logger'; // Direct port from frontend encryption.ts const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; export class EncryptionManager { private encryptionKey: string; private logger: Logger; constructor() { this.logger = new Logger('EncryptionManager'); this.encryptionKey = this.getOrCreateEncryptionKey(); } /** * Get or create CLI-specific encryption key */ private getOrCreateEncryptionKey(): string { const keyPath = path.join(os.homedir(), '.vcsys', 'encryption.key'); try { if (fs.existsSync(keyPath)) { const key = fs.readFileSync(keyPath, 'utf8'); if (this.validateKeyFormat(key)) { return key; } this.logger.warn('Invalid encryption key format, generating new key'); } } catch (error) { this.logger.warn('Failed to read encryption key, generating new one', error); } // Generate new encryption key const newKey = crypto.randomBytes(32).toString('base64'); // Ensure directory exists fs.ensureDirSync(path.dirname(keyPath)); // Write key to file (readable only by user) fs.writeFileSync(keyPath, newKey, { mode: 0o600 }); this.logger.info('New encryption key generated', { keyPath }); return newKey; } /** * Encrypts a plain text string - EXACT PORT from frontend * @param text The plain text to encrypt * @returns The encrypted string, including IV and auth tag */ encrypt(text: string): string { if (!text || typeof text !== 'string') { throw new Error('Text must be a non-empty string'); } const iv = crypto.randomBytes(IV_LENGTH); const key = Buffer.from(this.encryptionKey, 'base64'); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); cipher.setAAD(Buffer.from('vcsys-cli', 'utf8')); // Additional authenticated data let encrypted = cipher.update(text, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); const authTag = cipher.getAuthTag(); // Combine IV, auth tag, and encrypted data into a single string return Buffer.concat([iv, authTag, encrypted]).toString('hex'); } /** * Decrypts an encrypted string - EXACT PORT from frontend * @param encryptedText The encrypted text, including IV and auth tag * @returns The original plain text string */ decrypt(encryptedText: string): string { if (!encryptedText || typeof encryptedText !== 'string') { throw new Error('Encrypted text must be a non-empty string'); } const data = Buffer.from(encryptedText, 'hex'); const key = Buffer.from(this.encryptionKey, 'base64'); const iv = data.slice(0, IV_LENGTH); const authTag = data.slice(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH); const encrypted = data.slice(IV_LENGTH + AUTH_TAG_LENGTH); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); decipher.setAAD(Buffer.from('vcsys-cli', 'utf8')); // Additional authenticated data let decrypted = decipher.update(encrypted); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString('utf8'); } /** * Validate encryption key format */ private validateKeyFormat(key: string): boolean { try { const keyBuffer = Buffer.from(key, 'base64'); return keyBuffer.length === 32; } catch (error) { return false; } } /** * Get encryption key info (for debugging) */ getKeyInfo(): { keyExists: boolean; keyValid: boolean; keyPath: string } { const keyPath = path.join(os.homedir(), '.vcsys', 'encryption.key'); return { keyExists: fs.existsSync(keyPath), keyValid: this.validateKeyFormat(this.encryptionKey), keyPath }; } /** * Rotate encryption key (for security) */ rotateKey(): void { const keyPath = path.join(os.homedir(), '.vcsys', 'encryption.key'); // Backup old key if (fs.existsSync(keyPath)) { const backupPath = `${keyPath}.backup.${Date.now()}`; fs.copyFileSync(keyPath, backupPath); this.logger.info('Old encryption key backed up', { backupPath }); } // Generate and save new key this.encryptionKey = crypto.randomBytes(32).toString('base64'); fs.writeFileSync(keyPath, this.encryptionKey, { mode: 0o600 }); this.logger.info('Encryption key rotated successfully'); } } // Export singleton instance export const encryptionManager = new EncryptionManager();