/** * 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 PBKDF2 */ async generateSecureCredentials( 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); // Vérifier si WebAuthn est disponible et si on est en HTTPS const isSecureContext = window.isSecureContext; const hasWebAuthn = navigator.credentials && navigator.credentials.create; secureLogger.info('WebAuthn availability check', { component: 'SecureCredentialsService', operation: 'webauthn_check', isSecureContext, hasWebAuthn, userAgent: navigator.userAgent, protocol: window.location.protocol }); let credential = null; if (isSecureContext && hasWebAuthn) { // Stocker dans les credentials du navigateur (HTTPS requis) try { secureLogger.info('Attempting to create WebAuthn credential', { component: 'SecureCredentialsService', operation: 'webauthn_create_attempt' }); 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' }); } catch (error) { secureLogger.warn('WebAuthn credential creation failed, using fallback', error as Error, { component: 'SecureCredentialsService', operation: 'webauthn_create' }); } } else { secureLogger.info('WebAuthn not available (HTTP context), using fallback storage', { component: 'SecureCredentialsService', operation: 'webauthn_fallback', isSecureContext, hasWebAuthn }); } 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: 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 hmacKey = await crypto.subtle.importKey( 'raw', await crypto.subtle.exportKey('raw', masterKey), { 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 hmacKey = await crypto.subtle.importKey( 'raw', await crypto.subtle.exportKey('raw', masterKey), { 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();