ihm_client/docs/CODE_ANALYSIS_REPORT.md
NicolasCantu f7c2f86d30 Fix pairing credentials restoration and wallet keys management
**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()
2025-10-29 20:26:33 +01:00

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

  1. 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> { ... }
    }
    
  2. Couplage fort : Services directement liés entre eux

    // ❌ 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

// ✅ 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

  1. Cache désactivé : processesCache existe mais est désactivé (maxCacheSize = 0)

    // ⚠️ É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

    // ❌ Problème actuel
    window.addEventListener('message', handleMessage);
    // Jamais supprimé, s'accumule
    
  3. WebSocket non fermé : Connexions persistantes

    // ❌ Problème actuel
    let ws: WebSocket; // Variable globale
    // Pas de cleanup, pas de reconnexion
    

B. Opérations bloquantes

  1. 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),
    };
    
  2. 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

  1. 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
    
  2. Logs avec données sensibles : Information leakage

    // ❌ Problème actuel
    console.log('encodedPrivateData:', encodedPrivateData);
    // Données privées dans les logs
    
  3. 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

  1. Stack traces exposés : Information disclosure

    // ❌ 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

    // ❌ 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 :

  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

// ✅ 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)

  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.