"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SecureVaultClient = exports.VaultAuthenticationError = exports.VaultDecryptionError = exports.VaultApiError = void 0; exports.createSecureVaultClient = createSecureVaultClient; exports.createSecureVaultClientWithConfig = createSecureVaultClientWithConfig; const node_fetch_1 = require("node-fetch"); const https_1 = require("https"); const dotenv_1 = require("dotenv"); const { chacha20poly1305 } = require('@noble/ciphers/chacha.js'); const fs_1 = require("fs"); const path_1 = require("path"); class VaultApiError extends Error { constructor(message, code, statusCode) { super(message); this.name = 'VaultApiError'; this.code = code; this.statusCode = statusCode; } } exports.VaultApiError = VaultApiError; class VaultDecryptionError extends Error { constructor(message, code) { super(message); this.name = 'VaultDecryptionError'; this.code = code; } } exports.VaultDecryptionError = VaultDecryptionError; class VaultAuthenticationError extends Error { constructor(message, code, statusCode) { super(message); this.name = 'VaultAuthenticationError'; this.code = code; this.statusCode = statusCode; } } exports.VaultAuthenticationError = VaultAuthenticationError; /** * 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 */ class SecureVaultClient { constructor(config) { this.vaultKey = null; // 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, filePath) { 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, filePaths) { const promises = filePaths.map(filePath => this.getFile(env, filePath)); return Promise.all(promises); } /** * Recherche des fichiers correspondant à un pattern */ async searchFiles(_env, _pattern) { // 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() { 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(); } 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() { 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(); } 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() { try { await this.health(); return true; } catch (error) { return false; } } /** * Récupère toutes les routes disponibles de l'API */ async getRoutes() { 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(); } 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) { // 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 = { synced: 0, skipped: 0, errors: 0, details: [] }; try { // 1. Créer le dossier de destination s'il n'existe pas // Résoudre le chemin par rapport au répertoire du fichier .env let targetDir; if (path_1.default.isAbsolute(localDir)) { targetDir = localDir; } else { // Pour les chemins relatifs, partir du répertoire du fichier .env const envFileDir = this._getEnvFileDirectory(); targetDir = path_1.default.resolve(envFileDir, localDir); } if (!fs_1.default.existsSync(targetDir)) { fs_1.default.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 = []; 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_1.default.join(targetDir, file.project); const localFilePath = path_1.default.join(localProjectDir, file.fileName); // Créer le dossier du projet s'il n'existe pas if (!fs_1.default.existsSync(localProjectDir)) { fs_1.default.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_1.default.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_1.default.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é */ decryptContent(encryptedData, responseHeaders) { 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 */ updateNextKey(nextKey) { 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)}...`); } } /** * Retourne le répertoire du fichier .env trouvé */ _getEnvFileDirectory() { // Chercher le fichier .env dans l'ordre de priorité const possibleEnvPaths = [ path_1.default.join(__dirname, '.env'), // Répertoire du SDK (priorité 1) path_1.default.join(process.cwd(), '.env'), // Répertoire de travail (priorité 2) '.env', // Répertoire courant (priorité 3) '../.env', // Répertoire parent (priorité 4) '../../.env', // Répertoire grand-parent (priorité 5) path_1.default.join(__dirname, '../.env'), // Parent du SDK (priorité 6) path_1.default.join(__dirname, '../../.env'), // Grand-parent du SDK (priorité 7) ]; for (const envPath of possibleEnvPaths) { try { if (fs_1.default.existsSync(envPath)) { // Retourner le répertoire du fichier .env const envDir = path_1.default.dirname(path_1.default.resolve(envPath)); return envDir; } } catch (error) { // Ignorer les erreurs et continuer avec le chemin suivant continue; } } // Fallback vers le répertoire du SDK si aucun .env n'est trouvé return __dirname; } /** * Charge les variables d'environnement depuis plusieurs emplacements possibles */ _loadEnvironmentVariables() { const possibleEnvPaths = [ '.env', // Répertoire courant '../.env', // Répertoire parent '../../.env', // Répertoire grand-parent path_1.default.join(__dirname, '.env'), // Répertoire du SDK path_1.default.join(__dirname, '../.env'), // Parent du SDK path_1.default.join(__dirname, '../../.env'), // Grand-parent du SDK path_1.default.join(process.cwd(), '.env'), // Répertoire de travail ]; let envLoaded = false; for (const envPath of possibleEnvPaths) { try { if (fs_1.default.existsSync(envPath)) { const result = dotenv_1.default.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_1.default.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é */ updateEnvFile(newKey) { try { const envPath = path_1.default.join(__dirname, '../../.env'); // Lire le fichier .env actuel let envContent = ''; if (fs_1.default.existsSync(envPath)) { envContent = fs_1.default.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_1.default.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 */ async _fetchApi(url) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { const mergedOptions = { 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_1.default.Agent({ rejectUnauthorized: false }); } const response = await (0, node_fetch_1.default)(url, mergedOptions); 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'}`); } } } exports.SecureVaultClient = SecureVaultClient; /** * Fonction utilitaire pour créer un client sécurisé */ function createSecureVaultClient(baseUrl, userId) { 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 */ function createSecureVaultClientWithConfig(config) { return new SecureVaultClient(config); }