ihm_client/src/services/secure-credentials.service.ts
NicolasCantu b83725e112 Use create_device_from_sp_wallet instead of restore_device for key updates
**Motivations :**
- restore_device() ne met pas à jour les clés dans le SpClient interne
- create_device_from_sp_wallet() crée un nouveau device avec les clés mises à jour
- Cela force le SDK à reconnaître les nouvelles clés

**Modifications :**
- src/services/service.ts : Remplacé restoreDevice() par create_device_from_sp_wallet()
- Utilise dumpWallet() pour récupérer le JSON du SpClient avec les clés mises à jour
- Supprimé l'injection manuelle des clés dans le device en mémoire
- Fallback vers restoreDevice() si create_device_from_sp_wallet() échoue

**Pages affectées :**
- src/services/service.ts : Amélioration de ensureWalletKeysAvailable() pour forcer la reconnaissance des clés
2025-10-29 22:41:44 +01:00

1252 lines
42 KiB
TypeScript

/**
* 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<CredentialOptions> = {
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<CredentialData> {
// 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<boolean> {
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<string | null> {
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<string | null>((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<void> {
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<void>((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<string | null> {
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<string> {
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<CredentialData> {
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<CredentialData> {
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<CredentialData> {
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<CredentialData> {
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<CredentialData | null> {
// 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<CredentialData> {
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<CredentialData> {
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<void> {
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<boolean> {
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<string> {
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<boolean> {
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<void> {
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<void> {
return this.clearCredentials();
}
/**
* Demande un mot de passe à l'utilisateur
*/
private async promptForPassword(): Promise<string> {
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 = `
<h3 style="margin: 0 0 20px 0; color: #333;">🔐 Mot de passe de sécurité</h3>
<p style="margin: 0 0 15px 0; color: #666; font-size: 14px;">
Entrez un mot de passe fort pour chiffrer votre clé PBKDF2.<br>
<strong>Attention :</strong> Ce mot de passe ne sera pas sauvegardé et ne pourra pas être récupéré !
</p>
<input type="password" id="passwordInput" placeholder="Mot de passe"
style="width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; margin-bottom: 15px; font-size: 16px;">
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancelBtn" style="padding: 10px 20px; border: 1px solid #ccc; background: white; border-radius: 6px; cursor: pointer;">
Annuler
</button>
<button id="confirmBtn" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;">
Confirmer
</button>
</div>
`;
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<string> {
// 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 = `
<h3 style="margin: 0 0 20px 0; color: #333;">🔐 Mot de passe de sécurité</h3>
<p style="margin: 0 0 15px 0; color: #666; font-size: 14px;">
Entrez un mot de passe fort pour chiffrer votre clé PBKDF2.<br>
<strong>Le navigateur vous proposera de sauvegarder ce mot de passe.</strong>
</p>
<input type="password" id="passwordInput" placeholder="Mot de passe"
style="width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; margin-bottom: 15px; font-size: 16px;">
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancelBtn" style="padding: 10px 20px; border: 1px solid #ccc; background: white; border-radius: 6px; cursor: pointer;">
Annuler
</button>
<button id="confirmBtn" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;">
Confirmer
</button>
</div>
`;
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<string | null> {
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<string | null> {
// 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();