diff --git a/src/pages/wallet-setup/wallet-setup.ts b/src/pages/wallet-setup/wallet-setup.ts index b93220a..0860834 100644 --- a/src/pages/wallet-setup/wallet-setup.ts +++ b/src/pages/wallet-setup/wallet-setup.ts @@ -172,9 +172,7 @@ document.addEventListener('DOMContentLoaded', async () => { console.log('🔐 Using security mode for wallet encryption:', currentMode); // GĂ©nĂ©rer un wallet temporaire avec Ă©tat birthday_waiting - const { StorageService } = await import('../../services/credentials/storage.service'); - const { EncryptionService } = await import('../../services/credentials/encryption.service'); - const storageService = StorageService.getInstance(); + const { EncryptionService } = await import('../../services/encryption.service'); const encryptionService = EncryptionService.getInstance(); // GĂ©nĂ©rer des clĂ©s temporaires pour le wallet @@ -201,7 +199,7 @@ document.addEventListener('DOMContentLoaded', async () => { console.log('🔐 PBKDF2 key retrieved for wallet encryption'); // Chiffrer le wallet avec la clĂ© PBKDF2 - const encryptedWallet = await encryptionService.encryptWithPassword( + const encryptedWallet = await encryptionService.encrypt( JSON.stringify(walletData), pbkdf2Key ); @@ -274,7 +272,7 @@ document.addEventListener('DOMContentLoaded', async () => { // Le device contient des donnĂ©es sensibles (sp_wallet) qui ne doivent JAMAIS ĂȘtre en clair console.log('🔐 Encrypting device data before storage...'); const deviceString = JSON.stringify(device); - const encryptedDevice = await encryptionService.encryptWithPassword(deviceString, pbkdf2Key); + const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key); console.log('🔐 Device encrypted successfully'); await new Promise((resolve, reject) => { @@ -408,8 +406,8 @@ document.addEventListener('DOMContentLoaded', async () => { } else { console.log('✅ TEST: PBKDF2 key retrieved for decryption test'); - // DĂ©chiffrer le wallet chiffrĂ© (format base64) - const decryptedWallet = await encryptionService.decryptWithPasswordBase64( + // DĂ©chiffrer le wallet chiffrĂ© + const decryptedWallet = await encryptionService.decrypt( finalVerification.encrypted_wallet, pbkdf2KeyTest ); @@ -422,8 +420,8 @@ document.addEventListener('DOMContentLoaded', async () => { created_at: parsedWallet.created_at }); - // DĂ©chiffrer le device chiffrĂ© (format base64) - const decryptedDevice = await encryptionService.decryptWithPasswordBase64( + // DĂ©chiffrer le device chiffrĂ© + const decryptedDevice = await encryptionService.decrypt( finalVerification.encrypted_device, pbkdf2KeyTest ); diff --git a/src/services/encryption.service.ts b/src/services/encryption.service.ts new file mode 100644 index 0000000..95a1007 --- /dev/null +++ b/src/services/encryption.service.ts @@ -0,0 +1,200 @@ +/** + * EncryptionService - Centralized encryption/decryption service + * + * This service is the single source of truth for all encryption operations in the application. + * All encryption uses AES-GCM with PBKDF2 key derivation. + */ + +import { secureLogger } from './secure-logger'; + +export interface EncryptionConfig { + iterations?: number; + saltLength?: number; + ivLength?: number; +} + +export class EncryptionService { + private static instance: EncryptionService; + + private readonly defaultConfig: Required = { + iterations: 100000, + saltLength: 16, + ivLength: 12 + }; + + private constructor() {} + + public static getInstance(): EncryptionService { + if (!EncryptionService.instance) { + EncryptionService.instance = new EncryptionService(); + } + return EncryptionService.instance; + } + + /** + * Encrypts data using AES-GCM with PBKDF2 key derivation + * Returns base64-encoded string containing: salt (16 bytes) + IV (12 bytes) + encrypted data + * + * @param data - Plain text data to encrypt + * @param password - Password/passphrase for encryption + * @param config - Optional configuration (iterations, saltLength, ivLength) + * @returns Base64-encoded encrypted data (salt + IV + ciphertext) + */ + async encrypt(data: string, password: string, config: EncryptionConfig = {}): Promise { + try { + const cfg = { ...this.defaultConfig, ...config }; + + // Generate random salt and IV + const salt = crypto.getRandomValues(new Uint8Array(cfg.saltLength)); + const iv = crypto.getRandomValues(new Uint8Array(cfg.ivLength)); + + // Derive key using PBKDF2 + const keyMaterial = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(password), + 'PBKDF2', + false, + ['deriveBits'] + ); + + const derivedKey = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt: salt, + iterations: cfg.iterations, + hash: 'SHA-256' + }, + keyMaterial, + 256 + ); + + // Import as AES-GCM key + const cryptoKey = await crypto.subtle.importKey( + 'raw', + derivedKey, + { name: 'AES-GCM' }, + false, + ['encrypt'] + ); + + // Encrypt + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: iv }, + cryptoKey, + new TextEncoder().encode(data) + ); + + // Combine salt + IV + encrypted data + const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength); + combined.set(salt, 0); + combined.set(iv, salt.length); + combined.set(new Uint8Array(encrypted), salt.length + iv.length); + + // Return base64-encoded + return btoa(String.fromCharCode(...combined)); + } catch (error) { + secureLogger.error('Failed to encrypt data', error as Error, { + component: 'EncryptionService', + operation: 'encrypt' + }); + throw error; + } + } + + /** + * Decrypts data encrypted by the encrypt() method + * Expects base64-encoded string containing: salt (16 bytes) + IV (12 bytes) + encrypted data + * + * @param encryptedData - Base64-encoded encrypted data + * @param password - Password/passphrase used for encryption + * @param config - Optional configuration (iterations, saltLength, ivLength) + * @returns Decrypted plain text data + */ + async decrypt(encryptedData: string, password: string, config: EncryptionConfig = {}): Promise { + try { + const cfg = { ...this.defaultConfig, ...config }; + + // Decode base64 + const encrypted = atob(encryptedData); + const combined = new Uint8Array(encrypted.length); + for (let i = 0; i < encrypted.length; i++) { + combined[i] = encrypted.charCodeAt(i); + } + + // Extract salt, IV, and encrypted data + const salt = combined.slice(0, cfg.saltLength); + const iv = combined.slice(cfg.saltLength, cfg.saltLength + cfg.ivLength); + const ciphertext = combined.slice(cfg.saltLength + cfg.ivLength); + + // Derive key using PBKDF2 + const keyMaterial = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(password), + 'PBKDF2', + false, + ['deriveBits'] + ); + + const derivedKey = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt: salt, + iterations: cfg.iterations, + hash: 'SHA-256' + }, + keyMaterial, + 256 + ); + + // Import as AES-GCM key + const cryptoKey = await crypto.subtle.importKey( + 'raw', + derivedKey, + { name: 'AES-GCM' }, + false, + ['decrypt'] + ); + + // Decrypt + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, + cryptoKey, + ciphertext + ); + + return new TextDecoder().decode(decrypted); + } catch (error) { + secureLogger.error('Failed to decrypt data', error as Error, { + component: 'EncryptionService', + operation: 'decrypt' + }); + throw error; + } + } + + /** + * Generate a random key + * + * @param length - Key length in bytes (default: 32) + * @returns Hex-encoded random key + */ + generateRandomKey(length: number = 32): string { + const keyBytes = crypto.getRandomValues(new Uint8Array(length)); + return Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join(''); + } + + /** + * Generate random spend and scan keys + * + * @returns Object with spendKey and scanKey (hex-encoded) + */ + generateRandomKeys(): { spendKey: string; scanKey: string } { + const spendKey = crypto.getRandomValues(new Uint8Array(32)); + const scanKey = crypto.getRandomValues(new Uint8Array(32)); + + return { + spendKey: Array.from(spendKey).map(b => b.toString(16).padStart(2, '0')).join(''), + scanKey: Array.from(scanKey).map(b => b.toString(16).padStart(2, '0')).join('') + }; + } +}