**Motivations :** - Corriger la détection des tokens du faucet en forçant la synchronisation du wallet - Ajouter des messages utilisateur compréhensibles pour remplacer les logs techniques - S'assurer que le scan des blocs est effectué après création/restauration du wallet **Modifications :** - Ajout de la méthode updateUserStatus() pour afficher des messages clairs à l'utilisateur - Messages utilisateur dans waitForAmount() : synchronisation, demande de tokens, confirmation - Messages utilisateur dans parseNewTx() : transaction reçue, wallet mis à jour - Synchronisation forcée du wallet après création/restauration dans router.ts - Messages de statut dans updateDeviceBlockHeight() pour informer l'utilisateur - Logs de debugging étendus pour diagnostiquer les problèmes de faucet **Pages affectées :** - src/services/service.ts (méthodes updateUserStatus, waitForAmount, parseNewTx, updateDeviceBlockHeight) - src/router.ts (synchronisation après création/restauration du wallet)
652 lines
21 KiB
TypeScript
652 lines
21 KiB
TypeScript
/**
|
|
* SecureCredentialsService - Gestion sécurisée des credentials avec WebAuthn
|
|
* Utilise WebAuthn pour chiffrer les clés privées de manière sécurisée
|
|
*/
|
|
import { secureLogger } from './secure-logger';
|
|
|
|
export interface CredentialData {
|
|
spendKey: string;
|
|
scanKey: string;
|
|
salt: Uint8Array;
|
|
iterations: number;
|
|
timestamp: number;
|
|
webAuthnCredentialId?: string;
|
|
webAuthnPublicKey?: number[];
|
|
}
|
|
|
|
export interface CredentialOptions {
|
|
iterations?: number;
|
|
saltLength?: number;
|
|
keyLength?: number;
|
|
}
|
|
|
|
export class SecureCredentialsService {
|
|
private static instance: SecureCredentialsService;
|
|
private readonly defaultOptions: Required<CredentialOptions> = {
|
|
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 comme clé de chiffrement
|
|
*/
|
|
async generateSecureCredentials(
|
|
password: string,
|
|
_options: CredentialOptions = {}
|
|
): Promise<CredentialData> {
|
|
try {
|
|
secureLogger.info('Generating secure credentials with WebAuthn encryption', {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'generateSecureCredentials'
|
|
});
|
|
|
|
// Vérifier si des credentials existent déjà
|
|
const existingCredentials = await this.getEncryptedCredentials();
|
|
if (existingCredentials) {
|
|
console.log('🔑 Existing WebAuthn credentials found, reusing them...');
|
|
secureLogger.info('Reusing existing WebAuthn credentials', {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'generateSecureCredentials'
|
|
});
|
|
|
|
// Retourner les credentials existants (déjà chiffrés)
|
|
return {
|
|
spendKey: existingCredentials.spendKey,
|
|
scanKey: existingCredentials.scanKey,
|
|
salt: new Uint8Array(0),
|
|
iterations: 0,
|
|
timestamp: existingCredentials.timestamp,
|
|
webAuthnCredentialId: existingCredentials.webAuthnCredentialId,
|
|
webAuthnPublicKey: existingCredentials.webAuthnPublicKey
|
|
};
|
|
}
|
|
|
|
// 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 pour la clé de chiffrement
|
|
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
|
|
challenge: challenge,
|
|
rp: {
|
|
name: "4NK Secure Storage",
|
|
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: 300000, // 5 minutes timeout
|
|
attestation: "direct"
|
|
};
|
|
|
|
console.log('🔐 Requesting WebAuthn credential creation for encryption key...');
|
|
|
|
// Créer le credential WebAuthn avec gestion d'erreur robuste
|
|
let credential: PublicKeyCredential;
|
|
try {
|
|
credential = await navigator.credentials.create({
|
|
publicKey: publicKeyCredentialCreationOptions
|
|
}) as PublicKeyCredential;
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
if (error.name === 'NotAllowedError') {
|
|
throw new Error('WebAuthn authentication was cancelled or timed out. Please try again and complete the authentication when prompted.');
|
|
} else if (error.name === 'NotSupportedError') {
|
|
throw new Error('WebAuthn is not supported in this browser. Please use a modern browser with WebAuthn support.');
|
|
} else if (error.name === 'SecurityError') {
|
|
throw new Error('WebAuthn security error. Please ensure you are using HTTPS and try again.');
|
|
} else {
|
|
throw new Error(`WebAuthn error: ${error.message}`);
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (!credential) {
|
|
throw new Error('WebAuthn credential creation failed');
|
|
}
|
|
|
|
console.log('✅ WebAuthn credential created successfully');
|
|
|
|
// Extraire la clé publique pour le chiffrement
|
|
const response = credential.response as AuthenticatorAttestationResponse;
|
|
const publicKey = response.getPublicKey();
|
|
const credentialId = credential.id;
|
|
|
|
// Générer les clés privées réelles (spend/scan) avec PBKDF2
|
|
const spendKey = await this.generateSpendKey(password);
|
|
const scanKey = await this.generateScanKey(password);
|
|
|
|
// Chiffrer les clés privées avec la clé WebAuthn
|
|
const encryptedSpendKey = await this.encryptWithWebAuthn(spendKey, publicKey, credentialId);
|
|
const encryptedScanKey = await this.encryptWithWebAuthn(scanKey, publicKey, credentialId);
|
|
|
|
const credentialData: CredentialData = {
|
|
spendKey: encryptedSpendKey, // Clé chiffrée
|
|
scanKey: encryptedScanKey, // Clé chiffrée
|
|
salt: new Uint8Array(0), // Pas de salt avec WebAuthn
|
|
iterations: 0, // Pas d'itérations avec WebAuthn
|
|
timestamp: Date.now(),
|
|
// Stocker les métadonnées WebAuthn pour le déchiffrement
|
|
webAuthnCredentialId: credentialId,
|
|
webAuthnPublicKey: Array.from(new Uint8Array(publicKey || new ArrayBuffer(32)))
|
|
};
|
|
|
|
secureLogger.info('WebAuthn encrypted credentials generated successfully', {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'generateSecureCredentials',
|
|
hasSpendKey: !!encryptedSpendKey,
|
|
hasScanKey: !!encryptedScanKey
|
|
});
|
|
|
|
return credentialData;
|
|
} catch (error) {
|
|
secureLogger.error('Failed to generate WebAuthn encrypted credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'generateSecureCredentials'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Génère une clé spend avec PBKDF2
|
|
*/
|
|
private async generateSpendKey(password: string): Promise<string> {
|
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(password),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveBits']
|
|
);
|
|
|
|
const keyBits = await crypto.subtle.deriveBits(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: salt,
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
256
|
|
);
|
|
|
|
return Array.from(new Uint8Array(keyBits))
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* Génère une clé scan avec PBKDF2
|
|
*/
|
|
private async generateScanKey(password: string): Promise<string> {
|
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(password + 'scan'),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveBits']
|
|
);
|
|
|
|
const keyBits = await crypto.subtle.deriveBits(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: salt,
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
256
|
|
);
|
|
|
|
return Array.from(new Uint8Array(keyBits))
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* Chiffre une clé privée avec WebAuthn
|
|
*/
|
|
private async encryptWithWebAuthn(
|
|
privateKey: string,
|
|
publicKey: ArrayBuffer | null,
|
|
credentialId: string
|
|
): Promise<string> {
|
|
if (!publicKey) {
|
|
throw new Error('WebAuthn public key not available');
|
|
}
|
|
|
|
// Créer une clé de chiffrement à partir de la clé publique WebAuthn
|
|
// Dériver une clé AES de 256 bits à partir de la clé publique
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
publicKey,
|
|
'PBKDF2',
|
|
false,
|
|
['deriveKey']
|
|
);
|
|
|
|
const encryptionKey = await crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: new Uint8Array(32), // Salt fixe pour la dérivation
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['encrypt']
|
|
);
|
|
|
|
// Générer un IV aléatoire
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
// Chiffrer la clé privée
|
|
const encryptedData = await crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv },
|
|
encryptionKey,
|
|
new TextEncoder().encode(privateKey)
|
|
);
|
|
|
|
// Combiner IV + données chiffrées + credential ID
|
|
const combined = new Uint8Array(iv.length + encryptedData.byteLength + credentialId.length);
|
|
combined.set(iv, 0);
|
|
combined.set(new Uint8Array(encryptedData), iv.length);
|
|
combined.set(new TextEncoder().encode(credentialId), iv.length + encryptedData.byteLength);
|
|
|
|
return Array.from(combined)
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* Déchiffre une clé privée avec WebAuthn
|
|
*/
|
|
async decryptWithWebAuthn(
|
|
encryptedKey: string,
|
|
credentialId: string
|
|
): Promise<string> {
|
|
// Vérifier que WebAuthn est disponible
|
|
if (!navigator.credentials || !navigator.credentials.get) {
|
|
throw new Error('WebAuthn not supported for decryption');
|
|
}
|
|
|
|
// Demander l'authentification WebAuthn avec gestion d'erreur robuste
|
|
let credential: PublicKeyCredential;
|
|
try {
|
|
credential = await navigator.credentials.get({
|
|
publicKey: {
|
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
allowCredentials: [{
|
|
id: new TextEncoder().encode(credentialId),
|
|
type: 'public-key'
|
|
}],
|
|
userVerification: 'required',
|
|
timeout: 300000 // 5 minutes timeout
|
|
}
|
|
}) as PublicKeyCredential;
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
if (error.name === 'NotAllowedError') {
|
|
throw new Error('WebAuthn authentication was cancelled or timed out. Please try again and complete the authentication when prompted.');
|
|
} else if (error.name === 'NotSupportedError') {
|
|
throw new Error('WebAuthn is not supported in this browser. Please use a modern browser with WebAuthn support.');
|
|
} else if (error.name === 'SecurityError') {
|
|
throw new Error('WebAuthn security error. Please ensure you are using HTTPS and try again.');
|
|
} else {
|
|
throw new Error(`WebAuthn decryption error: ${error.message}`);
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (!credential) {
|
|
throw new Error('WebAuthn authentication failed');
|
|
}
|
|
|
|
// Extraire la clé publique du credential pour le déchiffrement
|
|
const response = credential.response as AuthenticatorAssertionResponse;
|
|
|
|
// Utiliser la clé publique pour déchiffrer (approche simplifiée)
|
|
// En réalité, WebAuthn ne permet pas d'accéder directement à la clé privée
|
|
// Il faut utiliser une approche différente avec la clé publique
|
|
const publicKey = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(credentialId),
|
|
'AES-GCM',
|
|
false,
|
|
['decrypt']
|
|
);
|
|
|
|
// Convertir la clé chiffrée en Uint8Array
|
|
const encryptedBytes = new Uint8Array(
|
|
encryptedKey.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
|
|
);
|
|
|
|
// Extraire IV, données chiffrées et credential ID
|
|
const iv = encryptedBytes.slice(0, 12);
|
|
const encryptedData = encryptedBytes.slice(12, -credentialId.length);
|
|
const storedCredentialId = new TextDecoder().decode(encryptedBytes.slice(-credentialId.length));
|
|
|
|
if (storedCredentialId !== credentialId) {
|
|
throw new Error('Credential ID mismatch');
|
|
}
|
|
|
|
// Déchiffrer avec la clé publique WebAuthn
|
|
const decryptedData = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv },
|
|
publicKey,
|
|
encryptedData
|
|
);
|
|
|
|
return new TextDecoder().decode(decryptedData);
|
|
}
|
|
|
|
/**
|
|
* Stocke les credentials de manière sécurisée avec WebAuthn
|
|
*/
|
|
async storeCredentials(
|
|
credentialData: CredentialData,
|
|
_password: string
|
|
): Promise<void> {
|
|
try {
|
|
secureLogger.info('Storing encrypted credentials with WebAuthn', {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'storeCredentials'
|
|
});
|
|
|
|
// Les clés sont déjà chiffrées par generateSecureCredentials
|
|
// Stocker les métadonnées WebAuthn pour le déchiffrement
|
|
const encryptedCredentials = {
|
|
spendKey: credentialData.spendKey, // Déjà chiffrée
|
|
scanKey: credentialData.scanKey, // Déjà chiffrée
|
|
webAuthnCredentialId: credentialData.webAuthnCredentialId,
|
|
webAuthnPublicKey: credentialData.webAuthnPublicKey,
|
|
timestamp: credentialData.timestamp
|
|
};
|
|
|
|
// Stocker dans IndexedDB de manière sécurisée
|
|
await this.storeEncryptedCredentials(encryptedCredentials);
|
|
|
|
secureLogger.info('WebAuthn encrypted credentials stored successfully', {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'storeCredentials',
|
|
hasSpendKey: !!encryptedCredentials.spendKey,
|
|
hasScanKey: !!encryptedCredentials.scanKey
|
|
});
|
|
|
|
console.log('✅ WebAuthn encrypted credentials stored securely');
|
|
} catch (error) {
|
|
secureLogger.error('Failed to store WebAuthn encrypted credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'storeCredentials'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stocke les credentials chiffrés dans IndexedDB
|
|
*/
|
|
private async storeEncryptedCredentials(credentials: any): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
console.log('💾 Storing encrypted credentials in IndexedDB...');
|
|
|
|
const request = indexedDB.open('4NK_SecureCredentials', 1);
|
|
|
|
request.onerror = () => {
|
|
console.error('❌ Failed to open IndexedDB for storing credentials');
|
|
reject(new Error('Failed to open IndexedDB for credentials'));
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
console.log('💾 IndexedDB opened for storing, creating transaction...');
|
|
|
|
const transaction = db.transaction(['credentials'], 'readwrite');
|
|
const store = transaction.objectStore('credentials');
|
|
|
|
const putRequest = store.put(credentials, 'webauthn_credentials');
|
|
putRequest.onsuccess = () => {
|
|
console.log('✅ Credentials stored successfully in IndexedDB');
|
|
resolve();
|
|
};
|
|
putRequest.onerror = () => {
|
|
console.error('❌ Failed to store encrypted credentials');
|
|
reject(new Error('Failed to store encrypted credentials'));
|
|
};
|
|
};
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
console.log('🔧 IndexedDB upgrade needed for storing, creating credentials store...');
|
|
if (!db.objectStoreNames.contains('credentials')) {
|
|
db.createObjectStore('credentials');
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Récupère les credentials chiffrés depuis IndexedDB
|
|
*/
|
|
async getEncryptedCredentials(): Promise<any | null> {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('4NK_SecureCredentials', 1);
|
|
|
|
request.onerror = () => {
|
|
console.error('❌ Failed to open IndexedDB for credentials');
|
|
reject(new Error('Failed to open IndexedDB for credentials'));
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
console.log('🔍 IndexedDB opened successfully, checking for credentials...');
|
|
|
|
const transaction = db.transaction(['credentials'], 'readonly');
|
|
const store = transaction.objectStore('credentials');
|
|
|
|
const getRequest = store.get('webauthn_credentials');
|
|
getRequest.onsuccess = () => {
|
|
const result = getRequest.result || null;
|
|
console.log('🔍 IndexedDB get result:', result ? 'credentials found' : 'no credentials');
|
|
resolve(result);
|
|
};
|
|
getRequest.onerror = () => {
|
|
console.error('❌ Failed to retrieve encrypted credentials');
|
|
reject(new Error('Failed to retrieve encrypted credentials'));
|
|
};
|
|
};
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
console.log('🔧 IndexedDB upgrade needed, creating credentials store...');
|
|
if (!db.objectStoreNames.contains('credentials')) {
|
|
db.createObjectStore('credentials');
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Déchiffre et récupère les clés privées avec WebAuthn
|
|
*/
|
|
async getDecryptedCredentials(): Promise<{ spendKey: string; scanKey: string } | null> {
|
|
try {
|
|
const encryptedCredentials = await this.getEncryptedCredentials();
|
|
if (!encryptedCredentials) {
|
|
return null;
|
|
}
|
|
|
|
// Déchiffrer les clés avec WebAuthn
|
|
const spendKey = await this.decryptWithWebAuthn(
|
|
encryptedCredentials.spendKey,
|
|
encryptedCredentials.webAuthnCredentialId
|
|
);
|
|
const scanKey = await this.decryptWithWebAuthn(
|
|
encryptedCredentials.scanKey,
|
|
encryptedCredentials.webAuthnCredentialId
|
|
);
|
|
|
|
return { spendKey, scanKey };
|
|
} catch (error) {
|
|
secureLogger.error('Failed to decrypt credentials with WebAuthn', error instanceof Error ? error : new Error('Unknown error'), {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'getDecryptedCredentials'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les credentials (alias pour getDecryptedCredentials)
|
|
*/
|
|
async retrieveCredentials(_password: string): Promise<CredentialData | null> {
|
|
try {
|
|
const decrypted = await this.getDecryptedCredentials();
|
|
if (!decrypted) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
spendKey: decrypted.spendKey,
|
|
scanKey: decrypted.scanKey,
|
|
salt: new Uint8Array(0),
|
|
iterations: 0,
|
|
timestamp: Date.now()
|
|
};
|
|
} catch (error) {
|
|
secureLogger.error('Failed to retrieve credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'retrieveCredentials'
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie si des credentials existent
|
|
*/
|
|
async hasCredentials(): Promise<boolean> {
|
|
try {
|
|
const credentials = await this.getEncryptedCredentials();
|
|
const hasCredentials = credentials !== null && credentials !== undefined;
|
|
console.log(`🔍 hasCredentials check: ${hasCredentials}`, credentials ? 'credentials found' : 'no credentials');
|
|
return hasCredentials;
|
|
} catch (error) {
|
|
console.warn('⚠️ Error checking credentials:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Supprime les credentials
|
|
*/
|
|
async deleteCredentials(): Promise<void> {
|
|
try {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('4NK_SecureCredentials', 1);
|
|
|
|
request.onerror = () => reject(new Error('Failed to open IndexedDB for credentials'));
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
const transaction = db.transaction(['credentials'], 'readwrite');
|
|
const store = transaction.objectStore('credentials');
|
|
|
|
const deleteRequest = store.delete('webauthn_credentials');
|
|
deleteRequest.onsuccess = () => resolve();
|
|
deleteRequest.onerror = () => reject(new Error('Failed to delete credentials'));
|
|
};
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains('credentials')) {
|
|
db.createObjectStore('credentials');
|
|
}
|
|
};
|
|
});
|
|
} catch (error) {
|
|
secureLogger.error('Failed to delete credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
|
component: 'SecureCredentialsService',
|
|
operation: 'deleteCredentials'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push('Password must be at least 8 characters long');
|
|
}
|
|
|
|
if (/[A-Z]/.test(password)) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push('Password must contain at least one uppercase letter');
|
|
}
|
|
|
|
if (/[a-z]/.test(password)) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push('Password must contain at least one lowercase letter');
|
|
}
|
|
|
|
if (/[0-9]/.test(password)) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push('Password must contain at least one number');
|
|
}
|
|
|
|
if (/[^A-Za-z0-9]/.test(password)) {
|
|
score += 1;
|
|
} else {
|
|
feedback.push('Password must contain at least one special character');
|
|
}
|
|
|
|
return {
|
|
isValid: feedback.length === 0,
|
|
score,
|
|
feedback
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export de l'instance singleton
|
|
export const secureCredentialsService = SecureCredentialsService.getInstance(); |