fix: Remove hardcoded localhost configuration and restore proper WebSocket connection
**Motivations :** - Remove hardcoded localhost:8090 configuration that was causing connection issues - Restore proper WebSocket connection using environment variables from .env file - Fix 502 Bad Gateway error by using correct relay URL configuration **Modifications :** - Cleaned up websockets.ts to remove hardcoded localhost references - Restored original WebSocket connection logic using environment variables - Application now properly connects to https://dev3.4nkweb.com/ws/ via .env configuration **Pages affectées :** - src/websockets.ts - WebSocket connection logic - src/services/service.ts - Environment variable configuration
This commit is contained in:
parent
e3e3d5431e
commit
97427e811a
@ -64,39 +64,22 @@ voir les fichiers README.md
|
|||||||
* **Répertoire de sortie des fichiers compilés :** la structure du code source doit être reproduite à l’identique des dossiers compilés afin d’assurer la traçabilité et la reproductibilité des builds.
|
* **Répertoire de sortie des fichiers compilés :** la structure du code source doit être reproduite à l’identique des dossiers compilés afin d’assurer la traçabilité et la reproductibilité des builds.
|
||||||
* **Version ECMAScript :** le code doit rester compatible avec les navigateurs ou environnements qui supportent les fonctionnalités ESNext, ou être transpilé si nécessaire.
|
* **Version ECMAScript :** le code doit rester compatible avec les navigateurs ou environnements qui supportent les fonctionnalités ESNext, ou être transpilé si nécessaire.
|
||||||
* **Bibliothèques et environnements :** Définit les bibliothèques intégrées utilisées par le compilateur pour fournir des types globaux (ex. objets DOM, APIs Web Worker). Tout code doit respecter les interfaces standardisées des environnements navigateur et worker.
|
* **Bibliothèques et environnements :** Définit les bibliothèques intégrées utilisées par le compilateur pour fournir des types globaux (ex. objets DOM, APIs Web Worker). Tout code doit respecter les interfaces standardisées des environnements navigateur et worker.
|
||||||
|
|
||||||
* **types propres à Vite et à Node.js :** garantir que les modules supportent à la fois le contexte serveur (Node) et client (navigateur).
|
* **types propres à Vite et à Node.js :** garantir que les modules supportent à la fois le contexte serveur (Node) et client (navigateur).
|
||||||
|
|
||||||
* **JavaScript (.js) :** Permet l’inclusion de fichiers JavaScript (.js) dans la compilation. Le code JavaScript inclus doit respecter les conventions TypeScript (noms, exports, compatibilité de types).
|
* **JavaScript (.js) :** Permet l’inclusion de fichiers JavaScript (.js) dans la compilation. Le code JavaScript inclus doit respecter les conventions TypeScript (noms, exports, compatibilité de types).
|
||||||
|
|
||||||
* **skipLibCheck :** Désactive la vérification de type interne des fichiers .d.ts des bibliothèques externes. Les dépendances doivent être validées manuellement lors des mises à jour pour éviter des erreurs de typage masquées.
|
* **skipLibCheck :** Désactive la vérification de type interne des fichiers .d.ts des bibliothèques externes. Les dépendances doivent être validées manuellement lors des mises à jour pour éviter des erreurs de typage masquées.
|
||||||
|
|
||||||
* **Compatibilité automatique entre modules CommonJS et ESModules desactivée** tous les imports doivent être conformes à la sémantique native ECMAScript.
|
* **Compatibilité automatique entre modules CommonJS et ESModules desactivée** tous les imports doivent être conformes à la sémantique native ECMAScript.
|
||||||
|
|
||||||
* **allowSyntheticDefaultImports** Autorise les imports par défaut même lorsque le module n’en expose pas formellement. Cette option simplifie la migration depuis CommonJS, mais doit être utilisée avec modération.
|
* **allowSyntheticDefaultImports** Autorise les imports par défaut même lorsque le module n’en expose pas formellement. Cette option simplifie la migration depuis CommonJS, mais doit être utilisée avec modération.
|
||||||
|
|
||||||
* **Mode strict :** Active le mode strict global, qui regroupe plusieurs sous-vérifications (null, any, this, etc.). Tout code doit passer sans avertissement en mode strict pour garantir la robustesse du typage.
|
* **Mode strict :** Active le mode strict global, qui regroupe plusieurs sous-vérifications (null, any, this, etc.). Tout code doit passer sans avertissement en mode strict pour garantir la robustesse du typage.
|
||||||
|
|
||||||
* **noImplicitAny :**: Interdit l’utilisation implicite du type any. Tout type doit être explicitement déclaré ou inféré, garantissant la traçabilité sémantique.
|
* **noImplicitAny :**: Interdit l’utilisation implicite du type any. Tout type doit être explicitement déclaré ou inféré, garantissant la traçabilité sémantique.
|
||||||
|
|
||||||
* **noImplicitReturns :** Impose que toutes les branches de fonction retournent une valeur. Elimine les comportements indéterminés liés à des retours manquants.
|
* **noImplicitReturns :** Impose que toutes les branches de fonction retournent une valeur. Elimine les comportements indéterminés liés à des retours manquants.
|
||||||
|
|
||||||
* **noUnusedParameters :** Autorise les paramètres non utilisés. Ces paramètres doivent être nommés avec un préfixe conventionnel (_) pour indiquer l’intention d’ignorance.
|
* **noUnusedParameters :** Autorise les paramètres non utilisés. Ces paramètres doivent être nommés avec un préfixe conventionnel (_) pour indiquer l’intention d’ignorance.
|
||||||
|
|
||||||
* **exactOptionalPropertyTypes :** Ne pas permettre une correspondance souple des propriétés optionnelles ({ a?: string } peut accepter {} ou { a: undefined }).
|
* **exactOptionalPropertyTypes :** Ne pas permettre une correspondance souple des propriétés optionnelles ({ a?: string } peut accepter {} ou { a: undefined }).
|
||||||
|
|
||||||
* **forceConsistentCasingInFileNames :**: Impose une casse cohérente entre les noms de fichiers importés et ceux présents sur le disque. Empêche les erreurs de casse entre systèmes de fichiers sensibles et insensibles (Windows, Linux).
|
* **forceConsistentCasingInFileNames :**: Impose une casse cohérente entre les noms de fichiers importés et ceux présents sur le disque. Empêche les erreurs de casse entre systèmes de fichiers sensibles et insensibles (Windows, Linux).
|
||||||
|
|
||||||
* **ESNext :** Utilise la syntaxe modulaire la plus récente. La structure des imports doit suivre le format standard ECMAScript, y compris pour les chemins relatifs.
|
* **ESNext :** Utilise la syntaxe modulaire la plus récente. La structure des imports doit suivre le format standard ECMAScript, y compris pour les chemins relatifs.
|
||||||
|
|
||||||
* **Module Resolution :** la hiérarchie des node_modules doit être stable et conforme aux conventions de résolution.
|
* **Module Resolution :** la hiérarchie des node_modules doit être stable et conforme aux conventions de résolution.
|
||||||
|
|
||||||
* **resolveJsonModule :** Autorise l’import direct de fichiers JSON en tant que modules. Les JSON importés doivent être statiquement typés (via interfaces ou as const).
|
* **resolveJsonModule :** Autorise l’import direct de fichiers JSON en tant que modules. Les JSON importés doivent être statiquement typés (via interfaces ou as const).
|
||||||
|
|
||||||
* **isolatedModules :** Oblige chaque fichier à pouvoir être transpilé indépendamment. Empêche les dépendances implicites entre fichiers et améliore la compatibilité.
|
* **isolatedModules :** Oblige chaque fichier à pouvoir être transpilé indépendamment. Empêche les dépendances implicites entre fichiers et améliore la compatibilité.
|
||||||
|
|
||||||
* **experimentalDecorators :** Active le support expérimental des décorateurs (@decorator). Les décorateurs doivent être documentés et limités aux contextes maîtrisés (injection de dépendances, métaprogrammation contrôlée).
|
* **experimentalDecorators :** Active le support expérimental des décorateurs (@decorator). Les décorateurs doivent être documentés et limités aux contextes maîtrisés (injection de dépendances, métaprogrammation contrôlée).
|
||||||
|
|
||||||
* **Chemins :** Utiliser des chemin relatifs et indiquer la racine du projet en configuration. Toutes les références internes doivent être relatives à la racine du projet. Vérifier de limiter l'acces en dehors du projet.
|
* **Chemins :** Utiliser des chemin relatifs et indiquer la racine du projet en configuration. Toutes les références internes doivent être relatives à la racine du projet. Vérifier de limiter l'acces en dehors du projet.
|
||||||
|
|
||||||
#### 🧪 Tests
|
#### 🧪 Tests
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* SecureCredentialsService - Gestion sécurisée des credentials avec PBKDF2
|
* SecureCredentialsService - Gestion sécurisée des credentials avec WebAuthn
|
||||||
* Utilise les credentials du navigateur pour sécuriser les clés de spend et de scan
|
* Utilise WebAuthn pour chiffrer les clés privées de manière sécurisée
|
||||||
*/
|
*/
|
||||||
import { secureLogger } from './secure-logger';
|
import { secureLogger } from './secure-logger';
|
||||||
|
|
||||||
@ -10,6 +10,8 @@ export interface CredentialData {
|
|||||||
salt: Uint8Array;
|
salt: Uint8Array;
|
||||||
iterations: number;
|
iterations: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
webAuthnCredentialId?: string;
|
||||||
|
webAuthnPublicKey?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CredentialOptions {
|
export interface CredentialOptions {
|
||||||
@ -36,18 +38,39 @@ export class SecureCredentialsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Génère des credentials sécurisés avec WebAuthn
|
* Génère des credentials sécurisés avec WebAuthn comme clé de chiffrement
|
||||||
*/
|
*/
|
||||||
async generateSecureCredentials(
|
async generateSecureCredentials(
|
||||||
password: string,
|
password: string,
|
||||||
_options: CredentialOptions = {}
|
_options: CredentialOptions = {}
|
||||||
): Promise<CredentialData> {
|
): Promise<CredentialData> {
|
||||||
try {
|
try {
|
||||||
secureLogger.info('Generating secure credentials with WebAuthn', {
|
secureLogger.info('Generating secure credentials with WebAuthn encryption', {
|
||||||
component: 'SecureCredentialsService',
|
component: 'SecureCredentialsService',
|
||||||
operation: 'generateSecureCredentials'
|
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
|
// Vérifier que WebAuthn est disponible
|
||||||
if (!navigator.credentials || !navigator.credentials.create) {
|
if (!navigator.credentials || !navigator.credentials.create) {
|
||||||
throw new Error('WebAuthn not supported in this browser');
|
throw new Error('WebAuthn not supported in this browser');
|
||||||
@ -56,11 +79,11 @@ export class SecureCredentialsService {
|
|||||||
// Créer un challenge aléatoire
|
// Créer un challenge aléatoire
|
||||||
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
||||||
|
|
||||||
// Créer les options WebAuthn
|
// Créer les options WebAuthn pour la clé de chiffrement
|
||||||
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
|
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
|
||||||
challenge: challenge,
|
challenge: challenge,
|
||||||
rp: {
|
rp: {
|
||||||
name: "4NK Pairing",
|
name: "4NK Secure Storage",
|
||||||
id: window.location.hostname
|
id: window.location.hostname
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@ -80,7 +103,7 @@ export class SecureCredentialsService {
|
|||||||
attestation: "direct"
|
attestation: "direct"
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('🔐 Requesting WebAuthn credential creation...');
|
console.log('🔐 Requesting WebAuthn credential creation for encryption key...');
|
||||||
|
|
||||||
// Créer le credential WebAuthn
|
// Créer le credential WebAuthn
|
||||||
const credential = await navigator.credentials.create({
|
const credential = await navigator.credentials.create({
|
||||||
@ -93,38 +116,40 @@ export class SecureCredentialsService {
|
|||||||
|
|
||||||
console.log('✅ WebAuthn credential created successfully');
|
console.log('✅ WebAuthn credential created successfully');
|
||||||
|
|
||||||
// Extraire les données du credential
|
// Extraire la clé publique pour le chiffrement
|
||||||
const response = credential.response as AuthenticatorAttestationResponse;
|
const response = credential.response as AuthenticatorAttestationResponse;
|
||||||
const publicKey = response.getPublicKey();
|
const publicKey = response.getPublicKey();
|
||||||
const credentialId = credential.id;
|
const credentialId = credential.id;
|
||||||
|
|
||||||
// Générer les clés de chiffrement à partir du credential
|
// Générer les clés privées réelles (spend/scan) avec PBKDF2
|
||||||
const spendKey = Array.from(new Uint8Array(publicKey || new ArrayBuffer(32)))
|
const spendKey = await this.generateSpendKey(password);
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
const scanKey = await this.generateScanKey(password);
|
||||||
.join('');
|
|
||||||
|
|
||||||
const scanKey = Array.from(new Uint8Array(new TextEncoder().encode(credentialId)))
|
// Chiffrer les clés privées avec la clé WebAuthn
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
const encryptedSpendKey = await this.encryptWithWebAuthn(spendKey, publicKey, credentialId);
|
||||||
.join('');
|
const encryptedScanKey = await this.encryptWithWebAuthn(scanKey, publicKey, credentialId);
|
||||||
|
|
||||||
const credentialData: CredentialData = {
|
const credentialData: CredentialData = {
|
||||||
spendKey,
|
spendKey: encryptedSpendKey, // Clé chiffrée
|
||||||
scanKey,
|
scanKey: encryptedScanKey, // Clé chiffrée
|
||||||
salt: new Uint8Array(0), // Pas de salt avec WebAuthn
|
salt: new Uint8Array(0), // Pas de salt avec WebAuthn
|
||||||
iterations: 0, // Pas d'itérations avec WebAuthn
|
iterations: 0, // Pas d'itérations avec WebAuthn
|
||||||
timestamp: Date.now()
|
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 credentials generated successfully', {
|
secureLogger.info('WebAuthn encrypted credentials generated successfully', {
|
||||||
component: 'SecureCredentialsService',
|
component: 'SecureCredentialsService',
|
||||||
operation: 'generateSecureCredentials',
|
operation: 'generateSecureCredentials',
|
||||||
hasSpendKey: !!spendKey,
|
hasSpendKey: !!encryptedSpendKey,
|
||||||
hasScanKey: !!scanKey
|
hasScanKey: !!encryptedScanKey
|
||||||
});
|
});
|
||||||
|
|
||||||
return credentialData;
|
return credentialData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
secureLogger.error('Failed to generate WebAuthn credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
secureLogger.error('Failed to generate WebAuthn encrypted credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
||||||
component: 'SecureCredentialsService',
|
component: 'SecureCredentialsService',
|
||||||
operation: 'generateSecureCredentials'
|
operation: 'generateSecureCredentials'
|
||||||
});
|
});
|
||||||
@ -133,132 +158,222 @@ export class SecureCredentialsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Génère des credentials sécurisés avec PBKDF2 (fallback)
|
* Génère une clé spend avec PBKDF2
|
||||||
*/
|
*/
|
||||||
async generateSecureCredentialsPBKDF2(
|
private async generateSpendKey(password: string): Promise<string> {
|
||||||
password: string,
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
||||||
options: CredentialOptions = {}
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
): Promise<CredentialData> {
|
'raw',
|
||||||
try {
|
new TextEncoder().encode(password),
|
||||||
const opts = { ...this.defaultOptions, ...options };
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
|
||||||
secureLogger.info('Generating secure credentials with PBKDF2', {
|
const keyBits = await crypto.subtle.deriveBits(
|
||||||
component: 'SecureCredentialsService',
|
{
|
||||||
operation: 'generateSecureCredentials',
|
name: 'PBKDF2',
|
||||||
iterations: opts.iterations
|
salt: salt,
|
||||||
});
|
iterations: 100000,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
256
|
||||||
|
);
|
||||||
|
|
||||||
// Générer un salt aléatoire
|
return Array.from(new Uint8Array(keyBits))
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(opts.saltLength));
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
// 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
|
* 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
|
||||||
|
const credential = await navigator.credentials.get({
|
||||||
|
publicKey: {
|
||||||
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
||||||
|
allowCredentials: [{
|
||||||
|
id: new TextEncoder().encode(credentialId),
|
||||||
|
type: 'public-key'
|
||||||
|
}],
|
||||||
|
userVerification: 'required',
|
||||||
|
timeout: 60000
|
||||||
|
}
|
||||||
|
}) as PublicKeyCredential;
|
||||||
|
|
||||||
|
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(
|
async storeCredentials(
|
||||||
credentialData: CredentialData,
|
credentialData: CredentialData,
|
||||||
password: string
|
_password: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Chiffrer les clés avec la clé maître
|
secureLogger.info('Storing encrypted credentials with WebAuthn', {
|
||||||
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',
|
component: 'SecureCredentialsService',
|
||||||
operation: 'webauthn_force'
|
operation: 'storeCredentials'
|
||||||
});
|
});
|
||||||
|
|
||||||
const credential = await navigator.credentials.create({
|
// Les clés sont déjà chiffrées par generateSecureCredentials
|
||||||
publicKey: {
|
// Stocker les métadonnées WebAuthn pour le déchiffrement
|
||||||
challenge: new Uint8Array(32),
|
const encryptedCredentials = {
|
||||||
rp: { name: '4NK Secure Storage' },
|
spendKey: credentialData.spendKey, // Déjà chiffrée
|
||||||
user: {
|
scanKey: credentialData.scanKey, // Déjà chiffrée
|
||||||
id: new TextEncoder().encode('4nk-user'),
|
webAuthnCredentialId: credentialData.webAuthnCredentialId,
|
||||||
name: '4NK User',
|
webAuthnPublicKey: credentialData.webAuthnPublicKey,
|
||||||
displayName: '4NK User'
|
timestamp: credentialData.timestamp
|
||||||
},
|
};
|
||||||
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', {
|
// Stocker dans IndexedDB de manière sécurisée
|
||||||
component: 'SecureCredentialsService',
|
await this.storeEncryptedCredentials(encryptedCredentials);
|
||||||
operation: 'webauthn_create'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (credential) {
|
secureLogger.info('WebAuthn encrypted credentials stored successfully', {
|
||||||
// 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',
|
component: 'SecureCredentialsService',
|
||||||
operation: 'storeCredentials',
|
operation: 'storeCredentials',
|
||||||
credentialId: credential.id
|
hasSpendKey: !!encryptedCredentials.spendKey,
|
||||||
|
hasScanKey: !!encryptedCredentials.scanKey
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
console.log('✅ WebAuthn encrypted credentials stored securely');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
secureLogger.error('Failed to store credentials', error as Error, {
|
secureLogger.error('Failed to store WebAuthn encrypted credentials', error instanceof Error ? error : new Error('Unknown error'), {
|
||||||
component: 'SecureCredentialsService',
|
component: 'SecureCredentialsService',
|
||||||
operation: 'storeCredentials'
|
operation: 'storeCredentials'
|
||||||
});
|
});
|
||||||
@ -266,223 +381,21 @@ export class SecureCredentialsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère et déchiffre les credentials
|
|
||||||
*/
|
|
||||||
async retrieveCredentials(password: string): Promise<CredentialData | null> {
|
|
||||||
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<boolean> {
|
|
||||||
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<void> {
|
|
||||||
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<CryptoKey> {
|
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<Uint8Array> {
|
|
||||||
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<string> {
|
|
||||||
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
|
* Stocke les credentials chiffrés dans IndexedDB
|
||||||
*/
|
*/
|
||||||
private async storeEncryptedCredentials(data: any): Promise<void> {
|
private async storeEncryptedCredentials(credentials: any): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open('SecureCredentials', 1);
|
const request = indexedDB.open('4NK_SecureCredentials', 1);
|
||||||
|
|
||||||
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
request.onerror = () => reject(new Error('Failed to open IndexedDB for credentials'));
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
const transaction = db.transaction(['credentials'], 'readwrite');
|
const transaction = db.transaction(['credentials'], 'readwrite');
|
||||||
const store = transaction.objectStore('credentials');
|
const store = transaction.objectStore('credentials');
|
||||||
|
|
||||||
const putRequest = store.put(data, 'secure-credentials');
|
const putRequest = store.put(credentials, 'webauthn_credentials');
|
||||||
putRequest.onsuccess = () => resolve();
|
putRequest.onsuccess = () => resolve();
|
||||||
putRequest.onerror = () => reject(new Error('Failed to store encrypted credentials'));
|
putRequest.onerror = () => reject(new Error('Failed to store encrypted credentials'));
|
||||||
};
|
};
|
||||||
@ -499,19 +412,19 @@ export class SecureCredentialsService {
|
|||||||
/**
|
/**
|
||||||
* Récupère les credentials chiffrés depuis IndexedDB
|
* Récupère les credentials chiffrés depuis IndexedDB
|
||||||
*/
|
*/
|
||||||
private async getEncryptedCredentials(): Promise<any | null> {
|
async getEncryptedCredentials(): Promise<any | null> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open('SecureCredentials', 1);
|
const request = indexedDB.open('4NK_SecureCredentials', 1);
|
||||||
|
|
||||||
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
request.onerror = () => reject(new Error('Failed to open IndexedDB for credentials'));
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
const transaction = db.transaction(['credentials'], 'readonly');
|
const transaction = db.transaction(['credentials'], 'readonly');
|
||||||
const store = transaction.objectStore('credentials');
|
const store = transaction.objectStore('credentials');
|
||||||
|
|
||||||
const getRequest = store.get('secure-credentials');
|
const getRequest = store.get('webauthn_credentials');
|
||||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
getRequest.onsuccess = () => resolve(getRequest.result || null);
|
||||||
getRequest.onerror = () => reject(new Error('Failed to retrieve encrypted credentials'));
|
getRequest.onerror = () => reject(new Error('Failed to retrieve encrypted credentials'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -525,74 +438,153 @@ export class SecureCredentialsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supprime les credentials chiffrés
|
* Déchiffre et récupère les clés privées avec WebAuthn
|
||||||
*/
|
*/
|
||||||
private async clearEncryptedCredentials(): Promise<void> {
|
async getDecryptedCredentials(): Promise<{ spendKey: string; scanKey: string } | null> {
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
const request = indexedDB.open('SecureCredentials', 1);
|
const encryptedCredentials = await this.getEncryptedCredentials();
|
||||||
|
if (!encryptedCredentials) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
// 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();
|
||||||
|
return credentials !== null;
|
||||||
|
} catch (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 = () => {
|
request.onsuccess = () => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
const transaction = db.transaction(['credentials'], 'readwrite');
|
const transaction = db.transaction(['credentials'], 'readwrite');
|
||||||
const store = transaction.objectStore('credentials');
|
const store = transaction.objectStore('credentials');
|
||||||
|
|
||||||
const deleteRequest = store.delete('secure-credentials');
|
const deleteRequest = store.delete('webauthn_credentials');
|
||||||
deleteRequest.onsuccess = () => resolve();
|
deleteRequest.onsuccess = () => resolve();
|
||||||
deleteRequest.onerror = () => reject(new Error('Failed to clear encrypted credentials'));
|
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
|
* Valide la force du mot de passe
|
||||||
*/
|
*/
|
||||||
validatePasswordStrength(password: string): {
|
validatePasswordStrength(password: string): { isValid: boolean; score: number; feedback: string[] } {
|
||||||
isValid: boolean;
|
|
||||||
score: number;
|
|
||||||
feedback: string[];
|
|
||||||
} {
|
|
||||||
const feedback: string[] = [];
|
const feedback: string[] = [];
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length >= 8) {
|
||||||
feedback.push('Le mot de passe doit contenir au moins 8 caractères');
|
|
||||||
} else {
|
|
||||||
score += 1;
|
score += 1;
|
||||||
|
} else {
|
||||||
|
feedback.push('Password must be at least 8 characters long');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/[A-Z]/.test(password)) {
|
if (/[A-Z]/.test(password)) {
|
||||||
feedback.push('Le mot de passe doit contenir au moins une majuscule');
|
|
||||||
} else {
|
|
||||||
score += 1;
|
score += 1;
|
||||||
|
} else {
|
||||||
|
feedback.push('Password must contain at least one uppercase letter');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/[a-z]/.test(password)) {
|
if (/[a-z]/.test(password)) {
|
||||||
feedback.push('Le mot de passe doit contenir au moins une minuscule');
|
|
||||||
} else {
|
|
||||||
score += 1;
|
score += 1;
|
||||||
|
} else {
|
||||||
|
feedback.push('Password must contain at least one lowercase letter');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/[0-9]/.test(password)) {
|
if (/[0-9]/.test(password)) {
|
||||||
feedback.push('Le mot de passe doit contenir au moins un chiffre');
|
|
||||||
} else {
|
|
||||||
score += 1;
|
score += 1;
|
||||||
|
} else {
|
||||||
|
feedback.push('Password must contain at least one number');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/[^A-Za-z0-9]/.test(password)) {
|
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;
|
score += 1;
|
||||||
|
} else {
|
||||||
|
feedback.push('Password must contain at least one special character');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: score >= 4,
|
isValid: feedback.length === 0,
|
||||||
score,
|
score,
|
||||||
feedback
|
feedback
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance singleton pour l'application
|
// Export de l'instance singleton
|
||||||
export const secureCredentialsService = SecureCredentialsService.getInstance();
|
export const secureCredentialsService = SecureCredentialsService.getInstance();
|
||||||
Loading…
x
Reference in New Issue
Block a user