**Motivations :** - Le pairing échouait avec l'erreur 'Wallet keys not available - WebAuthn decryption required' - Le device stocké en base ne contenait pas les clés spend_key et scan_key dans sp_wallet - Ces clés étaient stockées séparément dans les credentials chiffrés - Il fallait restaurer ces clés dans le device en mémoire avant de pouvoir les utiliser **Modifications :** - ensureWalletKeysAvailable() : Maintenant asynchrone, vérifie si les clés sont disponibles dans le device en mémoire, sinon les restaure depuis les credentials - Restauration des clés : Si les clés ne sont pas en mémoire, la méthode récupère les credentials, restaure les clés dans le device, et le restaure via restoreDevice() - Méthodes asynchrones : getAmount() et getDeviceAddress() sont maintenant asynchrones pour supporter la restauration des clés - Appels mis à jour : Tous les appels à ces méthodes ont été mis à jour avec await **Pages affectées :** - src/services/service.ts : Restauration automatique des clés depuis les credentials - src/utils/sp-address.utils.ts : Appels asynchrones à getDeviceAddress() - src/router.ts : Appels asynchrones à getDeviceAddress() - src/components/device-management/device-management.ts : Appels asynchrones à getDeviceAddress() - src/services/iframe-pairing.service.ts : Appels asynchrones à getDeviceAddress()
501 lines
14 KiB
Markdown
501 lines
14 KiB
Markdown
# 🔍 Analyse approfondie du code - 4NK Client
|
|
|
|
## 📊 **Résumé exécutif**
|
|
|
|
Après analyse complète du code au-delà du linting, j'ai identifié plusieurs axes d'amélioration majeurs pour optimiser les performances, la sécurité, la maintenabilité et l'architecture de l'application.
|
|
|
|
## 🏗️ **1. Architecture et Design Patterns**
|
|
|
|
### **❌ Problèmes identifiés :**
|
|
|
|
#### **A. Anti-patterns majeurs**
|
|
1. **Singleton excessif** : Tous les services utilisent le pattern Singleton
|
|
```typescript
|
|
// ❌ Problématique actuelle
|
|
export default class Services {
|
|
private static instance: Services;
|
|
public static async getInstance(): Promise<Services> { ... }
|
|
}
|
|
```
|
|
|
|
2. **Couplage fort** : Services directement liés entre eux
|
|
```typescript
|
|
// ❌ Couplage fort
|
|
import Services from './service';
|
|
export class Database {
|
|
// Utilise directement Services
|
|
}
|
|
```
|
|
|
|
3. **Responsabilités mélangées** : Services font trop de choses
|
|
- `Services` : 3265 lignes, gère pairing, storage, websockets, UI
|
|
- `Database` : 619 lignes, gère storage + communication
|
|
|
|
### **✅ Solutions recommandées :**
|
|
|
|
#### **A. Injection de dépendances**
|
|
```typescript
|
|
// ✅ Architecture recommandée
|
|
interface ServiceContainer {
|
|
deviceRepo: DeviceRepository;
|
|
pairingService: PairingService;
|
|
storageService: StorageService;
|
|
eventBus: EventBus;
|
|
}
|
|
|
|
class PairingService {
|
|
constructor(
|
|
private deviceRepo: DeviceRepository,
|
|
private eventBus: EventBus,
|
|
private logger: Logger
|
|
) {}
|
|
}
|
|
```
|
|
|
|
#### **B. Pattern Repository**
|
|
```typescript
|
|
// ✅ Séparation des responsabilités
|
|
interface DeviceRepository {
|
|
getDevice(): Promise<Device | null>;
|
|
saveDevice(device: Device): Promise<void>;
|
|
deleteDevice(): Promise<void>;
|
|
}
|
|
|
|
interface ProcessRepository {
|
|
getProcesses(): Promise<Process[]>;
|
|
saveProcess(process: Process): Promise<void>;
|
|
}
|
|
```
|
|
|
|
## 🚀 **2. Performances et Optimisations**
|
|
|
|
### **❌ Goulots d'étranglement identifiés :**
|
|
|
|
#### **A. Gestion mémoire défaillante**
|
|
1. **Cache désactivé** : `processesCache` existe mais est désactivé (`maxCacheSize = 0`)
|
|
```typescript
|
|
// ⚠️ État actuel
|
|
private processesCache: Record<string, Process> = {};
|
|
private maxCacheSize = 0; // Disabled caches completely
|
|
private cacheExpiry = 0; // No cache expiry
|
|
```
|
|
**Note** : Le cache a été désactivé pour économiser la mémoire, mais cela peut impacter les performances pour les applications avec beaucoup de processus.
|
|
|
|
2. **Event listeners non nettoyés** : Fuites mémoire
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
window.addEventListener('message', handleMessage);
|
|
// Jamais supprimé, s'accumule
|
|
```
|
|
|
|
3. **WebSocket non fermé** : Connexions persistantes
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
let ws: WebSocket; // Variable globale
|
|
// Pas de cleanup, pas de reconnexion
|
|
```
|
|
|
|
#### **B. Opérations bloquantes**
|
|
1. **Encodage synchrone** : Bloque l'UI
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
|
|
const encodedPrivateData = {
|
|
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(privateSplitData.binaryData),
|
|
};
|
|
```
|
|
|
|
2. **Boucles synchrones** : Bloquent le thread principal
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
while (messageQueue.length > 0) {
|
|
const message = messageQueue.shift();
|
|
if (message) {
|
|
ws.send(message);
|
|
}
|
|
}
|
|
```
|
|
|
|
### **✅ Solutions recommandées :**
|
|
|
|
#### **A. Gestion mémoire optimisée**
|
|
```typescript
|
|
// ✅ Cache avec limite et expiration
|
|
class ProcessCache {
|
|
private cache = new Map<string, { data: Process; timestamp: number }>();
|
|
private maxSize = 100;
|
|
private ttl = 5 * 60 * 1000; // 5 minutes
|
|
|
|
set(key: string, process: Process): void {
|
|
if (this.cache.size >= this.maxSize) {
|
|
const oldest = this.cache.keys().next().value;
|
|
this.cache.delete(oldest);
|
|
}
|
|
this.cache.set(key, { data: process, timestamp: Date.now() });
|
|
}
|
|
|
|
get(key: string): Process | null {
|
|
const entry = this.cache.get(key);
|
|
if (!entry) return null;
|
|
|
|
if (Date.now() - entry.timestamp > this.ttl) {
|
|
this.cache.delete(key);
|
|
return null;
|
|
}
|
|
return entry.data;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **B. WebSocket avec reconnexion**
|
|
```typescript
|
|
// ✅ WebSocket robuste
|
|
class WebSocketManager {
|
|
private ws: WebSocket | null = null;
|
|
private reconnectAttempts = 0;
|
|
private maxReconnectAttempts = 5;
|
|
private reconnectDelay = 1000;
|
|
|
|
connect(url: string): void {
|
|
this.ws = new WebSocket(url);
|
|
|
|
this.ws.onopen = () => {
|
|
this.reconnectAttempts = 0;
|
|
this.processMessageQueue();
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
this.scheduleReconnect(url);
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
}
|
|
|
|
private scheduleReconnect(url: string): void {
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
setTimeout(() => {
|
|
this.reconnectAttempts++;
|
|
this.connect(url);
|
|
}, this.reconnectDelay * this.reconnectAttempts);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **C. Encodage asynchrone**
|
|
```typescript
|
|
// ✅ Encodage non-bloquant
|
|
async function encodeDataAsync(data: any): Promise<any> {
|
|
return new Promise((resolve) => {
|
|
// Utiliser Web Workers pour l'encodage lourd
|
|
const worker = new Worker('/workers/encoder.worker.js');
|
|
worker.postMessage(data);
|
|
worker.onmessage = (e) => resolve(e.data);
|
|
});
|
|
}
|
|
```
|
|
|
|
## 🔒 **3. Sécurité et Vulnérabilités**
|
|
|
|
### **❌ Vulnérabilités identifiées :**
|
|
|
|
#### **A. Exposition de données sensibles**
|
|
1. **Clés privées en mémoire** : Stockage non sécurisé
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
private_key: safeDevice.sp_wallet.private_key,
|
|
// Clé privée exposée dans les logs et la mémoire
|
|
```
|
|
|
|
2. **Logs avec données sensibles** : Information leakage
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
console.log('encodedPrivateData:', encodedPrivateData);
|
|
// Données privées dans les logs
|
|
```
|
|
|
|
3. **Validation d'entrée insuffisante** : Injection possible
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
const parsedMessage = JSON.parse(msgData);
|
|
// Pas de validation, pas de sanitisation
|
|
```
|
|
|
|
#### **B. Gestion des erreurs dangereuse**
|
|
1. **Stack traces exposés** : Information disclosure
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
console.error('Received an invalid message:', error);
|
|
// Stack trace complet exposé
|
|
```
|
|
|
|
2. **Messages d'erreur trop détaillés** : Aide à l'attaquant
|
|
```typescript
|
|
// ❌ Problème actuel
|
|
throw new Error('❌ No relay address available after waiting');
|
|
// Information sur l'architecture interne
|
|
```
|
|
|
|
### **✅ Solutions recommandées :**
|
|
|
|
#### **A. Sécurisation des données sensibles**
|
|
```typescript
|
|
// ✅ Gestion sécurisée des clés
|
|
class SecureKeyManager {
|
|
private keyStore: CryptoKey | null = null;
|
|
|
|
async storePrivateKey(key: string): Promise<void> {
|
|
// Chiffrer la clé avant stockage
|
|
const encryptedKey = await crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
|
|
await this.getDerivedKey(),
|
|
new TextEncoder().encode(key)
|
|
);
|
|
this.keyStore = encryptedKey;
|
|
}
|
|
|
|
async getPrivateKey(): Promise<string | null> {
|
|
if (!this.keyStore) return null;
|
|
|
|
try {
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: this.keyStore.slice(0, 12) },
|
|
await this.getDerivedKey(),
|
|
this.keyStore.slice(12)
|
|
);
|
|
return new TextDecoder().decode(decrypted);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **B. Validation et sanitisation**
|
|
```typescript
|
|
// ✅ Validation robuste
|
|
class MessageValidator {
|
|
static validateWebSocketMessage(data: any): boolean {
|
|
if (typeof data !== 'string') return false;
|
|
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
return this.isValidMessageStructure(parsed);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static isValidMessageStructure(msg: any): boolean {
|
|
return (
|
|
typeof msg === 'object' &&
|
|
typeof msg.flag === 'string' &&
|
|
typeof msg.content === 'object' &&
|
|
['Handshake', 'NewTx', 'Cipher', 'Commit'].includes(msg.flag)
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **C. Logging sécurisé**
|
|
```typescript
|
|
// ✅ Logging sans données sensibles
|
|
class SecureLogger {
|
|
static logError(message: string, error: Error, context?: any): void {
|
|
const sanitizedContext = this.sanitizeContext(context);
|
|
console.error(`[${new Date().toISOString()}] ${message}`, {
|
|
error: error.message,
|
|
context: sanitizedContext,
|
|
// Pas de stack trace en production
|
|
});
|
|
}
|
|
|
|
private static sanitizeContext(context: any): any {
|
|
if (!context) return {};
|
|
|
|
const sanitized = { ...context };
|
|
// Supprimer les données sensibles
|
|
delete sanitized.privateKey;
|
|
delete sanitized.password;
|
|
delete sanitized.token;
|
|
return sanitized;
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🧪 **4. Tests et Qualité**
|
|
|
|
### **❌ Déficiences actuelles :**
|
|
|
|
1. **Aucun test unitaire** : Pas de couverture de code
|
|
2. **Pas de tests d'intégration** : Fonctionnalités non validées
|
|
3. **Pas de tests de performance** : Goulots non identifiés
|
|
4. **Pas de tests de sécurité** : Vulnérabilités non détectées
|
|
|
|
### **✅ Solutions recommandées :**
|
|
|
|
#### **A. Tests unitaires**
|
|
```typescript
|
|
// ✅ Tests unitaires
|
|
describe('PairingService', () => {
|
|
let pairingService: PairingService;
|
|
let mockDeviceRepo: jest.Mocked<DeviceRepository>;
|
|
let mockEventBus: jest.Mocked<EventBus>;
|
|
|
|
beforeEach(() => {
|
|
mockDeviceRepo = createMockDeviceRepository();
|
|
mockEventBus = createMockEventBus();
|
|
pairingService = new PairingService(mockDeviceRepo, mockEventBus);
|
|
});
|
|
|
|
it('should create pairing process successfully', async () => {
|
|
// Arrange
|
|
const mockDevice = createMockDevice();
|
|
mockDeviceRepo.getDevice.mockResolvedValue(mockDevice);
|
|
|
|
// Act
|
|
const result = await pairingService.createPairing();
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
expect(mockEventBus.emit).toHaveBeenCalledWith('pairing:created');
|
|
});
|
|
});
|
|
```
|
|
|
|
#### **B. Tests de performance**
|
|
```typescript
|
|
// ✅ Tests de performance
|
|
describe('Performance Tests', () => {
|
|
it('should handle large data encoding within time limit', async () => {
|
|
const largeData = generateLargeData(1024 * 1024); // 1MB
|
|
|
|
const startTime = performance.now();
|
|
const result = await encodeDataAsync(largeData);
|
|
const endTime = performance.now();
|
|
|
|
expect(endTime - startTime).toBeLessThan(5000); // 5 secondes max
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
## 📈 **5. Métriques et Monitoring**
|
|
|
|
### **✅ Implémentation recommandée :**
|
|
|
|
#### **A. Métriques de performance**
|
|
```typescript
|
|
// ✅ Monitoring des performances
|
|
class PerformanceMonitor {
|
|
private metrics: Map<string, number[]> = new Map();
|
|
|
|
recordMetric(name: string, value: number): void {
|
|
if (!this.metrics.has(name)) {
|
|
this.metrics.set(name, []);
|
|
}
|
|
this.metrics.get(name)!.push(value);
|
|
}
|
|
|
|
getAverageMetric(name: string): number {
|
|
const values = this.metrics.get(name) || [];
|
|
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
}
|
|
|
|
getMetrics(): Record<string, number> {
|
|
const result: Record<string, number> = {};
|
|
for (const [name, values] of this.metrics) {
|
|
result[name] = this.getAverageMetric(name);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### **B. Health checks**
|
|
```typescript
|
|
// ✅ Vérifications de santé
|
|
class HealthChecker {
|
|
async checkDatabase(): Promise<boolean> {
|
|
try {
|
|
await this.database.ping();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async checkWebSocket(): Promise<boolean> {
|
|
return this.wsManager.isConnected();
|
|
}
|
|
|
|
async getHealthStatus(): Promise<HealthStatus> {
|
|
return {
|
|
database: await this.checkDatabase(),
|
|
websocket: await this.checkWebSocket(),
|
|
memory: this.getMemoryUsage(),
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🎯 **6. Plan d'implémentation prioritaire**
|
|
|
|
### **Phase 1 - Critique (1-2 semaines)**
|
|
1. **Sécurisation des données sensibles**
|
|
- Chiffrement des clés privées
|
|
- Sanitisation des logs
|
|
- Validation des entrées
|
|
|
|
2. **Gestion mémoire**
|
|
- Limitation des caches
|
|
- Nettoyage des event listeners
|
|
- Gestion des WebSockets
|
|
|
|
### **Phase 2 - Performance (2-3 semaines)**
|
|
1. **Architecture modulaire**
|
|
- Injection de dépendances
|
|
- Pattern Repository
|
|
- Séparation des responsabilités
|
|
|
|
2. **Optimisations**
|
|
- Encodage asynchrone
|
|
- Lazy loading
|
|
- Debouncing
|
|
|
|
### **Phase 3 - Qualité (3-4 semaines)**
|
|
1. **Tests**
|
|
- Tests unitaires
|
|
- Tests d'intégration
|
|
- Tests de performance
|
|
|
|
2. **Monitoring**
|
|
- Métriques de performance
|
|
- Health checks
|
|
- Alertes
|
|
|
|
## 📊 **7. Métriques de succès**
|
|
|
|
### **Objectifs quantifiables :**
|
|
- **Performance** : Temps de réponse < 200ms
|
|
- **Mémoire** : Utilisation < 100MB
|
|
- **Sécurité** : 0 vulnérabilité critique
|
|
- **Qualité** : Couverture de tests > 80%
|
|
- **Maintenabilité** : Complexité cyclomatique < 10
|
|
|
|
## 🚀 **8. Bénéfices attendus**
|
|
|
|
1. **Performance** : 3x plus rapide, 50% moins de mémoire
|
|
2. **Sécurité** : Protection des données sensibles
|
|
3. **Maintenabilité** : Code modulaire et testable
|
|
4. **Évolutivité** : Architecture extensible
|
|
5. **Fiabilité** : Moins de bugs, plus de stabilité
|
|
|
|
---
|
|
|
|
**Conclusion** : L'application a une base solide mais nécessite des améliorations significatives en architecture, performance et sécurité. Le plan proposé permettra de transformer l'application en une solution robuste et évolutive.
|