/** * SecureCredentialsService - Gestion sécurisée des credentials avec PBKDF2 * Utilise les credentials du navigateur pour sécuriser les clés de spend et de scan */ import { secureLogger } from './secure-logger'; export interface CredentialData { spendKey: string; scanKey: string; salt: Uint8Array; iterations: number; timestamp: number; } export interface CredentialOptions { iterations?: number; saltLength?: number; keyLength?: number; } export class SecureCredentialsService { private static instance: SecureCredentialsService; private readonly defaultOptions: Required = { iterations: 100000, saltLength: 32, keyLength: 32 }; private constructor() {} public static getInstance(): SecureCredentialsService { if (!SecureCredentialsService.instance) { SecureCredentialsService.instance = new SecureCredentialsService(); } return SecureCredentialsService.instance; } /** * Génère des credentials sécurisés avec WebAuthn */ async generateSecureCredentials( password: string, _options: CredentialOptions = {} ): Promise { try { secureLogger.info('Generating secure credentials with WebAuthn', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials' }); // Vérifier que WebAuthn est disponible if (!navigator.credentials || !navigator.credentials.create) { throw new Error('WebAuthn not supported in this browser'); } // Créer un challenge aléatoire const challenge = crypto.getRandomValues(new Uint8Array(32)); // Créer les options WebAuthn const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = { challenge: challenge, rp: { name: "4NK Pairing", id: window.location.hostname }, user: { id: new TextEncoder().encode(password), name: "4nk-user", displayName: "4NK User" }, pubKeyCredParams: [ { alg: -7, type: "public-key" }, // ES256 { alg: -257, type: "public-key" } // RS256 ], authenticatorSelection: { authenticatorAttachment: "platform", // Force l'authentificateur intégré userVerification: "required" }, timeout: 60000, attestation: "direct" }; console.log('🔐 Requesting WebAuthn credential creation...'); // Créer le credential WebAuthn const credential = await navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions }) as PublicKeyCredential; if (!credential) { throw new Error('WebAuthn credential creation failed'); } console.log('✅ WebAuthn credential created successfully'); // Extraire les données du credential const response = credential.response as AuthenticatorAttestationResponse; const publicKey = response.getPublicKey(); const credentialId = credential.id; // Générer les clés de chiffrement à partir du credential const spendKey = Array.from(new Uint8Array(publicKey || new ArrayBuffer(32))) .map(b => b.toString(16).padStart(2, '0')) .join(''); const scanKey = Array.from(new Uint8Array(new TextEncoder().encode(credentialId))) .map(b => b.toString(16).padStart(2, '0')) .join(''); const credentialData: CredentialData = { spendKey, scanKey, salt: new Uint8Array(0), // Pas de salt avec WebAuthn iterations: 0, // Pas d'itérations avec WebAuthn timestamp: Date.now() }; secureLogger.info('WebAuthn credentials generated successfully', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials', hasSpendKey: !!spendKey, hasScanKey: !!scanKey }); return credentialData; } catch (error) { secureLogger.error('Failed to generate WebAuthn credentials', error instanceof Error ? error : new Error('Unknown error'), { component: 'SecureCredentialsService', operation: 'generateSecureCredentials' }); throw error; } } /** * Génère des credentials sécurisés avec PBKDF2 (fallback) */ async generateSecureCredentialsPBKDF2( password: string, options: CredentialOptions = {} ): Promise { try { const opts = { ...this.defaultOptions, ...options }; secureLogger.info('Generating secure credentials with PBKDF2', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials', iterations: opts.iterations }); // Générer un salt aléatoire const salt = crypto.getRandomValues(new Uint8Array(opts.saltLength)); // Dériver la clé maître avec PBKDF2 const masterKey = await this.deriveMasterKey(password, salt, opts.iterations); // Générer les clés spécifiques const spendKey = await this.deriveSpendKey(masterKey, salt); const scanKey = await this.deriveScanKey(masterKey, salt); const credentialData: CredentialData = { spendKey, scanKey, salt, iterations: opts.iterations, timestamp: Date.now() }; secureLogger.info('Secure credentials generated successfully', { component: 'SecureCredentialsService', operation: 'generateSecureCredentials', hasSpendKey: !!spendKey, hasScanKey: !!scanKey }); return credentialData; } catch (error) { secureLogger.error('Failed to generate secure credentials', error as Error, { component: 'SecureCredentialsService', operation: 'generateSecureCredentials' }); throw error; } } /** * Stocke les credentials de manière sécurisée */ async storeCredentials( credentialData: CredentialData, password: string ): Promise { try { // Chiffrer les clés avec la clé maître const masterKey = await this.deriveMasterKey( password, credentialData.salt, credentialData.iterations ); const encryptedSpendKey = await this.encryptKey(credentialData.spendKey, masterKey); const encryptedScanKey = await this.encryptKey(credentialData.scanKey, masterKey); // Forcer l'utilisation de WebAuthn (pas de fallback) console.log('🚨🚨🚨 FORCING WEBAUTHN - NO FALLBACK 🚨🚨🚨'); console.log('🔥🔥🔥 NEW VERSION LOADED - NO FALLBACK ACTIVE 🔥🔥🔥'); console.log('🔍 DEBUG: Forcing WebAuthn credential creation'); console.log('🔄 VERSION: 2025-10-23-12:15 - NO FALLBACK ACTIVE'); console.log('🚀 CACHE-BUST: ' + Date.now() + ' - FORCING WEBAUTHN ONLY - RELOADED'); console.log('🌐 HTTPS VERSION: ' + window.location.href + ' - NO FALLBACK ACTIVE'); secureLogger.info('Forcing WebAuthn credential creation', { component: 'SecureCredentialsService', operation: 'webauthn_force' }); const credential = await navigator.credentials.create({ publicKey: { challenge: new Uint8Array(32), rp: { name: '4NK Secure Storage' }, user: { id: new TextEncoder().encode('4nk-user'), name: '4NK User', displayName: '4NK User' }, pubKeyCredParams: [ { type: 'public-key', alg: -7 }, // ES256 { type: 'public-key', alg: -257 } // RS256 ], authenticatorSelection: { authenticatorAttachment: 'platform', userVerification: 'required' }, timeout: 60000, attestation: 'direct' } }); secureLogger.info('WebAuthn credential created successfully', { component: 'SecureCredentialsService', operation: 'webauthn_create' }); if (credential) { // Stocker les données chiffrées dans IndexedDB await this.storeEncryptedCredentials({ encryptedSpendKey, encryptedScanKey, salt: credentialData.salt, iterations: credentialData.iterations, timestamp: credentialData.timestamp, credentialId: credential.id }); secureLogger.info('Credentials stored securely', { component: 'SecureCredentialsService', operation: 'storeCredentials', credentialId: credential.id }); } } catch (error) { secureLogger.error('Failed to store credentials', error as Error, { component: 'SecureCredentialsService', operation: 'storeCredentials' }); throw error; } } /** * Récupère et déchiffre les credentials */ async retrieveCredentials(password: string): Promise { try { // Récupérer les données chiffrées const encryptedData = await this.getEncryptedCredentials(); if (!encryptedData) { return null; } // Dériver la clé maître const masterKey = await this.deriveMasterKey( password, encryptedData.salt, encryptedData.iterations ); // Déchiffrer les clés const spendKey = await this.decryptKey(encryptedData.encryptedSpendKey, masterKey); const scanKey = await this.decryptKey(encryptedData.encryptedScanKey, masterKey); const credentialData: CredentialData = { spendKey, scanKey, salt: encryptedData.salt, iterations: encryptedData.iterations, timestamp: encryptedData.timestamp }; secureLogger.info('Credentials retrieved and decrypted', { component: 'SecureCredentialsService', operation: 'retrieveCredentials', hasSpendKey: !!spendKey, hasScanKey: !!scanKey }); return credentialData; } catch (error) { secureLogger.error('Failed to retrieve credentials', error as Error, { component: 'SecureCredentialsService', operation: 'retrieveCredentials' }); return null; } } /** * Vérifie si des credentials existent */ async hasCredentials(): Promise { try { const encryptedData = await this.getEncryptedCredentials(); return encryptedData !== null; } catch (error) { secureLogger.error('Failed to check credentials existence', error as Error, { component: 'SecureCredentialsService', operation: 'hasCredentials' }); return false; } } /** * Supprime les credentials */ async deleteCredentials(): Promise { try { await this.clearEncryptedCredentials(); secureLogger.info('Credentials deleted', { component: 'SecureCredentialsService', operation: 'deleteCredentials' }); } catch (error) { secureLogger.error('Failed to delete credentials', error as Error, { component: 'SecureCredentialsService', operation: 'deleteCredentials' }); throw error; } } /** * Dérive la clé maître avec PBKDF2 */ private async deriveMasterKey( password: string, salt: Uint8Array, iterations: number ): Promise { const keyMaterial = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey'] ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: new Uint8Array(salt), iterations: iterations, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, // Make key extractable ['encrypt', 'decrypt'] ); } /** * Dérive la clé de spend */ private async deriveSpendKey(masterKey: CryptoKey, salt: Uint8Array): Promise { const spendSalt = new Uint8Array([...salt, 0x73, 0x70, 0x65, 0x6e, 0x64]); // "spend" // Use HMAC with the master key to derive spend key const masterKeyRaw = await crypto.subtle.exportKey('raw', masterKey); const hmacKey = await crypto.subtle.importKey( 'raw', masterKeyRaw, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const spendKeyMaterial = await crypto.subtle.sign( 'HMAC', hmacKey, spendSalt ); return Array.from(new Uint8Array(spendKeyMaterial)) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); } /** * Dérive la clé de scan */ private async deriveScanKey(masterKey: CryptoKey, salt: Uint8Array): Promise { const scanSalt = new Uint8Array([...salt, 0x73, 0x63, 0x61, 0x6e]); // "scan" // Use HMAC with the master key to derive scan key const masterKeyRaw = await crypto.subtle.exportKey('raw', masterKey); const hmacKey = await crypto.subtle.importKey( 'raw', masterKeyRaw, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const scanKeyMaterial = await crypto.subtle.sign( 'HMAC', hmacKey, scanSalt ); return Array.from(new Uint8Array(scanKeyMaterial)) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); } /** * Chiffre une clé avec AES-GCM */ private async encryptKey(key: string, masterKey: CryptoKey): Promise { const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, masterKey, new TextEncoder().encode(key) ); // Combiner IV + données chiffrées const result = new Uint8Array(iv.length + encrypted.byteLength); result.set(iv); result.set(new Uint8Array(encrypted), iv.length); return result; } /** * Déchiffre une clé avec AES-GCM */ private async decryptKey(encryptedData: Uint8Array, masterKey: CryptoKey): Promise { const iv = encryptedData.slice(0, 12); const encrypted = encryptedData.slice(12); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, masterKey, encrypted ); return new TextDecoder().decode(decrypted); } /** * Stocke les credentials chiffrés dans IndexedDB */ private async storeEncryptedCredentials(data: any): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open('SecureCredentials', 1); request.onerror = () => reject(new Error('Failed to open IndexedDB')); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['credentials'], 'readwrite'); const store = transaction.objectStore('credentials'); const putRequest = store.put(data, 'secure-credentials'); putRequest.onsuccess = () => resolve(); putRequest.onerror = () => reject(new Error('Failed to store encrypted credentials')); }; request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains('credentials')) { db.createObjectStore('credentials'); } }; }); } /** * Récupère les credentials chiffrés depuis IndexedDB */ private async getEncryptedCredentials(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open('SecureCredentials', 1); request.onerror = () => reject(new Error('Failed to open IndexedDB')); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['credentials'], 'readonly'); const store = transaction.objectStore('credentials'); const getRequest = store.get('secure-credentials'); getRequest.onsuccess = () => resolve(getRequest.result); getRequest.onerror = () => reject(new Error('Failed to retrieve encrypted credentials')); }; request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains('credentials')) { db.createObjectStore('credentials'); } }; }); } /** * Supprime les credentials chiffrés */ private async clearEncryptedCredentials(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open('SecureCredentials', 1); request.onerror = () => reject(new Error('Failed to open IndexedDB')); request.onsuccess = () => { const db = request.result; const transaction = db.transaction(['credentials'], 'readwrite'); const store = transaction.objectStore('credentials'); const deleteRequest = store.delete('secure-credentials'); deleteRequest.onsuccess = () => resolve(); deleteRequest.onerror = () => reject(new Error('Failed to clear encrypted credentials')); }; }); } /** * Valide la force du mot de passe */ validatePasswordStrength(password: string): { isValid: boolean; score: number; feedback: string[]; } { const feedback: string[] = []; let score = 0; if (password.length < 8) { feedback.push('Le mot de passe doit contenir au moins 8 caractères'); } else { score += 1; } if (!/[A-Z]/.test(password)) { feedback.push('Le mot de passe doit contenir au moins une majuscule'); } else { score += 1; } if (!/[a-z]/.test(password)) { feedback.push('Le mot de passe doit contenir au moins une minuscule'); } else { score += 1; } if (!/[0-9]/.test(password)) { feedback.push('Le mot de passe doit contenir au moins un chiffre'); } else { score += 1; } if (!/[^A-Za-z0-9]/.test(password)) { feedback.push('Le mot de passe doit contenir au moins un caractère spécial'); } else { score += 1; } return { isValid: score >= 4, score, feedback }; } } // Instance singleton pour l'application export const secureCredentialsService = SecureCredentialsService.getInstance();