import { log } from '../utils/logger'; /** * Client-side password encryption service * Uses Web Crypto API with AES-256-GCM algorithm */ class EncryptionService { private encryptionKey: CryptoKey | null = null; /** * Initialize encryption key * @param keyBase64 Base64 encoded key from server */ async initializeKey(keyBase64: string): Promise { try { // this._keyBase64 = keyBase64; // Decode Base64 key const keyBuffer = this.base64ToArrayBuffer(keyBase64); // Import key for use with Web Crypto API this.encryptionKey = await crypto.subtle.importKey( 'raw', keyBuffer, { name: 'AES-GCM' }, false, // not exportable ['encrypt', 'decrypt'] ); log.info('encryption-service', 'Encryption key initialized successfully'); } catch (error) { log.error('encryption-service', 'Failed to initialize encryption key', { error }); throw new Error('Failed to initialize encryption key'); } } /** * Encrypt password using AES-GCM and AEAD * @param password Password in plain text * @param sessionId Session ID for AAD * @returns Encrypted password in Base64 */ async encryptPassword(password: string, sessionId?: string): Promise<{encrypted: string, nonce: string, timestamp: number}> { if (!this.encryptionKey) { throw new Error('Encryption key not initialized'); } try { // CRITICAL: Generate unique IV (12 bytes for GCM) const iv = crypto.getRandomValues(new Uint8Array(12)); // Convert password to ArrayBuffer const passwordBuffer = new TextEncoder().encode(password); // AAD used ONLY for saved machines (when sessionId exists) // For temporary connection passwords AAD is NOT needed const encryptParams: AesGcmParams = { name: 'AES-GCM', iv: iv }; let timestamp = 0; let clientNonce: Uint8Array | undefined; // If sessionId provided - use AAD for additional security if (sessionId) { timestamp = Date.now(); clientNonce = crypto.getRandomValues(new Uint8Array(16)); const aadData = { sessionId: sessionId, timestamp: timestamp, clientNonce: Array.from(clientNonce) }; encryptParams.additionalData = new TextEncoder().encode(JSON.stringify(aadData)); } // Encrypt const encryptedBuffer = await crypto.subtle.encrypt( encryptParams, this.encryptionKey, passwordBuffer ); // Combine IV + encrypted data const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength); combined.set(iv, 0); combined.set(new Uint8Array(encryptedBuffer), iv.length); // Convert to Base64 const encrypted = this.arrayBufferToBase64(combined.buffer); const nonce = clientNonce ? this.arrayBufferToBase64(clientNonce.buffer as ArrayBuffer) : ''; log.debug('encryption-service', 'Password encrypted successfully', { passwordLength: password.length, encryptedLength: encrypted.length, sessionId: sessionId, usesAAD: !!sessionId }); return { encrypted, nonce, timestamp }; } catch (error) { log.error('encryption-service', 'Failed to encrypt password', { error }); throw new Error('Password encryption failed'); } } /** * Decrypt password (for testing) * @param encryptedPassword Base64 encrypted string * @param sessionId Session ID for AAD (optional - for testing) * @param nonce Client nonce for AAD (optional - for testing) * @param timestamp Timestamp for AAD (optional - for testing) * @returns Password in plain text */ async decryptPassword( encryptedPassword: string, sessionId?: string, nonce?: string, timestamp?: number ): Promise { if (!this.encryptionKey) { throw new Error('Encryption key not initialized'); } try { // Decode Base64 const combined = this.base64ToArrayBuffer(encryptedPassword); // Extract IV (first 12 bytes) and encrypted data const iv = combined.slice(0, 12); const encryptedData = combined.slice(12); // FIXED: Create AAD if parameters provided let additionalData: Uint8Array | undefined = undefined; if (sessionId && nonce && timestamp) { const clientNonceArray = new Uint8Array(this.base64ToArrayBuffer(nonce)); const aadData = { sessionId: sessionId, timestamp: timestamp, clientNonce: Array.from(clientNonceArray) }; additionalData = new TextEncoder().encode(JSON.stringify(aadData)); } // Decrypt const decryptedBuffer = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv, ...(additionalData && { additionalData }) // Add AAD if exists }, this.encryptionKey, encryptedData ); // Convert back to string const password = new TextDecoder().decode(decryptedBuffer); log.debug('encryption-service', 'Password decrypted successfully'); return password; } catch (error) { log.error('encryption-service', 'Failed to decrypt password', { error }); throw new Error('Password decryption failed'); } } /** * Check if encryption key is initialized */ isInitialized(): boolean { return this.encryptionKey !== null; } /** * Get current encryption status */ getStatus(): { initialized: boolean; } { return { initialized: this.isInitialized(), }; } /** * Clear encryption key */ clearKey(): void { this.encryptionKey = null; log.info('encryption-service', 'Encryption key cleared'); } /** * Convert Base64 string to ArrayBuffer */ private base64ToArrayBuffer(base64: string): ArrayBuffer { const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } /** * Convert ArrayBuffer to Base64 string */ private arrayBufferToBase64(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** * Test encryption/decryption */ async testEncryption(testPassword: string = 'test123', sessionId: string = 'test-session'): Promise { try { if (!this.isInitialized()) { throw new Error('Encryption key not initialized'); } // Encrypt test password const encryptionResult = await this.encryptPassword(testPassword, sessionId); // FIXED: Decrypt with same AAD parameters const decrypted = await this.decryptPassword( encryptionResult.encrypted, sessionId, encryptionResult.nonce, encryptionResult.timestamp ); // Check that we got the same password const success = decrypted === testPassword; log.info('encryption-service', 'Encryption test completed', { success, testPasswordLength: testPassword.length, encryptedLength: encryptionResult.encrypted.length }); return success; } catch (error) { log.error('encryption-service', 'Encryption test failed', { error }); return false; } } } // Export singleton instance export const encryptionService = new EncryptionService(); export default encryptionService;