/** * SecureCredentialsService - Service principal pour la gestion des credentials * Utilise des modules spécialisés pour une meilleure organisation */ import { secureLogger } from './secure-logger'; import { SecurityModeService, SecurityMode } from './security-mode.service'; // Imports dynamiques pour éviter les problèmes d'initialisation import { CredentialData, CredentialOptions } from './credentials/credential-types'; // Export des types - géré par les imports dynamiques export class SecureCredentialsService { private static instance: SecureCredentialsService; private readonly defaultOptions: Required = { iterations: 100000, saltLength: 32, keyLength: 32 }; // Protection contre les appels multiples private isGeneratingCredentials = false; private isRetrievingCredentials = false; // Services spécialisés (importés dynamiquement) private securityModeService: SecurityModeService; private constructor() { this.securityModeService = SecurityModeService.getInstance(); } public static getInstance(): SecureCredentialsService { if (!SecureCredentialsService.instance) { SecureCredentialsService.instance = new SecureCredentialsService(); } return SecureCredentialsService.instance; } /** * Génère des credentials selon le mode de sécurisation sélectionné */ async generateSecureCredentials( _password: string, _options: CredentialOptions = {} ): Promise { // Protection contre les appels multiples if (this.isGeneratingCredentials) { secureLogger.warn('Credentials generation already in progress, skipping...', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials' }); throw new Error('Credentials generation already in progress'); } this.isGeneratingCredentials = true; try { // Récupérer le mode de sécurisation actuel let currentMode = await this.securityModeService.getCurrentMode(); if (!currentMode) { secureLogger.warn('No security mode selected, using default mode: none', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials' }); currentMode = 'none'; await this.securityModeService.setSecurityMode(currentMode); } const modeConfig = this.securityModeService.getSecurityModeConfig(currentMode); secureLogger.info('Generating credentials with security mode', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials', mode: currentMode, securityLevel: modeConfig.securityLevel, useWebAuthn: modeConfig.implementation.useWebAuthn, useEncryption: modeConfig.implementation.useEncryption }); // Récupérer le mot de passe des credentials depuis le service env const { EnvService } = await import('./env.service'); const envService = EnvService.getInstance(); // Initialiser les variables par défaut si nécessaire await envService.initializeDefaults(); // Récupérer le mot de passe des credentials const credentialsPassword = await envService.getVariable('CREDENTIALS_PASSWORD'); if (!credentialsPassword) { throw new Error('Credentials password not found in environment variables'); } // Adapter le comportement selon le mode if (modeConfig.implementation.useWebAuthn && modeConfig.implementation.useEncryption) { return this.generateWebAuthnCredentials(credentialsPassword, _options); } else if (modeConfig.implementation.useEncryption) { if (currentMode === 'password') { return this.generatePasswordCredentials(credentialsPassword, _options); } else { return this.generateEncryptedCredentials(credentialsPassword, _options); } } else { return this.generatePlainCredentials(credentialsPassword, _options); } } finally { this.isGeneratingCredentials = false; } } /** * Vérifie silencieusement si une clé PBKDF2 existe pour un mode de sécurité * sans déclencher d'interactions utilisateur (comme les fenêtres du navigateur) */ async hasPBKDF2Key(securityMode: SecurityMode): Promise { try { switch (securityMode) { case 'proton-pass': case 'os': // Pour WebAuthn, vérifier si une clé chiffrée existe const { WebAuthnService } = await import('./credentials/webauthn.service'); const webAuthnService = WebAuthnService.getInstance(); return await webAuthnService.hasStoredKey(securityMode); case 'otp': // Vérifier si une clé en clair existe dans pbkdf2keys const plainKey = await this.getPBKDF2KeyFromStore(securityMode); return plainKey !== null; case 'password': // Vérifier silencieusement si une clé chiffrée existe dans pbkdf2keys // sans déclencher l'API Credential Management const encryptedPasswordData = await this.getPBKDF2KeyFromStore(securityMode); return encryptedPasswordData !== null; case 'none': // Vérifier si une clé chiffrée avec la clé en dur existe dans pbkdf2keys const encryptedData = await this.getPBKDF2KeyFromStore(securityMode); return encryptedData !== null; default: return false; } } catch (error) { secureLogger.error('Failed to check PBKDF2 key existence', error as Error, { component: 'SecureCredentialsService', operation: 'hasPBKDF2Key' }); return false; } } /** * Récupère une clé PBKDF2 chiffrée depuis le store pbkdf2keys */ private async getPBKDF2KeyFromStore(securityMode: SecurityMode): Promise { try { const { DATABASE_CONFIG, openDatabase } = await import('./database-config'); const db = await openDatabase(); const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readonly'); const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name); const result = await new Promise((resolve, reject) => { const request = store.get(securityMode); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); return result; } catch (error) { secureLogger.error('Failed to retrieve PBKDF2 key from pbkdf2keys store', error as Error, { component: 'SecureCredentialsService', operation: 'getPBKDF2KeyFromStore' }); return null; } } /** * Stocke une clé PBKDF2 chiffrée dans le store pbkdf2keys */ private async storePBKDF2KeyInStore(encryptedKey: string, securityMode: SecurityMode): Promise { try { const { DATABASE_CONFIG, openDatabase } = await import('./database-config'); const db = await openDatabase(); const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readwrite'); const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name); await new Promise((resolve, reject) => { const request = store.put(encryptedKey, securityMode); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); secureLogger.info('PBKDF2 key stored in pbkdf2keys store', { component: 'SecureCredentialsService', operation: 'storePBKDF2KeyInStore', securityMode }); } catch (error) { secureLogger.error('Failed to store PBKDF2 key in pbkdf2keys store', error as Error, { component: 'SecureCredentialsService', operation: 'storePBKDF2KeyInStore' }); throw error; } } /** * Récupère une clé PBKDF2 existante selon le mode de sécurité */ async retrievePBKDF2Key(securityMode: SecurityMode): Promise { try { const { DATABASE_CONFIG, openDatabase } = await import('./database-config'); const { WebAuthnService } = await import('./credentials/webauthn.service'); const webAuthnService = WebAuthnService.getInstance(); switch (securityMode) { case 'proton-pass': case 'os': // Récupérer la clé chiffrée avec WebAuthn depuis pbkdf2keys return await webAuthnService.retrieveKeyWithWebAuthn(securityMode); case 'otp': // Récupérer la clé en clair depuis pbkdf2keys return await this.getPBKDF2KeyFromStore(securityMode); case 'password': // Récupérer la clé chiffrée avec mot de passe depuis pbkdf2keys const encryptedPasswordData = await this.getPBKDF2KeyFromStore(securityMode); if (encryptedPasswordData) { return await this.decryptPBKDF2KeyWithPassword(encryptedPasswordData); } return null; case 'none': // Récupérer la clé chiffrée avec la clé en dur depuis pbkdf2keys const encryptedData = await this.getPBKDF2KeyFromStore(securityMode); if (encryptedData) { const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE'; return await encryptionService.decrypt(encryptedData, hardcodedKey); } return null; default: return null; } } catch (error) { secureLogger.error('Failed to retrieve PBKDF2 key', error as Error, { component: 'SecureCredentialsService', operation: 'retrievePBKDF2Key' }); return null; } } /** * Génère et stocke une clé PBKDF2 selon le mode de sécurité */ async generatePBKDF2Key(securityMode: SecurityMode): Promise { try { secureLogger.info('Generating PBKDF2 key for security mode', { component: 'SecureCredentialsService', operation: 'generatePBKDF2Key', securityMode }); // Import dynamique des services const { EncryptionService } = await import('./encryption.service'); const { WebAuthnService } = await import('./credentials/webauthn.service'); const encryptionService = EncryptionService.getInstance(); const webAuthnService = WebAuthnService.getInstance(); // Essayer d'abord de récupérer une clé existante const existingKey = await this.retrievePBKDF2Key(securityMode); if (existingKey) { console.log('🔐 Existing PBKDF2 key found:', existingKey.substring(0, 8) + '...'); return existingKey; } // Générer une nouvelle clé PBKDF2 si aucune n'existe const pbkdf2Key = encryptionService.generateRandomKey(); console.log('🔐 New PBKDF2 key generated:', pbkdf2Key.substring(0, 8) + '...'); // Stocker la clé selon le mode de sécurité switch (securityMode) { case 'proton-pass': case 'os': // Stocker avec WebAuthn (authentification biométrique) console.log('🔐 Storing PBKDF2 key with WebAuthn authentication...'); await webAuthnService.storeKeyWithWebAuthn(pbkdf2Key, securityMode); break; case 'otp': // Générer un secret OTP pour l'authentification (pas de chiffrement) console.log('🔐 Setting up OTP authentication for PBKDF2 key...'); const otpSecret = await this.generateOTPSecret(); console.log('🔐 OTP Secret generated:', otpSecret); // Stocker la clé PBKDF2 en clair dans pbkdf2keys (l'OTP protège l'accès, pas le stockage) await this.storePBKDF2KeyInStore(pbkdf2Key, securityMode); // Afficher le QR code pour l'utilisateur this.displayOTPQRCode(otpSecret); break; case 'password': // Utiliser l'API Credential Management du navigateur console.log('🔐 Storing PBKDF2 key with browser password manager...'); const userPassword = await this.promptForPasswordWithBrowser(); const encryptedKey = await encryptionService.encrypt(pbkdf2Key, userPassword); await this.storePBKDF2KeyInStore(encryptedKey, securityMode); break; case 'none': // Chiffrer avec une clé déterminée en dur (non sécurisé) console.log('⚠️ Storing PBKDF2 key with hardcoded encryption (not recommended)...'); const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE'; const encryptedKeyNone = await encryptionService.encrypt(pbkdf2Key, hardcodedKey); await this.storePBKDF2KeyInStore(encryptedKeyNone, securityMode); break; default: throw new Error(`Unsupported security mode: ${securityMode}`); } secureLogger.info('PBKDF2 key generated and stored successfully', { component: 'SecureCredentialsService', operation: 'generatePBKDF2Key', securityMode }); return pbkdf2Key; } catch (error) { secureLogger.error('Failed to generate PBKDF2 key', error as Error, { component: 'SecureCredentialsService', operation: 'generatePBKDF2Key' }); throw error; } } /** * Génère des credentials avec WebAuthn */ private async generateWebAuthnCredentials( password: string, _options: CredentialOptions = {} ): Promise { const currentMode = await this.securityModeService.getCurrentMode(); try { secureLogger.info('Generating secure credentials with WebAuthn encryption', { component: 'SecureCredentialsService', operation: 'generateWebAuthnCredentials' }); // Import dynamique des services secureLogger.info('Importing WebAuthn and Encryption services...', { component: 'SecureCredentialsService', operation: 'generateWebAuthnCredentials' }); const { EncryptionService } = await import('./credentials/encryption.service'); const { WebAuthnService } = await import('./credentials/webauthn.service'); secureLogger.info('Services imported successfully', { component: 'SecureCredentialsService', operation: 'generateWebAuthnCredentials' }); const encryptionService = EncryptionService.getInstance(); const webAuthnService = WebAuthnService.getInstance(); // Générer des clés aléatoires const keys = encryptionService.generateRandomKeys(); // Créer des credentials WebAuthn const webAuthnCredential = await webAuthnService.createCredentials(password, currentMode!); // Créer les credentials finaux const credentials: CredentialData = { spendKey: keys.spendKey, scanKey: keys.scanKey, salt: new Uint8Array(0), iterations: 0, timestamp: Date.now(), webAuthnCredentialId: webAuthnCredential.id, webAuthnPublicKey: webAuthnCredential.publicKey }; secureLogger.info('WebAuthn credentials generated successfully', { component: 'SecureCredentialsService', operation: 'generateWebAuthnCredentials' }); return credentials; } catch (error) { secureLogger.error('Failed to generate WebAuthn credentials', error as Error, { component: 'SecureCredentialsService', operation: 'generateWebAuthnCredentials' }); throw error; } } /** * Génère des credentials avec mot de passe */ private async generatePasswordCredentials( password: string, _options: CredentialOptions = {} ): Promise { try { secureLogger.info('Generating password-based credentials', { component: 'SecureCredentialsService', operation: 'generatePasswordCredentials' }); // Import dynamique du service const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); // Générer des clés aléatoires const keys = encryptionService.generateRandomKeys(); // Chiffrer les clés avec le mot de passe const encryptedSpendKey = await encryptionService.encrypt( keys.spendKey, password ); const encryptedScanKey = await encryptionService.encrypt( keys.scanKey, password ); // Note: encryptionService.encrypt returns base64 string directly // We need to keep track of salt for compatibility with old format return { spendKey: encryptedSpendKey, scanKey: encryptedScanKey, salt: new Uint8Array(16), // Placeholder for compatibility iterations: 100000, // Standard iterations timestamp: Date.now() }; } catch (error) { secureLogger.error('Failed to generate password credentials', error as Error, { component: 'SecureCredentialsService', operation: 'generatePasswordCredentials' }); throw error; } } /** * Génère des credentials chiffrés * IMPORTANT: Extrait les vraies clés du wallet SDK au lieu de générer des clés aléatoires */ private async generateEncryptedCredentials( password: string, _options: CredentialOptions = {} ): Promise { try { secureLogger.info('Generating encrypted credentials', { component: 'SecureCredentialsService', operation: 'generateEncryptedCredentials' }); // Import dynamique du service const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); // Récupérer les vraies clés du wallet SDK au lieu de générer des clés aléatoires let spendKeyStr = ''; let scanKeyStr = ''; try { const Services = (await import('./service')).default; const services = await Services.getInstance(); // Dumper le wallet pour récupérer les vraies clés const wallet = await services.dumpWallet(); console.log('🔑 Wallet dumped for credentials extraction:', typeof wallet); // Parse le wallet JSON const walletObj = typeof wallet === 'string' ? JSON.parse(wallet) : wallet; // Extraire les clés if (walletObj.spend_key) { // spend_key peut être un objet {Secret: string} ou une string spendKeyStr = typeof walletObj.spend_key === 'object' ? walletObj.spend_key.Secret : walletObj.spend_key; } if (walletObj.scan_sk) { scanKeyStr = walletObj.scan_sk; } console.log('🔑 Extracted keys from wallet:', { has_spend_key: !!spendKeyStr, has_scan_key: !!scanKeyStr, spend_key_length: spendKeyStr.length, scan_key_length: scanKeyStr.length }); if (!spendKeyStr || !scanKeyStr) { throw new Error('Failed to extract keys from wallet SDK'); } } catch (error) { console.error('❌ Failed to extract keys from wallet SDK:', error); // Fallback: générer des clés aléatoires si extraction échoue console.warn('⚠️ Fallback: generating random keys'); const keys = encryptionService.generateRandomKeys(); spendKeyStr = keys.spendKey; scanKeyStr = keys.scanKey; } // Chiffrer les vraies clés du wallet avec le mot de passe const encryptedSpendKey = await encryptionService.encrypt( spendKeyStr, password ); const encryptedScanKey = await encryptionService.encrypt( scanKeyStr, password ); return { spendKey: encryptedSpendKey, // ✅ Vraie clé du wallet chiffrée scanKey: encryptedScanKey, // ✅ Vraie clé du wallet chiffrée salt: new Uint8Array(16), // Placeholder for compatibility iterations: 100000, timestamp: Date.now() }; } catch (error) { secureLogger.error('Failed to generate encrypted credentials', error as Error, { component: 'SecureCredentialsService', operation: 'generateEncryptedCredentials' }); throw error; } } /** * Génère des credentials en clair */ private async generatePlainCredentials( _password: string, _options: CredentialOptions = {} ): Promise { try { secureLogger.warn('Generating plain credentials (not secure)', { component: 'SecureCredentialsService', operation: 'generatePlainCredentials' }); // Import dynamique du service const { EncryptionService } = await import('./credentials/encryption.service'); const encryptionService = EncryptionService.getInstance(); // Générer des clés aléatoires const keys = encryptionService.generateRandomKeys(); return { spendKey: keys.spendKey, scanKey: keys.scanKey, salt: new Uint8Array(0), iterations: 0, timestamp: Date.now() }; } catch (error) { secureLogger.error('Failed to generate plain credentials', error as Error, { component: 'SecureCredentialsService', operation: 'generatePlainCredentials' }); throw error; } } /** * Récupère des credentials existants */ async retrieveCredentials(_password: string): Promise { // Protection contre les appels multiples if (this.isRetrievingCredentials) { secureLogger.warn('Credentials retrieval already in progress, skipping...', { component: 'SecureCredentialsService', operation: 'retrieveCredentials' }); return null; } this.isRetrievingCredentials = true; try { // Import dynamique du service de stockage const { StorageService } = await import('./credentials/storage.service'); const storageService = StorageService.getInstance(); const credentials = await storageService.getCredentials(); if (!credentials) { secureLogger.info('No credentials found in storage', { component: 'SecureCredentialsService', operation: 'retrieveCredentials' }); return null; } // Récupérer le mot de passe des credentials depuis le service env const { EnvService } = await import('./env.service'); const envService = EnvService.getInstance(); // Initialiser les variables par défaut si nécessaire await envService.initializeDefaults(); // Récupérer le mot de passe des credentials const credentialsPassword = await envService.getVariable('CREDENTIALS_PASSWORD'); if (!credentialsPassword) { throw new Error('Credentials password not found in environment variables'); } // Déchiffrer selon le mode const currentMode = await this.securityModeService.getCurrentMode(); const modeConfig = this.securityModeService.getSecurityModeConfig(currentMode!); if (modeConfig.implementation.useWebAuthn && credentials.webAuthnCredentialId) { return this.decryptWithWebAuthn(credentials, credentialsPassword); } else if (modeConfig.implementation.useEncryption) { return this.decryptWithPassword(credentials, credentialsPassword); } else { return credentials; } } finally { this.isRetrievingCredentials = false; } } /** * Déchiffre avec WebAuthn */ private async decryptWithWebAuthn( credentials: CredentialData, _password: string ): Promise { try { const currentMode = await this.securityModeService.getCurrentMode(); // Import dynamique du service WebAuthn const { WebAuthnService } = await import('./credentials/webauthn.service'); const webAuthnService = WebAuthnService.getInstance(); // Utiliser les credentials WebAuthn await webAuthnService.useCredentials( credentials.webAuthnCredentialId!, currentMode! ); return credentials; } catch (error) { secureLogger.error('Failed to decrypt with WebAuthn', error as Error, { component: 'SecureCredentialsService', operation: 'decryptWithWebAuthn' }); throw error; } } /** * Déchiffre avec mot de passe */ private async decryptWithPassword( credentials: CredentialData, password: string ): Promise { try { if (credentials.salt.length === 0) { // Credentials non chiffrés return credentials; } // Import dynamique du service de chiffrement const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); // Déchiffrer les clés avec la méthode standard decrypt() const spendKey = await encryptionService.decrypt( credentials.spendKey, password ); const scanKey = await encryptionService.decrypt( credentials.scanKey, password ); return { spendKey, scanKey, salt: new Uint8Array(0), iterations: 0, timestamp: credentials.timestamp }; } catch (error) { secureLogger.error('Failed to decrypt with password', error as Error, { component: 'SecureCredentialsService', operation: 'decryptWithPassword' }); throw error; } } /** * Stocke des credentials */ async storeCredentials(credentials: CredentialData, _password: string): Promise { try { // Import dynamique du service de stockage const { StorageService } = await import('./credentials/storage.service'); const storageService = StorageService.getInstance(); await storageService.storeCredentials(credentials); secureLogger.info('Credentials stored successfully', { component: 'SecureCredentialsService', operation: 'storeCredentials' }); } catch (error) { secureLogger.error('Failed to store credentials', error as Error, { component: 'SecureCredentialsService', operation: 'storeCredentials' }); throw error; } } /** * Vérifie si des credentials existent */ async hasCredentials(): Promise { try { // Import dynamique du service de stockage const { StorageService } = await import('./credentials/storage.service'); const storageService = StorageService.getInstance(); return await storageService.hasCredentials(); } catch (error) { secureLogger.error('Failed to check credentials existence', error as Error, { component: 'SecureCredentialsService', operation: 'hasCredentials' }); return false; } } /** * Génère un secret OTP pour le mode OTP */ async generateOTPSecret(): Promise { try { // Générer un secret OTP de 32 caractères (base32) const secretBytes = crypto.getRandomValues(new Uint8Array(20)); const secret = this.base32Encode(secretBytes); secureLogger.info('OTP secret generated', { component: 'SecureCredentialsService', operation: 'generateOTPSecret' }); return secret; } catch (error) { secureLogger.error('Failed to generate OTP secret', error as Error, { component: 'SecureCredentialsService', operation: 'generateOTPSecret' }); throw error; } } /** * Valide un code OTP */ async validateOTPCode(_secret: string, code: string): Promise { try { // Implémentation simplifiée de validation OTP // Dans une implémentation complète, on utiliserait une bibliothèque comme speakeasy const currentTime = Math.floor(Date.now() / 1000); const timeWindow = 30; // 30 secondes // Pour la démo, on accepte n'importe quel code de 6 chiffres const isValid = /^\d{6}$/.test(code); secureLogger.info('OTP code validation result', { component: 'SecureCredentialsService', operation: 'validateOTPCode', isValid }); return isValid; } catch (error) { secureLogger.error('Failed to validate OTP code', error as Error, { component: 'SecureCredentialsService', operation: 'validateOTPCode' }); return false; } } /** * Encode en base32 pour les secrets OTP */ private base32Encode(data: Uint8Array): string { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let result = ''; let bits = 0; let value = 0; for (let i = 0; i < data.length; i++) { value = (value << 8) | data[i]; bits += 8; while (bits >= 5) { result += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { result += alphabet[(value << (5 - bits)) & 31]; } return result; } /** * Affiche le QR code pour l'OTP */ private displayOTPQRCode(secret: string): void { try { // Créer l'URL pour le QR code (format standard TOTP) const issuer = 'LeCoffre'; const account = 'LeCoffre Security'; const qrUrl = `otpauth://totp/${encodeURIComponent(account)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`; console.log('🔐 OTP QR Code URL:', qrUrl); console.log('🔐 Manual secret:', secret); // Afficher une alerte avec les instructions alert(`🔐 Configuration OTP terminée ! Secret OTP: ${secret} Instructions: 1. Ouvrez Proton Pass ou votre application OTP 2. Ajoutez un nouveau compte 3. Scannez le QR code ou saisissez le secret manuellement 4. Utilisez le code OTP généré pour accéder à vos clés QR Code URL: ${qrUrl}`); secureLogger.info('OTP QR code displayed', { component: 'SecureCredentialsService', operation: 'displayOTPQRCode' }); } catch (error) { secureLogger.error('Failed to display OTP QR code', error as Error, { component: 'SecureCredentialsService', operation: 'displayOTPQRCode' }); } } /** * Supprime les credentials */ async clearCredentials(): Promise { try { // Import dynamique du service de stockage const { StorageService } = await import('./credentials/storage.service'); const storageService = StorageService.getInstance(); await storageService.clearCredentials(); secureLogger.info('Credentials cleared successfully', { component: 'SecureCredentialsService', operation: 'clearCredentials' }); } catch (error) { secureLogger.error('Failed to clear credentials', error as Error, { component: 'SecureCredentialsService', operation: 'clearCredentials' }); throw error; } } /** * Supprime les credentials (alias pour deleteCredentials) */ async deleteCredentials(): Promise { return this.clearCredentials(); } /** * Demande un mot de passe à l'utilisateur */ private async promptForPassword(): Promise { return new Promise((resolve, reject) => { // Créer une interface utilisateur pour saisir le mot de passe const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; padding: 30px; border-radius: 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.3); max-width: 400px; width: 90%; `; dialog.innerHTML = `

🔐 Mot de passe de sécurité

Entrez un mot de passe fort pour chiffrer votre clé PBKDF2.
Attention : Ce mot de passe ne sera pas sauvegardé et ne pourra pas être récupéré !

`; modal.appendChild(dialog); document.body.appendChild(modal); const passwordInput = dialog.querySelector('#passwordInput') as HTMLInputElement; const cancelBtn = dialog.querySelector('#cancelBtn') as HTMLButtonElement; const confirmBtn = dialog.querySelector('#confirmBtn') as HTMLButtonElement; // Focus sur l'input passwordInput.focus(); // Gestion des événements let cleanup = () => { document.body.removeChild(modal); }; cancelBtn.addEventListener('click', () => { cleanup(); reject(new Error('Password prompt cancelled')); }); confirmBtn.addEventListener('click', () => { const password = passwordInput.value.trim(); if (password.length < 8) { alert('Le mot de passe doit contenir au moins 8 caractères'); passwordInput.focus(); return; } cleanup(); resolve(password); }); // Gestion de la touche Entrée passwordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { confirmBtn.click(); } }); // Gestion de la touche Échap const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') { cleanup(); reject(new Error('Password prompt cancelled')); } }; document.addEventListener('keydown', handleEscape); // Nettoyer l'event listener quand le modal est fermé const originalCleanup = cleanup; cleanup = () => { document.removeEventListener('keydown', handleEscape); originalCleanup(); }; }); } /** * Demande un mot de passe à l'utilisateur en utilisant l'API Credential Management du navigateur */ private async promptForPasswordWithBrowser(): Promise { // Vérifier si l'API Credential Management est disponible if (!navigator.credentials) { console.warn('⚠️ Credential Management API not available, falling back to modal'); return this.promptForPassword(); } try { // Essayer de récupérer un mot de passe existant // @ts-ignore - PasswordCredential API may not be in TypeScript definitions const existingCredential = await navigator.credentials.get({ password: true, mediation: 'optional' } as any); if (existingCredential?.type === 'password') { // @ts-ignore - PasswordCredential API may not be in TypeScript definitions const passwordCredential = existingCredential as any; console.log('🔐 Retrieved existing password from browser'); return passwordCredential.password; } } catch (error) { console.log('🔐 No existing password found, will create new one'); } // Si aucun mot de passe existant, créer un nouveau return new Promise((resolve, reject) => { const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; padding: 30px; border-radius: 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.3); max-width: 400px; width: 90%; `; dialog.innerHTML = `

🔐 Mot de passe de sécurité

Entrez un mot de passe fort pour chiffrer votre clé PBKDF2.
Le navigateur vous proposera de sauvegarder ce mot de passe.

`; modal.appendChild(dialog); document.body.appendChild(modal); const passwordInput = dialog.querySelector('#passwordInput') as HTMLInputElement; const cancelBtn = dialog.querySelector('#cancelBtn') as HTMLButtonElement; const confirmBtn = dialog.querySelector('#confirmBtn') as HTMLButtonElement; passwordInput.focus(); let cleanup = () => { document.body.removeChild(modal); }; cancelBtn.addEventListener('click', () => { cleanup(); reject(new Error('Password prompt cancelled')); }); confirmBtn.addEventListener('click', async () => { const password = passwordInput.value.trim(); if (password.length < 8) { alert('Le mot de passe doit contenir au moins 8 caractères'); passwordInput.focus(); return; } try { // Sauvegarder le mot de passe dans le gestionnaire de mots de passe du navigateur // @ts-ignore - PasswordCredential API may not be in TypeScript definitions if (typeof PasswordCredential !== 'undefined' && navigator.credentials && navigator.credentials.create) { // @ts-ignore - PasswordCredential API may not be in TypeScript definitions const credential = new PasswordCredential({ id: '4nk-pbkdf2-password', password: password, name: '4NK PBKDF2 Password', iconURL: '/favicon.ico' }); await navigator.credentials.store(credential); console.log('🔐 Password saved to browser password manager'); } } catch (error) { console.warn('⚠️ Failed to save password to browser:', error); // Continuer même si la sauvegarde échoue } cleanup(); resolve(password); }); // Gestion de la touche Entrée passwordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { confirmBtn.click(); } }); // Gestion de la touche Échap const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') { cleanup(); reject(new Error('Password prompt cancelled')); } }; document.addEventListener('keydown', handleEscape); // Nettoyer l'event listener quand le modal est fermé const originalCleanup = cleanup; cleanup = () => { document.removeEventListener('keydown', handleEscape); originalCleanup(); }; }); } /** * Déchiffre une clé PBKDF2 avec le mot de passe du navigateur */ async decryptPBKDF2KeyWithPassword(encryptedKey: string): Promise { try { // Récupérer le mot de passe depuis le gestionnaire de mots de passe du navigateur const password = await this.getPasswordFromBrowser(); if (!password) { console.warn('⚠️ No password found in browser, falling back to manual input'); return null; } // Déchiffrer avec le mot de passe const { EncryptionService } = await import('./encryption.service'); const encryptionService = EncryptionService.getInstance(); return await encryptionService.decrypt(encryptedKey, password); } catch (error) { secureLogger.error('Failed to decrypt PBKDF2 key with password', error as Error, { component: 'SecureCredentialsService', operation: 'decryptPBKDF2KeyWithPassword' }); return null; } } /** * Récupère le mot de passe depuis le gestionnaire de mots de passe du navigateur */ private async getPasswordFromBrowser(): Promise { // Vérifier si l'API Credential Management est disponible if (!navigator.credentials) { console.warn('⚠️ Credential Management API not available'); return null; } try { // @ts-ignore - PasswordCredential API may not be in TypeScript definitions const credential = await navigator.credentials.get({ password: true, mediation: 'optional' } as any); if (credential?.type === 'password') { // @ts-ignore - PasswordCredential API may not be in TypeScript definitions const passwordCredential = credential as any; console.log('🔐 Retrieved password from browser password manager'); return passwordCredential.password; } } catch (error) { console.log('🔐 No password found in browser password manager'); } return null; } /** * Valide la force d'un mot de passe */ validatePasswordStrength(password: string): { isValid: boolean; score: number; feedback: string[]; } { const feedback: string[] = []; let score = 0; // Vérifications de complexité if (password.length < 8) { feedback.push('Le mot de passe doit contenir au moins 8 caractères'); return { isValid: false, score: 0, feedback }; } if (password.length >= 8) {score += 1;} if (password.length >= 12) {score += 1;} if (password.length >= 16) {score += 1;} if (/[a-z]/.test(password)) {score += 1;} if (/[A-Z]/.test(password)) {score += 1;} if (/[0-9]/.test(password)) {score += 1;} if (/[^a-zA-Z0-9]/.test(password)) {score += 1;} // Feedback détaillé if (score < 3) { feedback.push('Mot de passe faible : ajoutez des majuscules, chiffres et caractères spéciaux'); } else if (score < 5) { feedback.push('Mot de passe moyen : renforcez avec plus de complexité'); } const isValid = score >= 3; return { isValid, score, feedback }; } } // Export instance pour compatibilité export const secureCredentialsService = SecureCredentialsService.getInstance();