import fetch from 'node-fetch'; import https from 'https'; import dotenv from 'dotenv'; const { chacha20poly1305 } = require('@noble/ciphers/chacha.js'); import fs from 'fs'; import path from 'path'; // Types pour l'API sécurisée export interface VaultConfig { baseUrl: string; verifySsl?: boolean; timeout?: number; userId: string; // ID utilisateur obligatoire } export interface VaultFile { content: string; filename: string; size: number; encrypted: boolean; algorithm?: string | undefined; user_id?: string | undefined; key_version?: string | undefined; timestamp?: string | undefined; } export interface VaultHealth { status: string; service: string; encryption: string; algorithm: string; authentication?: string; key_rotation?: string; timestamp?: string; } export interface VaultInfo { name: string; version: string; domain: string; port: number; protocol: string; encryption: string; authentication?: string; key_rotation?: string; endpoints?: Record; } export interface VaultRoute { method: string; path: string; description: string; authentication: string; headers_required: string[]; response_type: string; parameters?: Record; examples?: string[]; } export interface VaultRoutes { routes: VaultRoute[]; total_routes: number; authentication: { type: string; header: string; description: string; }; user_id: string; timestamp: string; } export interface SyncOptions { environment: string; localDir?: string; verbose?: boolean; } export interface SyncResult { synced: number; skipped: number; errors: number; details: Array<{ file: string; status: 'synced' | 'skipped' | 'error'; message?: string; }>; } // Classes d'erreurs personnalisées export interface VaultError { message: string; code?: string | undefined; statusCode?: number | undefined; } export class VaultApiError extends Error implements VaultError { public readonly code?: string | undefined; public readonly statusCode?: number | undefined; constructor(message: string, code?: string, statusCode?: number) { super(message); this.name = 'VaultApiError'; this.code = code; this.statusCode = statusCode; } } export class VaultDecryptionError extends Error implements VaultError { public readonly code?: string | undefined; constructor(message: string, code?: string) { super(message); this.name = 'VaultDecryptionError'; this.code = code; } } export class VaultAuthenticationError extends Error implements VaultError { public readonly code?: string | undefined; public readonly statusCode?: number | undefined; constructor(message: string, code?: string, statusCode?: number) { super(message); this.name = 'VaultAuthenticationError'; this.code = code; this.statusCode = statusCode; } } /** * Client sécurisé pour l'API Vault avec authentification par clés utilisateur * Les clés sont gérées côté serveur avec rotation automatique */ export class SecureVaultClient { private config: VaultConfig; private vaultKey: string | null = null; constructor(config?: VaultConfig) { // Charger les variables d'environnement depuis .env // Essayer plusieurs emplacements pour le fichier .env this._loadEnvironmentVariables(); // Si pas de config fournie, utiliser les variables d'environnement if (!config) { const envUser = process.env['VAULT_USER']; const envKey = process.env['VAULT_KEY']; const envEnv = process.env['VAULT_ENV']; if (!envUser || !envKey || !envEnv) { throw new Error('Variables d\'environnement requises: VAULT_USER, VAULT_KEY, VAULT_ENV'); } config = { baseUrl: 'https://vault.4nkweb.com:6666', userId: envUser, timeout: 30000, verifySsl: false }; this.vaultKey = envKey; } this.config = { verifySsl: false, timeout: 30000, ...config, }; // Validation de l'ID utilisateur if (!this.config.userId || this.config.userId.length < 3 || this.config.userId.length > 128) { throw new Error('ID utilisateur requis (3-128 caractères)'); } if (!/^[a-zA-Z0-9_-]+$/.test(this.config.userId)) { throw new Error('ID utilisateur invalide - caractères autorisés: a-z, A-Z, 0-9, _, -'); } } /** * Récupère un fichier depuis l'API Vault */ async getFile(env: string, filePath: string): Promise { const url = `${this.config.baseUrl}/${env}/${filePath}`; try { const response = await this._fetchApi(url); if (!response.ok) { if (response.status === 401) { throw new VaultAuthenticationError( 'Authentification échouée - vérifiez votre ID utilisateur', 'AUTH_FAILED', response.status ); } if (response.status === 403) { throw new VaultApiError( 'Accès non autorisé à ce fichier', 'ACCESS_DENIED', response.status ); } throw new VaultApiError( `Erreur API: ${response.status} ${response.statusText}`, 'API_ERROR', response.status ); } const encryptedData = await response.arrayBuffer(); // Extraction des métadonnées depuis les headers const user_id = response.headers.get('X-User-ID') || undefined; const keyRotation = response.headers.get('X-Key-Rotation') || undefined; const algorithm = response.headers.get('X-Algorithm') || undefined; // Déchiffrement du contenu avec les headers pour la prochaine clé const decryptedContent = this.decryptContent(Buffer.from(encryptedData), response.headers); return { content: decryptedContent, filename: filePath.split('/').pop() || filePath, size: decryptedContent.length, encrypted: true, algorithm, user_id, key_version: keyRotation, timestamp: new Date().toISOString() }; } catch (error) { if (error instanceof VaultApiError || error instanceof VaultAuthenticationError) { throw error; } throw new VaultApiError( `Erreur lors de la récupération du fichier: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } /** * Récupère plusieurs fichiers en parallèle */ async getFiles(env: string, filePaths: string[]): Promise { const promises = filePaths.map(filePath => this.getFile(env, filePath)); return Promise.all(promises); } /** * Recherche des fichiers correspondant à un pattern */ async searchFiles(_env: string, _pattern: string): Promise { // Cette fonctionnalité nécessiterait une implémentation côté serveur // Pour l'instant, retour d'une erreur explicative throw new VaultApiError( 'Recherche de fichiers non implémentée - contactez l\'administrateur', 'NOT_IMPLEMENTED' ); } /** * Vérifie l'état de santé de l'API */ async health(): Promise { const url = `${this.config.baseUrl}/health`; try { const response = await this._fetchApi(url); if (!response.ok) { throw new VaultApiError( `Erreur de santé API: ${response.status}`, 'HEALTH_CHECK_FAILED', response.status ); } return await response.json() as VaultHealth; } catch (error) { if (error instanceof VaultApiError) { throw error; } throw new VaultApiError( `Erreur lors du contrôle de santé: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } /** * Récupère les informations sur l'API */ async info(): Promise { const url = `${this.config.baseUrl}/info`; try { const response = await this._fetchApi(url); if (!response.ok) { throw new VaultApiError( `Erreur info API: ${response.status}`, 'INFO_ERROR', response.status ); } return await response.json() as VaultInfo; } catch (error) { if (error instanceof VaultApiError) { throw error; } throw new VaultApiError( `Erreur lors de la récupération des informations: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } /** * Test de connectivité simple */ async ping(): Promise { try { await this.health(); return true; } catch (error) { return false; } } /** * Récupère toutes les routes disponibles de l'API */ async getRoutes(): Promise { const url = `${this.config.baseUrl}/routes`; try { const response = await this._fetchApi(url); if (!response.ok) { if (response.status === 401) { throw new VaultAuthenticationError( 'Authentification échouée - vérifiez votre ID utilisateur', 'AUTH_FAILED', response.status ); } throw new VaultApiError( `Erreur API routes: ${response.status} ${response.statusText}`, 'ROUTES_API_ERROR', response.status ); } return await response.json() as VaultRoutes; } catch (error) { if (error instanceof VaultApiError || error instanceof VaultAuthenticationError) { throw error; } throw new VaultApiError( `Erreur lors de la récupération des routes: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } /** * Synchronise les fichiers déchiffrés localement * Route vault /// -> ../confs// * Les fichiers existants dans confs/ sont toujours écrasés pour avoir le contenu le plus récent */ async syncLocalFiles(options: SyncOptions): Promise { // Recharger les variables d'environnement au cas où elles auraient changé this._loadEnvironmentVariables(); // Récupérer le dossier de destination depuis les variables d'environnement const defaultConfsDir = process.env['VAULT_CONFS_DIR'] || '../confs'; const { environment, localDir = defaultConfsDir, verbose = false } = options; const result: SyncResult = { synced: 0, skipped: 0, errors: 0, details: [] }; try { // 1. Créer le dossier de destination s'il n'existe pas const targetDir = path.resolve(localDir); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); if (verbose) { console.log(`📁 Dossier créé: ${targetDir}`); } } // 2. Récupérer la liste des routes pour identifier les fichiers const routes = await this.getRoutes(); // 3. Extraire les fichiers disponibles depuis les exemples de routes const fileRoute = routes.routes.find(route => route.path.includes('') && route.path.includes('')); if (!fileRoute || !fileRoute.examples) { throw new VaultApiError('Impossible de déterminer les fichiers disponibles'); } // 4. Parser les exemples pour extraire les projets et fichiers const filesToSync: Array<{ project: string; fileName: string; vaultPath: string }> = []; for (const example of fileRoute.examples) { // Format: /dev/bitcoin/bitcoin.conf -> project: bitcoin, fileName: bitcoin.conf const pathParts = example.split('/').filter(part => part && part !== environment); if (pathParts.length >= 2) { const project = pathParts[0] || 'unknown'; const fileName = pathParts[pathParts.length - 1] || 'unknown'; const vaultPath = pathParts.join('/'); filesToSync.push({ project, fileName, vaultPath }); } } // 5. Synchroniser chaque fichier for (const file of filesToSync) { try { const localProjectDir = path.join(targetDir, file.project); const localFilePath = path.join(localProjectDir, file.fileName); // Créer le dossier du projet s'il n'existe pas if (!fs.existsSync(localProjectDir)) { fs.mkdirSync(localProjectDir, { recursive: true }); if (verbose) { console.log(`📁 Dossier projet créé: ${localProjectDir}`); } } // Toujours écraser les fichiers dans confs/ pour avoir le contenu le plus récent // La vérification d'existence est conservée pour le logging uniquement const fileExists = fs.existsSync(localFilePath); if (fileExists && verbose) { console.log(`🔄 Écrasement du fichier existant: ${file.vaultPath}`); } // Récupérer le fichier depuis le vault const vaultFile = await this.getFile(environment, file.vaultPath); // Extraire le contenu déchiffré (simulation pour le format de démonstration) let content = vaultFile.content; // Le contenu est maintenant déchiffré automatiquement par decryptContent // Pas besoin de récupérer depuis le storage, le contenu vient de l'API déchiffrée // Écrire le fichier local fs.writeFileSync(localFilePath, content, 'utf8'); result.synced++; result.details.push({ file: file.vaultPath, status: 'synced', message: `Synchronisé vers ${localFilePath}` }); if (verbose) { console.log(`✅ Synchronisé: ${file.vaultPath} -> ${localFilePath}`); } } catch (error) { result.errors++; const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue'; result.details.push({ file: file.vaultPath, status: 'error', message: errorMessage }); if (verbose) { console.log(`❌ Erreur: ${file.vaultPath} - ${errorMessage}`); } } } if (verbose) { console.log(`\n📊 Résumé de synchronisation:`); console.log(` ✅ Synchronisés: ${result.synced}`); console.log(` ⏭️ Ignorés: ${result.skipped}`); console.log(` ❌ Erreurs: ${result.errors}`); } return result; } catch (error) { throw new VaultApiError( `Erreur lors de la synchronisation: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } /** * Déchiffre le contenu avec les métadonnées utilisateur et gère la prochaine clé */ private decryptContent(encryptedData: Buffer, responseHeaders?: Headers): string { try { // Décoder le base64 const decoded = Buffer.from(encryptedData.toString(), 'base64'); // Nouveau format: nonce (12 bytes) + taille_métadonnées (4 bytes) + métadonnées + contenu chiffré if (decoded.length < 16) { throw new Error('Données chiffrées invalides - format incorrect'); } const nonce = decoded.subarray(0, 12); const metadataSize = decoded.readUInt32BE(12); const metadataJson = decoded.subarray(16, 16 + metadataSize); const ciphertext = decoded.subarray(16 + metadataSize); // Parse des métadonnées const metadata = JSON.parse(metadataJson.toString('utf-8')); // Vérification de l'utilisateur if (metadata.user_id !== this.config.userId) { throw new VaultAuthenticationError( 'Métadonnées utilisateur ne correspondent pas', 'USER_MISMATCH' ); } // Récupération de la prochaine clé depuis les headers ou les métadonnées const nextKey = responseHeaders?.get('X-Next-Key') || metadata.next_key; // Pour le déchiffrement, utiliser la clé courante d'abord const keyToUse = this.vaultKey; if (!keyToUse) { throw new VaultDecryptionError('Clé de déchiffrement non disponible'); } console.log(`🔑 Utilisation de la clé courante pour déchiffrement: ${keyToUse.substring(0, 20)}...`); // Ne pas mettre à jour la clé immédiatement, attendre un déchiffrement réussi try { // Convertir la clé base64 en Uint8Array const key = Buffer.from(keyToUse, 'base64'); // Déchiffrement ChaCha20-Poly1305 avec @noble/ciphers const cipher = chacha20poly1305(key, nonce); const decrypted = cipher.decrypt(ciphertext); // Déchiffrement réussi, mettre à jour la clé pour la prochaine requête if (nextKey) { this.updateNextKey(nextKey); } // Retourner le contenu déchiffré return Buffer.from(decrypted).toString('utf-8'); } catch (decryptError) { // Si le déchiffrement échoue, essayer avec la prochaine clé si elle est différente if (nextKey && nextKey !== keyToUse) { console.log(`🔄 Tentative de déchiffrement avec la prochaine clé...`); try { const nextKeyBuffer = Buffer.from(nextKey, 'base64'); const cipherNext = chacha20poly1305(nextKeyBuffer, nonce); const decryptedNext = cipherNext.decrypt(ciphertext); console.log(`✅ Déchiffrement réussi avec la prochaine clé !`); // Déchiffrement réussi avec la prochaine clé, mettre à jour this.updateNextKey(nextKey); return Buffer.from(decryptedNext).toString('utf-8'); } catch (nextDecryptError) { console.warn(`⚠️ Déchiffrement avec la prochaine clé échoué: ${nextDecryptError instanceof Error ? nextDecryptError.message : 'Erreur inconnue'}`); } } throw new VaultDecryptionError( `Erreur de déchiffrement ChaCha20-Poly1305: ${decryptError instanceof Error ? decryptError.message : 'Inconnue'}` ); } } catch (error) { if (error instanceof VaultAuthenticationError || error instanceof VaultDecryptionError) { throw error; } throw new VaultDecryptionError( `Erreur de déchiffrement: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } /** * Met à jour la prochaine clé pour les requêtes suivantes et le fichier .env */ private updateNextKey(nextKey: string): void { if (nextKey) { this.vaultKey = nextKey; // Mettre à jour la clé courante // Mettre à jour le fichier .env avec la nouvelle clé this.updateEnvFile(nextKey); console.log(`🔑 Prochaine clé mise à jour: ${nextKey.substring(0, 20)}...`); } } /** * Charge les variables d'environnement depuis plusieurs emplacements possibles */ private _loadEnvironmentVariables(): void { const possibleEnvPaths = [ '.env', // Répertoire courant '../.env', // Répertoire parent '../../.env', // Répertoire grand-parent path.join(__dirname, '.env'), // Répertoire du SDK path.join(__dirname, '../.env'), // Parent du SDK path.join(__dirname, '../../.env'), // Grand-parent du SDK path.join(process.cwd(), '.env'), // Répertoire de travail ]; let envLoaded = false; for (const envPath of possibleEnvPaths) { try { if (fs.existsSync(envPath)) { const result = dotenv.config({ path: envPath }); if (!result.error) { console.log(`📄 Variables d'environnement chargées depuis: ${envPath}`); envLoaded = true; break; } } } catch (error) { // Ignorer les erreurs et continuer avec le chemin suivant continue; } } // Si aucun fichier .env n'a été trouvé, essayer le chargement par défaut if (!envLoaded) { try { dotenv.config(); console.log('📄 Variables d\'environnement chargées depuis .env par défaut'); } catch (error) { console.log('⚠️ Aucun fichier .env trouvé, utilisation des variables système uniquement'); } } } /** * Met à jour le fichier .env avec la nouvelle clé */ private updateEnvFile(newKey: string): void { try { const envPath = path.join(__dirname, '../../.env'); // Lire le fichier .env actuel let envContent = ''; if (fs.existsSync(envPath)) { envContent = fs.readFileSync(envPath, 'utf8'); } // Mettre à jour ou ajouter la clé VAULT_KEY const keyRegex = /^VAULT_KEY=.*$/m; if (keyRegex.test(envContent)) { envContent = envContent.replace(keyRegex, `VAULT_KEY="${newKey}"`); } else { envContent += `\nVAULT_KEY="${newKey}"\n`; } // Écrire le fichier .env mis à jour fs.writeFileSync(envPath, envContent, 'utf8'); console.log(`📝 Fichier .env mis à jour avec la nouvelle clé`); } catch (error) { console.warn(`⚠️ Impossible de mettre à jour le fichier .env: ${error instanceof Error ? error.message : 'Erreur inconnue'}`); } } /** * Effectue une requête vers l'API avec authentification */ private async _fetchApi(url: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { const mergedOptions: any = { method: 'GET', headers: { 'User-Agent': 'SecureVaultClient/2.0.0', 'X-User-ID': this.config.userId, 'Accept': 'application/octet-stream' }, signal: controller.signal }; // Configuration SSL pour les certificats auto-signés if (this.config.verifySsl === false) { mergedOptions.agent = new https.Agent({ rejectUnauthorized: false }); } const response = await fetch(url, mergedOptions) as any; clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new VaultApiError( `Timeout de la requête (${this.config.timeout}ms)`, 'TIMEOUT' ); } throw new VaultApiError( `Erreur de connexion: ${error instanceof Error ? error.message : 'Inconnue'}` ); } } } /** * Fonction utilitaire pour créer un client sécurisé */ export function createSecureVaultClient(baseUrl: string, userId: string): SecureVaultClient { return new SecureVaultClient({ baseUrl, userId, verifySsl: false, // Pour les certificats auto-signés en développement timeout: 15000 }); } /** * Fonction utilitaire pour créer un client sécurisé avec configuration complète */ export function createSecureVaultClientWithConfig(config: VaultConfig): SecureVaultClient { return new SecureVaultClient(config); }