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:
NicolasCantu 2025-10-23 18:39:09 +02:00
parent e3e3d5431e
commit 97427e811a
2 changed files with 368 additions and 393 deletions

View File

@ -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 à lidentique des dossiers compilés afin dassurer 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.
* **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).
* **JavaScript (.js) :** Permet linclusion 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.
* **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 nen 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.
* **noImplicitAny :**: Interdit lutilisation 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.
* **noUnusedParameters :** Autorise les paramètres non utilisés. Ces paramètres doivent être nommés avec un préfixe conventionnel (_) pour indiquer lintention dignorance.
* **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).
* **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.
* **resolveJsonModule :** Autorise limport 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é.
* **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.
#### 🧪 Tests

View File

@ -1,6 +1,6 @@
/**
* SecureCredentialsService - Gestion sécurisée des credentials avec PBKDF2
* Utilise les credentials du navigateur pour sécuriser les clés de spend et de scan
* 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';
@ -10,6 +10,8 @@ export interface CredentialData {
salt: Uint8Array;
iterations: number;
timestamp: number;
webAuthnCredentialId?: string;
webAuthnPublicKey?: number[];
}
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(
password: string,
_options: CredentialOptions = {}
): Promise<CredentialData> {
try {
secureLogger.info('Generating secure credentials with WebAuthn', {
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');
@ -56,11 +79,11 @@ export class SecureCredentialsService {
// Créer un challenge aléatoire
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 = {
challenge: challenge,
rp: {
name: "4NK Pairing",
name: "4NK Secure Storage",
id: window.location.hostname
},
user: {
@ -80,7 +103,7 @@ export class SecureCredentialsService {
attestation: "direct"
};
console.log('🔐 Requesting WebAuthn credential creation...');
console.log('🔐 Requesting WebAuthn credential creation for encryption key...');
// Créer le credential WebAuthn
const credential = await navigator.credentials.create({
@ -93,38 +116,40 @@ export class SecureCredentialsService {
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 publicKey = response.getPublicKey();
const credentialId = credential.id;
// Générer les clés de chiffrement à partir du credential
const spendKey = Array.from(new Uint8Array(publicKey || new ArrayBuffer(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// 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);
const scanKey = Array.from(new Uint8Array(new TextEncoder().encode(credentialId)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// 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,
scanKey,
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()
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',
operation: 'generateSecureCredentials',
hasSpendKey: !!spendKey,
hasScanKey: !!scanKey
hasSpendKey: !!encryptedSpendKey,
hasScanKey: !!encryptedScanKey
});
return credentialData;
} 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',
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(
password: string,
options: CredentialOptions = {}
): Promise<CredentialData> {
try {
const opts = { ...this.defaultOptions, ...options };
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']
);
secureLogger.info('Generating secure credentials with PBKDF2', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials',
iterations: opts.iterations
});
const keyBits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
256
);
// Générer un salt aléatoire
const salt = crypto.getRandomValues(new Uint8Array(opts.saltLength));
// Dériver la clé maître avec PBKDF2
const masterKey = await this.deriveMasterKey(password, salt, opts.iterations);
// Générer les clés spécifiques
const spendKey = await this.deriveSpendKey(masterKey, salt);
const scanKey = await this.deriveScanKey(masterKey, salt);
const credentialData: CredentialData = {
spendKey,
scanKey,
salt,
iterations: opts.iterations,
timestamp: Date.now()
};
secureLogger.info('Secure credentials generated successfully', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials',
hasSpendKey: !!spendKey,
hasScanKey: !!scanKey
});
return credentialData;
} catch (error) {
secureLogger.error('Failed to generate secure credentials', error as Error, {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials'
});
throw error;
}
return Array.from(new Uint8Array(keyBits))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* 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(
credentialData: CredentialData,
password: string
_password: string
): Promise<void> {
try {
// Chiffrer les clés avec la clé maître
const masterKey = await this.deriveMasterKey(
password,
credentialData.salt,
credentialData.iterations
);
const encryptedSpendKey = await this.encryptKey(credentialData.spendKey, masterKey);
const encryptedScanKey = await this.encryptKey(credentialData.scanKey, masterKey);
// Forcer l'utilisation de WebAuthn (pas de fallback)
console.log('🚨🚨🚨 FORCING WEBAUTHN - NO FALLBACK 🚨🚨🚨');
console.log('🔥🔥🔥 NEW VERSION LOADED - NO FALLBACK ACTIVE 🔥🔥🔥');
console.log('🔍 DEBUG: Forcing WebAuthn credential creation');
console.log('🔄 VERSION: 2025-10-23-12:15 - NO FALLBACK ACTIVE');
console.log('🚀 CACHE-BUST: ' + Date.now() + ' - FORCING WEBAUTHN ONLY - RELOADED');
console.log('🌐 HTTPS VERSION: ' + window.location.href + ' - NO FALLBACK ACTIVE');
secureLogger.info('Forcing WebAuthn credential creation', {
secureLogger.info('Storing encrypted credentials with WebAuthn', {
component: 'SecureCredentialsService',
operation: 'webauthn_force'
operation: 'storeCredentials'
});
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array(32),
rp: { name: '4NK Secure Storage' },
user: {
id: new TextEncoder().encode('4nk-user'),
name: '4NK User',
displayName: '4NK User'
},
pubKeyCredParams: [
{ type: 'public-key', alg: -7 }, // ES256
{ type: 'public-key', alg: -257 } // RS256
],
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'required'
},
timeout: 60000,
attestation: 'direct'
}
});
// 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
};
secureLogger.info('WebAuthn credential created successfully', {
// Stocker dans IndexedDB de manière sécurisée
await this.storeEncryptedCredentials(encryptedCredentials);
secureLogger.info('WebAuthn encrypted credentials stored successfully', {
component: 'SecureCredentialsService',
operation: 'webauthn_create'
operation: 'storeCredentials',
hasSpendKey: !!encryptedCredentials.spendKey,
hasScanKey: !!encryptedCredentials.scanKey
});
if (credential) {
// Stocker les données chiffrées dans IndexedDB
await this.storeEncryptedCredentials({
encryptedSpendKey,
encryptedScanKey,
salt: credentialData.salt,
iterations: credentialData.iterations,
timestamp: credentialData.timestamp,
credentialId: credential.id
});
secureLogger.info('Credentials stored securely', {
component: 'SecureCredentialsService',
operation: 'storeCredentials',
credentialId: credential.id
});
}
console.log('✅ WebAuthn encrypted credentials stored securely');
} 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',
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
*/
private async storeEncryptedCredentials(data: any): Promise<void> {
private async storeEncryptedCredentials(credentials: any): Promise<void> {
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 = () => {
const db = request.result;
const transaction = db.transaction(['credentials'], 'readwrite');
const store = transaction.objectStore('credentials');
const putRequest = store.put(data, 'secure-credentials');
const putRequest = store.put(credentials, 'webauthn_credentials');
putRequest.onsuccess = () => resolve();
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
*/
private async getEncryptedCredentials(): Promise<any | null> {
async getEncryptedCredentials(): Promise<any | null> {
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 = () => {
const db = request.result;
const transaction = db.transaction(['credentials'], 'readonly');
const store = transaction.objectStore('credentials');
const getRequest = store.get('secure-credentials');
getRequest.onsuccess = () => resolve(getRequest.result);
const getRequest = store.get('webauthn_credentials');
getRequest.onsuccess = () => resolve(getRequest.result || null);
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> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SecureCredentials', 1);
async getDecryptedCredentials(): Promise<{ spendKey: string; scanKey: string } | null> {
try {
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
);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['credentials'], 'readwrite');
const store = transaction.objectStore('credentials');
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;
}
}
const deleteRequest = store.delete('secure-credentials');
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => reject(new Error('Failed to clear encrypted credentials'));
/**
* 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 = () => {
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[];
} {
validatePasswordStrength(password: string): { isValid: boolean; score: number; feedback: string[] } {
const feedback: string[] = [];
let score = 0;
if (password.length < 8) {
feedback.push('Le mot de passe doit contenir au moins 8 caractères');
} else {
if (password.length >= 8) {
score += 1;
} else {
feedback.push('Password must be at least 8 characters long');
}
if (!/[A-Z]/.test(password)) {
feedback.push('Le mot de passe doit contenir au moins une majuscule');
} else {
if (/[A-Z]/.test(password)) {
score += 1;
} else {
feedback.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
feedback.push('Le mot de passe doit contenir au moins une minuscule');
} else {
if (/[a-z]/.test(password)) {
score += 1;
} else {
feedback.push('Password must contain at least one lowercase letter');
}
if (!/[0-9]/.test(password)) {
feedback.push('Le mot de passe doit contenir au moins un chiffre');
} else {
if (/[0-9]/.test(password)) {
score += 1;
} else {
feedback.push('Password must contain at least one number');
}
if (!/[^A-Za-z0-9]/.test(password)) {
feedback.push('Le mot de passe doit contenir au moins un caractère spécial');
} else {
if (/[^A-Za-z0-9]/.test(password)) {
score += 1;
} else {
feedback.push('Password must contain at least one special character');
}
return {
isValid: score >= 4,
isValid: feedback.length === 0,
score,
feedback
};
}
}
// Instance singleton pour l'application
export const secureCredentialsService = SecureCredentialsService.getInstance();
// Export de l'instance singleton
export const secureCredentialsService = SecureCredentialsService.getInstance();