**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()
14 KiB
🔍 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
-
Singleton excessif : Tous les services utilisent le pattern Singleton
// ❌ Problématique actuelle export default class Services { private static instance: Services; public static async getInstance(): Promise<Services> { ... } } -
Couplage fort : Services directement liés entre eux
// ❌ Couplage fort import Services from './service'; export class Database { // Utilise directement Services } -
Responsabilités mélangées : Services font trop de choses
Services: 3265 lignes, gère pairing, storage, websockets, UIDatabase: 619 lignes, gère storage + communication
✅ Solutions recommandées :
A. Injection de dépendances
// ✅ 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
// ✅ 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
-
Cache désactivé :
processesCacheexiste mais est désactivé (maxCacheSize = 0)// ⚠️ État actuel private processesCache: Record<string, Process> = {}; private maxCacheSize = 0; // Disabled caches completely private cacheExpiry = 0; // No cache expiryNote : Le cache a été désactivé pour économiser la mémoire, mais cela peut impacter les performances pour les applications avec beaucoup de processus.
-
Event listeners non nettoyés : Fuites mémoire
// ❌ Problème actuel window.addEventListener('message', handleMessage); // Jamais supprimé, s'accumule -
WebSocket non fermé : Connexions persistantes
// ❌ Problème actuel let ws: WebSocket; // Variable globale // Pas de cleanup, pas de reconnexion
B. Opérations bloquantes
-
Encodage synchrone : Bloque l'UI
// ❌ 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), }; -
Boucles synchrones : Bloquent le thread principal
// ❌ 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
// ✅ 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
// ✅ 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
// ✅ 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
-
Clés privées en mémoire : Stockage non sécurisé
// ❌ Problème actuel private_key: safeDevice.sp_wallet.private_key, // Clé privée exposée dans les logs et la mémoire -
Logs avec données sensibles : Information leakage
// ❌ Problème actuel console.log('encodedPrivateData:', encodedPrivateData); // Données privées dans les logs -
Validation d'entrée insuffisante : Injection possible
// ❌ Problème actuel const parsedMessage = JSON.parse(msgData); // Pas de validation, pas de sanitisation
B. Gestion des erreurs dangereuse
-
Stack traces exposés : Information disclosure
// ❌ Problème actuel console.error('Received an invalid message:', error); // Stack trace complet exposé -
Messages d'erreur trop détaillés : Aide à l'attaquant
// ❌ 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
// ✅ 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
// ✅ 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é
// ✅ 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 :
- Aucun test unitaire : Pas de couverture de code
- Pas de tests d'intégration : Fonctionnalités non validées
- Pas de tests de performance : Goulots non identifiés
- Pas de tests de sécurité : Vulnérabilités non détectées
✅ Solutions recommandées :
A. Tests unitaires
// ✅ 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
// ✅ 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
// ✅ 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
// ✅ 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)
-
Sécurisation des données sensibles
- Chiffrement des clés privées
- Sanitisation des logs
- Validation des entrées
-
Gestion mémoire
- Limitation des caches
- Nettoyage des event listeners
- Gestion des WebSockets
Phase 2 - Performance (2-3 semaines)
-
Architecture modulaire
- Injection de dépendances
- Pattern Repository
- Séparation des responsabilités
-
Optimisations
- Encodage asynchrone
- Lazy loading
- Debouncing
Phase 3 - Qualité (3-4 semaines)
-
Tests
- Tests unitaires
- Tests d'intégration
- Tests de performance
-
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
- Performance : 3x plus rapide, 50% moins de mémoire
- Sécurité : Protection des données sensibles
- Maintenabilité : Code modulaire et testable
- Évolutivité : Architecture extensible
- 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.