ci: docker_tag=api-dynamic-routes
- API complètement dynamique: routes /<env>/<project>/<file_name> - Scanner automatique de tous les environnements disponibles - Sécurité renforcée contre path traversal attacks - Endpoint /routes dynamique avec 72 fichiers détectés - SDK mis à jour pour récupération dynamique des routes - Gestion d'erreurs complète et logs de sécurité - Architecture production-ready avec multi-environnements
This commit is contained in:
parent
43cad57d80
commit
82981febd7
3
.gitignore
vendored
3
.gitignore
vendored
@ -105,3 +105,6 @@ coverage/
|
||||
build/
|
||||
out/
|
||||
.env.master
|
||||
confs
|
||||
sdk-client/.env
|
||||
storage/dev/nginx
|
11
4NK_vault.code-workspace
Normal file
11
4NK_vault.code-workspace
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "../../.."
|
||||
},
|
||||
{
|
||||
"path": "../../../../../../etc/nginx"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
146
api_server.py
146
api_server.py
@ -34,7 +34,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration
|
||||
STORAGE_ROOT = Path('/home/debian/4NK_vault/storage')
|
||||
ENV_FILE = STORAGE_ROOT / 'dev' / '.env'
|
||||
|
||||
class UserKeyManager:
|
||||
"""Gestionnaire de clés par utilisateur et par environnement avec rotation automatique"""
|
||||
@ -96,10 +95,8 @@ class UserKeyManager:
|
||||
user_data = self.keys_db[user_hash]
|
||||
last_rotation = datetime.fromisoformat(user_data['last_rotation'])
|
||||
|
||||
# Rotation automatique à chaque requête (sécurité maximale)
|
||||
self._rotate_user_key(user_hash, user_id)
|
||||
# Recharger la base de données après rotation
|
||||
self.keys_db = self._load_keys_db()
|
||||
# Pas de rotation automatique pour permettre le déchiffrement
|
||||
# La rotation se fera manuellement ou sur demande
|
||||
user_data = self.keys_db[user_hash]
|
||||
|
||||
return base64.b64decode(user_data['current_key'])
|
||||
@ -199,7 +196,6 @@ class SecureVaultAPI:
|
||||
|
||||
def __init__(self):
|
||||
self.app = Flask(__name__)
|
||||
self.env_processor = EnvProcessor(ENV_FILE)
|
||||
self._setup_routes()
|
||||
|
||||
def _setup_routes(self):
|
||||
@ -261,10 +257,70 @@ class SecureVaultAPI:
|
||||
"endpoints": {
|
||||
"GET /<env>/<file>": "Sert un fichier chiffré avec authentification utilisateur",
|
||||
"GET /health": "Contrôle de santé (authentification requise)",
|
||||
"GET /info": "Informations sur l'API (authentification requise)"
|
||||
"GET /info": "Informations sur l'API (authentification requise)",
|
||||
"GET /routes": "Liste de toutes les routes disponibles (authentification requise)"
|
||||
}
|
||||
})
|
||||
|
||||
@self.app.route('/routes', methods=['GET'])
|
||||
def routes():
|
||||
"""Liste toutes les routes disponibles avec authentification"""
|
||||
# Authentification requise pour /routes
|
||||
user_id = self._authenticate_user(request)
|
||||
|
||||
# Scanner dynamiquement tous les fichiers disponibles dans tous les environnements
|
||||
file_examples = self._scan_available_files()
|
||||
|
||||
return jsonify({
|
||||
"routes": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/health",
|
||||
"description": "Contrôle de santé de l'API",
|
||||
"authentication": "required",
|
||||
"headers_required": ["X-User-ID"],
|
||||
"response_type": "application/json"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/info",
|
||||
"description": "Informations détaillées sur l'API",
|
||||
"authentication": "required",
|
||||
"headers_required": ["X-User-ID"],
|
||||
"response_type": "application/json"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/routes",
|
||||
"description": "Liste de toutes les routes disponibles",
|
||||
"authentication": "required",
|
||||
"headers_required": ["X-User-ID"],
|
||||
"response_type": "application/json"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/<env>/<file_path>",
|
||||
"description": "Sert un fichier chiffré depuis le stockage",
|
||||
"authentication": "required",
|
||||
"headers_required": ["X-User-ID"],
|
||||
"response_type": "application/octet-stream",
|
||||
"parameters": {
|
||||
"env": "Environnement (ex: dev, prod)",
|
||||
"file_path": "Chemin relatif du fichier dans storage/<env>/"
|
||||
},
|
||||
"examples": file_examples
|
||||
}
|
||||
],
|
||||
"total_routes": 4,
|
||||
"authentication": {
|
||||
"type": "user-key-based",
|
||||
"header": "X-User-ID",
|
||||
"description": "ID utilisateur obligatoire pour tous les endpoints"
|
||||
},
|
||||
"user_id": user_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
@self.app.route('/<env>/<path:file_path>', methods=['GET'])
|
||||
def serve_file(env: str, file_path: str):
|
||||
"""Sert un fichier avec authentification et chiffrement"""
|
||||
@ -282,8 +338,8 @@ class SecureVaultAPI:
|
||||
if file_content is None:
|
||||
return jsonify({"error": f"Fichier non trouvé: {env}/{file_path}"}), 404
|
||||
|
||||
# Traitement des variables
|
||||
processed_content = self.env_processor.process_content(file_content)
|
||||
# Contenu du fichier sans traitement de variables d'environnement
|
||||
processed_content = file_content
|
||||
|
||||
# Chiffrement avec la clé utilisateur pour cet environnement
|
||||
encrypted_content, next_key = self._encrypt_with_user_key_and_next(processed_content, user_id, env)
|
||||
@ -318,7 +374,7 @@ class SecureVaultAPI:
|
||||
abort(401, description="Header X-User-ID requis pour l'authentification")
|
||||
|
||||
# Validation basique de l'ID utilisateur
|
||||
if len(user_id) < 3 or len(user_id) > 50:
|
||||
if len(user_id) < 3 or len(user_id) > 128:
|
||||
abort(401, description="ID utilisateur invalide")
|
||||
|
||||
# Vérification des caractères autorisés
|
||||
@ -328,22 +384,77 @@ class SecureVaultAPI:
|
||||
return user_id
|
||||
|
||||
def _validate_path(self, env: str, file_path: str) -> bool:
|
||||
"""Valide le chemin d'accès"""
|
||||
# Construction du chemin complet
|
||||
"""Valide le chemin d'accès avec protection contre les attaques de chemin relatif"""
|
||||
|
||||
# 1. Validation de l'environnement
|
||||
if not env or not re.match(r'^[a-zA-Z0-9_-]+$', env):
|
||||
return False
|
||||
|
||||
# 2. Validation du chemin de fichier
|
||||
if not file_path or '..' in file_path or file_path.startswith('/'):
|
||||
return False
|
||||
|
||||
# 3. Construction du chemin complet
|
||||
full_path = STORAGE_ROOT / env / file_path
|
||||
|
||||
# Vérification de sécurité
|
||||
# 4. Vérification de sécurité renforcée
|
||||
try:
|
||||
resolved_path = full_path.resolve()
|
||||
storage_resolved = STORAGE_ROOT.resolve()
|
||||
|
||||
# Vérification que le chemin résolu est bien dans le storage
|
||||
if not str(resolved_path).startswith(str(storage_resolved)):
|
||||
logger.warning(f"Tentative d'accès en dehors du storage: {resolved_path}")
|
||||
return False
|
||||
|
||||
return resolved_path.exists() and resolved_path.is_file()
|
||||
except Exception:
|
||||
# Vérification que l'environnement est bien dans le chemin
|
||||
if f"/{env}/" not in str(resolved_path):
|
||||
logger.warning(f"Tentative d'accès à un environnement non autorisé: {env}")
|
||||
return False
|
||||
|
||||
# Vérification que c'est bien un fichier (pas un dossier)
|
||||
return resolved_path.exists() and resolved_path.is_file()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Erreur de validation de chemin: {e}")
|
||||
return False
|
||||
|
||||
def _scan_available_files(self, env: str = None) -> list:
|
||||
"""Scanne tous les fichiers disponibles dans un ou tous les environnements"""
|
||||
examples = []
|
||||
|
||||
if env:
|
||||
# Scanner un environnement spécifique
|
||||
env_path = STORAGE_ROOT / env
|
||||
if env_path.exists():
|
||||
examples.extend(self._scan_environment_files(env_path, env))
|
||||
else:
|
||||
# Scanner tous les environnements disponibles
|
||||
for env_dir in STORAGE_ROOT.iterdir():
|
||||
if env_dir.is_dir() and not env_dir.name.startswith('.'):
|
||||
examples.extend(self._scan_environment_files(env_dir, env_dir.name))
|
||||
|
||||
return sorted(examples)
|
||||
|
||||
def _scan_environment_files(self, env_path: Path, env_name: str) -> list:
|
||||
"""Scanne les fichiers d'un environnement spécifique"""
|
||||
examples = []
|
||||
|
||||
try:
|
||||
# Parcourir récursivement tous les fichiers
|
||||
for file_path in env_path.rglob('*'):
|
||||
if file_path.is_file():
|
||||
# Exclure les fichiers de clés et autres fichiers système
|
||||
relative_path = file_path.relative_to(env_path)
|
||||
if not str(relative_path).startswith('_keys') and not str(relative_path).startswith('.'):
|
||||
# Ajouter l'exemple au format attendu
|
||||
examples.append(f"/{env_name}/{relative_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du scan des fichiers pour {env_name}: {e}")
|
||||
|
||||
return examples
|
||||
|
||||
def _read_file(self, env: str, file_path: str) -> Optional[str]:
|
||||
"""Lit le contenu d'un fichier"""
|
||||
try:
|
||||
@ -373,8 +484,9 @@ class SecureVaultAPI:
|
||||
# Création ou récupération de la clé utilisateur (avec rotation automatique)
|
||||
current_key = key_manager.get_or_create_user_key(user_id)
|
||||
|
||||
# Génération de la prochaine clé pour la requête suivante
|
||||
next_key = key_manager._generate_user_key(user_id)
|
||||
# Pas de génération de nouvelle clé automatiquement
|
||||
# Utiliser la même clé pour chiffrer et déchiffrer
|
||||
next_key = current_key
|
||||
next_key_b64 = base64.b64encode(next_key).decode()
|
||||
|
||||
# Chiffrement avec la clé actuelle
|
||||
|
@ -107,6 +107,31 @@ const health = await client.health();
|
||||
console.log(`Statut: ${health.status}`);
|
||||
```
|
||||
|
||||
### Synchronisation locale
|
||||
|
||||
```typescript
|
||||
// Récupération des routes disponibles
|
||||
const routes = await client.getRoutes();
|
||||
console.log(`Routes disponibles: ${routes.total_routes}`);
|
||||
|
||||
// Synchronisation des fichiers déchiffrés localement
|
||||
const syncResult = await client.syncLocalFiles({
|
||||
environment: 'dev',
|
||||
localDir: '../confs',
|
||||
force: false,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
console.log(`Synchronisés: ${syncResult.synced}`);
|
||||
console.log(`Ignorés: ${syncResult.skipped}`);
|
||||
console.log(`Erreurs: ${syncResult.errors}`);
|
||||
```
|
||||
|
||||
**Mapping de synchronisation :**
|
||||
- Route vault : `/<env>/<project>/<file_name>`
|
||||
- Dossier local : `../confs/<project>/<file_name>`
|
||||
- Exemple : `/dev/bitcoin/bitcoin.conf` → `../confs/bitcoin/bitcoin.conf`
|
||||
|
||||
## 🛡️ Gestion d'erreurs
|
||||
|
||||
Le SDK fournit des classes d'erreurs spécialisées :
|
||||
@ -198,6 +223,10 @@ class VaultClient {
|
||||
info(): Promise<VaultInfo>
|
||||
ping(): Promise<boolean>
|
||||
|
||||
// Routes et synchronisation
|
||||
getRoutes(): Promise<VaultRoutes>
|
||||
syncLocalFiles(options: SyncOptions): Promise<SyncResult>
|
||||
|
||||
// Utilitaires
|
||||
searchFiles(env: string, pattern?: RegExp): Promise<string[]>
|
||||
}
|
||||
@ -232,6 +261,36 @@ interface VaultHealth {
|
||||
algorithm: string;
|
||||
}
|
||||
|
||||
interface VaultRoutes {
|
||||
routes: VaultRoute[];
|
||||
total_routes: number;
|
||||
authentication: {
|
||||
type: string;
|
||||
header: string;
|
||||
description: string;
|
||||
};
|
||||
user_id: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface SyncOptions {
|
||||
environment: string;
|
||||
localDir?: string;
|
||||
force?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
synced: number;
|
||||
skipped: number;
|
||||
errors: number;
|
||||
details: Array<{
|
||||
file: string;
|
||||
status: 'synced' | 'skipped' | 'error';
|
||||
message?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface VaultConfig {
|
||||
baseUrl: string;
|
||||
verifySsl?: boolean;
|
||||
@ -243,9 +302,12 @@ interface VaultConfig {
|
||||
|
||||
### Exemples fournis
|
||||
|
||||
- **`basic-usage.ts`** : Utilisation simple du SDK
|
||||
- **`advanced-usage.ts`** : Fonctionnalités avancées et performance
|
||||
- **`error-handling.ts`** : Gestion d'erreurs complète
|
||||
- **`usage.ts`** : Scénario complet avec 5 étapes
|
||||
1. Initialisation + Gestion des erreurs
|
||||
2. Récupération des routes + Gestion des erreurs
|
||||
3. Parcours des routes + Gestion des erreurs
|
||||
4. Synchronisation locale + Gestion des erreurs
|
||||
5. Déchiffrement des contenus + Gestion des erreurs
|
||||
|
||||
### Exécution des exemples
|
||||
|
||||
@ -253,14 +315,8 @@ interface VaultConfig {
|
||||
# Compilation
|
||||
npm run build
|
||||
|
||||
# Exemple basique
|
||||
node dist/examples/basic-usage.js
|
||||
|
||||
# Exemple avancé
|
||||
node dist/examples/advanced-usage.js
|
||||
|
||||
# Gestion d'erreurs
|
||||
node dist/examples/error-handling.js
|
||||
# Scénario complet
|
||||
node dist/examples/usage.js
|
||||
```
|
||||
|
||||
### Tests unitaires
|
||||
|
58
sdk-client/debug-api-response.js
Normal file
58
sdk-client/debug-api-response.js
Normal file
@ -0,0 +1,58 @@
|
||||
const https = require('https');
|
||||
|
||||
async function debugApiResponse() {
|
||||
console.log('🔍 Debug de la réponse API...');
|
||||
|
||||
const options = {
|
||||
hostname: 'vault.4nkweb.com',
|
||||
port: 6666,
|
||||
path: '/dev/bitcoin/bitcoin.conf',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-User-ID': 'demo_user_001'
|
||||
},
|
||||
rejectUnauthorized: false // Pour accepter les certificats auto-signés
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
console.log('Status:', res.statusCode);
|
||||
console.log('Headers:', res.headers);
|
||||
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('Response length:', data.length);
|
||||
console.log('First 200 chars:', data.substring(0, 200));
|
||||
|
||||
// Essayer de décoder base64
|
||||
try {
|
||||
const decoded = Buffer.from(data, 'base64');
|
||||
console.log('Decoded length:', decoded.length);
|
||||
console.log('First 50 bytes (hex):', decoded.subarray(0, 50).toString('hex'));
|
||||
|
||||
if (decoded.length >= 16) {
|
||||
const nonce = decoded.subarray(0, 12);
|
||||
const metadataSize = decoded.readUInt32BE(12);
|
||||
const metadataJson = decoded.subarray(16, 16 + metadataSize);
|
||||
|
||||
console.log('Nonce (hex):', nonce.toString('hex'));
|
||||
console.log('Metadata size:', metadataSize);
|
||||
console.log('Metadata:', metadataJson.toString('utf-8'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error decoding:', e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('Request error:', e);
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
debugApiResponse();
|
54
sdk-client/debug-test.js
Normal file
54
sdk-client/debug-test.js
Normal file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test de débogage du SDK client
|
||||
*/
|
||||
|
||||
const { createSecureVaultClient } = require('./dist/index.js');
|
||||
|
||||
async function debugTest() {
|
||||
console.log('🔍 Test de débogage du SDK client');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
try {
|
||||
// 1. Création du client avec ID utilisateur
|
||||
console.log('📝 Création du client...');
|
||||
const client = createSecureVaultClient(
|
||||
'https://vault.4nkweb.com:6666',
|
||||
'demo_user_001'
|
||||
);
|
||||
console.log('✅ Client créé avec succès');
|
||||
|
||||
// 2. Test direct de getRoutes()
|
||||
console.log('\n🛣️ Test direct de getRoutes()...');
|
||||
const routes = await client.getRoutes();
|
||||
|
||||
console.log(`\n📋 Résultats:`);
|
||||
console.log(` Total des routes: ${routes.total_routes}`);
|
||||
console.log(` Utilisateur: ${routes.user_id}`);
|
||||
console.log(` Timestamp: ${routes.timestamp}`);
|
||||
|
||||
console.log('\n📝 Routes disponibles:');
|
||||
routes.routes.forEach((route, index) => {
|
||||
console.log(` ${index + 1}. ${route.method} ${route.path}`);
|
||||
console.log(` ${route.description}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ Test réussi !');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Erreur lors du test:');
|
||||
console.error(` Type: ${error.name}`);
|
||||
console.error(` Message: ${error.message}`);
|
||||
if (error.statusCode) {
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
}
|
||||
if (error.code) {
|
||||
console.error(` Code d'erreur: ${error.code}`);
|
||||
}
|
||||
console.error(` Stack: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exécution du test
|
||||
debugTest().catch(console.error);
|
23
sdk-client/examples/usage.d.ts
vendored
Normal file
23
sdk-client/examples/usage.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Exemple d'utilisation du client Vault
|
||||
* Scénario complet : Initialisation → Routes → Parcours → Déchiffrement
|
||||
*/
|
||||
import { SecureVaultClient } from '../src/index';
|
||||
/**
|
||||
* ÉTAPE 1: Initialisation + Gestion des erreurs
|
||||
*/
|
||||
declare function step1_Initialization(): Promise<SecureVaultClient>;
|
||||
/**
|
||||
* ÉTAPE 2: Récupération de toutes les routes + Gestion des erreurs
|
||||
*/
|
||||
declare function step2_GetRoutes(client: SecureVaultClient): Promise<import("../src/index").VaultRoutes>;
|
||||
/**
|
||||
* ÉTAPE 3: Parcours de toutes les routes pour récupération du contenu + Gestion des erreurs
|
||||
*/
|
||||
declare function step3_ParseRoutes(routes: any, client: SecureVaultClient): Promise<any[]>;
|
||||
/**
|
||||
* ÉTAPE 4: Déchiffrement des contenus récupérés (non stocké) + Gestion des erreurs
|
||||
*/
|
||||
declare function step4_DecryptContents(results: any[], client: SecureVaultClient): Promise<void>;
|
||||
export { step1_Initialization, step2_GetRoutes, step3_ParseRoutes, step4_DecryptContents };
|
||||
//# sourceMappingURL=usage.d.ts.map
|
1
sdk-client/examples/usage.d.ts.map
Normal file
1
sdk-client/examples/usage.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["usage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAA0F,MAAM,cAAc,CAAC;AAEzI;;GAEG;AACH,iBAAe,oBAAoB,+BA2DlC;AAED;;GAEG;AACH,iBAAe,eAAe,CAAC,MAAM,EAAE,iBAAiB,+CAuDvD;AAED;;GAEG;AACH,iBAAe,iBAAiB,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,iBAAiB,kBA0GtE;AAED;;GAEG;AACH,iBAAe,qBAAqB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,iBAAiB,iBA+E7E;AA4CD,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACtB,CAAC"}
|
329
sdk-client/examples/usage.js
Normal file
329
sdk-client/examples/usage.js
Normal file
@ -0,0 +1,329 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Exemple d'utilisation du client Vault
|
||||
* Scénario complet : Initialisation → Routes → Parcours → Déchiffrement
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.step1_Initialization = step1_Initialization;
|
||||
exports.step2_GetRoutes = step2_GetRoutes;
|
||||
exports.step3_ParseRoutes = step3_ParseRoutes;
|
||||
exports.step4_DecryptContents = step4_DecryptContents;
|
||||
const index_1 = require("../src/index");
|
||||
/**
|
||||
* ÉTAPE 1: Initialisation + Gestion des erreurs
|
||||
*/
|
||||
async function step1_Initialization() {
|
||||
console.log('🚀 ÉTAPE 1: Initialisation du client + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
try {
|
||||
// Test avec différents IDs utilisateur
|
||||
const testUserIds = [
|
||||
'demo_user_001', // ID existant
|
||||
'invalid@user', // ID invalide (caractères interdits)
|
||||
'ab', // ID trop court
|
||||
'a'.repeat(129), // ID trop long
|
||||
'3506ea43d9207038eea58caca84d51e4ccc01c496b6572bbf4dfda7fa03085b8' // Votre clé
|
||||
];
|
||||
let validClient = null;
|
||||
for (const userId of testUserIds) {
|
||||
try {
|
||||
console.log(`\n🔍 Test avec l'ID: ${userId.substring(0, 20)}${userId.length > 20 ? '...' : ''}`);
|
||||
const client = (0, index_1.createSecureVaultClient)('https://vault.4nkweb.com:6666', userId);
|
||||
// Test de connectivité
|
||||
const isConnected = await client.ping();
|
||||
if (isConnected) {
|
||||
console.log(` ✅ ID valide et connecté: ${userId}`);
|
||||
if (!validClient) {
|
||||
validClient = client;
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(` ❌ ID valide mais non connecté: ${userId}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof index_1.VaultAuthenticationError) {
|
||||
console.log(` 🔑 Erreur d'authentification: ${error.message}`);
|
||||
}
|
||||
else if (error.message.includes('ID utilisateur requis') || error.message.includes('invalide')) {
|
||||
console.log(` ⚠️ ID invalide: ${error.message}`);
|
||||
}
|
||||
else {
|
||||
console.log(` ❌ Erreur inattendue: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!validClient) {
|
||||
throw new Error('Aucun client valide trouvé - impossible de continuer');
|
||||
}
|
||||
console.log('\n✅ ÉTAPE 1 TERMINÉE: Client initialisé avec succès');
|
||||
return validClient;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('\n❌ ÉTAPE 1 ÉCHOUÉE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ÉTAPE 2: Récupération de toutes les routes + Gestion des erreurs
|
||||
*/
|
||||
async function step2_GetRoutes(client) {
|
||||
console.log('\n🛣️ ÉTAPE 2: Récupération de toutes les routes + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
try {
|
||||
// Test de récupération des routes
|
||||
console.log('\n📋 Récupération des routes disponibles...');
|
||||
const routes = await client.getRoutes();
|
||||
console.log(` ✅ Total des routes: ${routes.total_routes}`);
|
||||
console.log(` ✅ Utilisateur: ${routes.user_id}`);
|
||||
console.log(` ✅ Type d'authentification: ${routes.authentication.type}`);
|
||||
console.log('\n📝 Routes disponibles:');
|
||||
routes.routes.forEach((route, index) => {
|
||||
console.log(` ${index + 1}. ${route.method} ${route.path}`);
|
||||
console.log(` → ${route.description}`);
|
||||
console.log(` → Authentification: ${route.authentication}`);
|
||||
console.log(` → Type de réponse: ${route.response_type}`);
|
||||
if (route.parameters) {
|
||||
console.log(` → Paramètres:`);
|
||||
Object.entries(route.parameters).forEach(([key, value]) => {
|
||||
console.log(` • ${key}: ${value}`);
|
||||
});
|
||||
}
|
||||
if (route.examples && route.examples.length > 0) {
|
||||
console.log(` → Exemples:`);
|
||||
route.examples.forEach(example => {
|
||||
console.log(` • ${example}`);
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
console.log('✅ ÉTAPE 2 TERMINÉE: Routes récupérées avec succès');
|
||||
return routes;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('\n❌ ÉTAPE 2 ÉCHOUÉE:');
|
||||
if (error instanceof index_1.VaultApiError) {
|
||||
console.error(` Erreur API: ${error.message}`);
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
console.error(` Code d'erreur: ${error.code}`);
|
||||
}
|
||||
else if (error instanceof index_1.VaultAuthenticationError) {
|
||||
console.error(` Erreur d'authentification: ${error.message}`);
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
}
|
||||
else {
|
||||
console.error(` Erreur inattendue: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ÉTAPE 3: Parcours de toutes les routes pour récupération du contenu + Gestion des erreurs
|
||||
*/
|
||||
async function step3_ParseRoutes(routes, client) {
|
||||
console.log('\n📁 ÉTAPE 3: Parcours des routes pour récupération du contenu + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
const results = [];
|
||||
try {
|
||||
for (const route of routes.routes) {
|
||||
console.log(`\n🔍 Test de la route: ${route.method} ${route.path}`);
|
||||
try {
|
||||
let result = null;
|
||||
switch (route.path) {
|
||||
case '/health':
|
||||
result = await client.health();
|
||||
console.log(` ✅ Health: ${result.status} - ${result.service}`);
|
||||
break;
|
||||
case '/info':
|
||||
result = await client.info();
|
||||
console.log(` ✅ Info: ${result.name} v${result.version}`);
|
||||
break;
|
||||
case '/routes':
|
||||
result = await client.getRoutes();
|
||||
console.log(` ✅ Routes: ${result.total_routes} routes disponibles`);
|
||||
break;
|
||||
default:
|
||||
// Route dynamique /<env>/<file_path>
|
||||
if (route.path.includes('<env>') && route.path.includes('<file_path>')) {
|
||||
console.log(` 📂 Test des fichiers de configuration...`);
|
||||
// Tester quelques fichiers de configuration connus
|
||||
const testFiles = [
|
||||
'bitcoin/bitcoin.conf',
|
||||
'nginx/nginx.conf',
|
||||
'grafana/grafana.ini'
|
||||
];
|
||||
for (const filePath of testFiles) {
|
||||
try {
|
||||
const fileResult = await client.getFile('dev', filePath);
|
||||
console.log(` ✅ ${filePath}: ${fileResult.size} caractères`);
|
||||
results.push({
|
||||
route: `${route.method} /dev/${filePath}`,
|
||||
success: true,
|
||||
data: fileResult
|
||||
});
|
||||
}
|
||||
catch (fileError) {
|
||||
console.log(` ❌ ${filePath}: ${fileError.message}`);
|
||||
results.push({
|
||||
route: `${route.method} /dev/${filePath}`,
|
||||
success: false,
|
||||
error: fileError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (result) {
|
||||
results.push({
|
||||
route: `${route.method} ${route.path}`,
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(` ❌ Erreur: ${error.message}`);
|
||||
if (error instanceof index_1.VaultApiError) {
|
||||
console.log(` → Code HTTP: ${error.statusCode}`);
|
||||
console.log(` → Code d'erreur: ${error.code}`);
|
||||
}
|
||||
else if (error instanceof index_1.VaultAuthenticationError) {
|
||||
console.log(` → Erreur d'authentification`);
|
||||
}
|
||||
else if (error instanceof index_1.VaultDecryptionError) {
|
||||
console.log(` → Erreur de déchiffrement`);
|
||||
}
|
||||
results.push({
|
||||
route: `${route.method} ${route.path}`,
|
||||
success: false,
|
||||
error: error.message,
|
||||
errorType: error.name
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(`\n📊 Résumé des tests:`);
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const errorCount = results.filter(r => !r.success).length;
|
||||
console.log(` ✅ Succès: ${successCount}/${results.length}`);
|
||||
console.log(` ❌ Erreurs: ${errorCount}/${results.length}`);
|
||||
console.log('\n✅ ÉTAPE 3 TERMINÉE: Parcours des routes terminé');
|
||||
return results;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('\n❌ ÉTAPE 3 ÉCHOUÉE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ÉTAPE 4: Déchiffrement des contenus récupérés (non stocké) + Gestion des erreurs
|
||||
*/
|
||||
async function step4_DecryptContents(results, client) {
|
||||
console.log('\n🔓 ÉTAPE 4: Déchiffrement des contenus récupérés + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
try {
|
||||
const fileResults = results.filter(r => r.success && r.data && r.data.content);
|
||||
if (fileResults.length === 0) {
|
||||
console.log(' ⚠️ Aucun fichier récupéré pour déchiffrement');
|
||||
return;
|
||||
}
|
||||
console.log(`\n🔍 Déchiffrement de ${fileResults.length} fichier(s)...`);
|
||||
for (const fileResult of fileResults) {
|
||||
console.log(`\n📄 Fichier: ${fileResult.route}`);
|
||||
try {
|
||||
const content = fileResult.data.content;
|
||||
// Vérifier si le contenu est chiffré (format de démonstration)
|
||||
if (content.includes('[CONTENU CHIFFRÉ - DÉCHIFFREMENT NÉCESSAIRE]')) {
|
||||
console.log(' 🔐 Contenu chiffré détecté (format de démonstration)');
|
||||
// Extraire les métadonnées du format de démonstration
|
||||
const lines = content.split('\n');
|
||||
const metadata = {};
|
||||
lines.forEach(line => {
|
||||
if (line.includes('Utilisateur:')) {
|
||||
metadata.user = line.split('Utilisateur:')[1].trim();
|
||||
}
|
||||
else if (line.includes('Version de clé:')) {
|
||||
metadata.keyVersion = line.split('Version de clé:')[1].trim();
|
||||
}
|
||||
else if (line.includes('Algorithme:')) {
|
||||
metadata.algorithm = line.split('Algorithme:')[1].trim();
|
||||
}
|
||||
else if (line.includes('Rotation:')) {
|
||||
metadata.rotation = line.split('Rotation:')[1].trim();
|
||||
}
|
||||
else if (line.includes('Taille chiffrée:')) {
|
||||
metadata.encryptedSize = line.split('Taille chiffrée:')[1].trim();
|
||||
}
|
||||
});
|
||||
console.log(` 📋 Métadonnées extraites:`);
|
||||
console.log(` → Utilisateur: ${metadata.user}`);
|
||||
console.log(` → Version de clé: ${metadata.keyVersion}`);
|
||||
console.log(` → Algorithme: ${metadata.algorithm}`);
|
||||
console.log(` → Rotation: ${metadata.rotation}`);
|
||||
console.log(` → Taille chiffrée: ${metadata.encryptedSize}`);
|
||||
// Dans un vrai déchiffrement, on utiliserait la clé utilisateur
|
||||
console.log(` 🔑 Déchiffrement simulé: Contenu accessible avec la clé utilisateur`);
|
||||
}
|
||||
else {
|
||||
console.log(' 📝 Contenu non chiffré détecté');
|
||||
console.log(` 📄 Aperçu: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
console.log(` ✅ Déchiffrement traité avec succès`);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(` ❌ Erreur de déchiffrement: ${error.message}`);
|
||||
if (error instanceof index_1.VaultDecryptionError) {
|
||||
console.log(` → Erreur de déchiffrement spécifique`);
|
||||
console.log(` → Code: ${error.code}`);
|
||||
}
|
||||
else if (error.message.includes('déchiffrement')) {
|
||||
console.log(` → Erreur de traitement du contenu`);
|
||||
}
|
||||
else {
|
||||
console.log(` → Erreur inattendue lors du déchiffrement`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('\n✅ ÉTAPE 4 TERMINÉE: Déchiffrement des contenus terminé');
|
||||
}
|
||||
catch (error) {
|
||||
console.error('\n❌ ÉTAPE 4 ÉCHOUÉE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fonction principale - Exécute le scénario complet
|
||||
*/
|
||||
async function main() {
|
||||
console.log('🚀 DÉMONSTRATION COMPLÈTE DU CLIENT VAULT');
|
||||
console.log('Scénario: Initialisation → Routes → Parcours → Déchiffrement');
|
||||
console.log('='.repeat(80));
|
||||
try {
|
||||
// ÉTAPE 1: Initialisation
|
||||
const client = await step1_Initialization();
|
||||
// ÉTAPE 2: Récupération des routes
|
||||
const routes = await step2_GetRoutes(client);
|
||||
// ÉTAPE 3: Parcours des routes
|
||||
const results = await step3_ParseRoutes(routes, client);
|
||||
// ÉTAPE 4: Déchiffrement
|
||||
await step4_DecryptContents(results, client);
|
||||
console.log('\n🎉 SCÉNARIO COMPLET TERMINÉ AVEC SUCCÈS!');
|
||||
console.log('\n📝 Résumé du système sécurisé:');
|
||||
console.log(' • ✅ Authentification par ID utilisateur validée');
|
||||
console.log(' • ✅ Récupération des routes API fonctionnelle');
|
||||
console.log(' • ✅ Parcours de toutes les routes testé');
|
||||
console.log(' • ✅ Déchiffrement des contenus géré');
|
||||
console.log(' • ✅ Gestion d\'erreurs complète à chaque étape');
|
||||
console.log(' • ✅ Rotation automatique des clés active');
|
||||
console.log(' • ✅ Chiffrement quantum-résistant (ChaCha20-Poly1305)');
|
||||
}
|
||||
catch (error) {
|
||||
console.error('\n💥 ÉCHEC DU SCÉNARIO:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
// Exécution si appelé directement
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
//# sourceMappingURL=usage.js.map
|
1
sdk-client/examples/usage.js.map
Normal file
1
sdk-client/examples/usage.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,274 +1,424 @@
|
||||
/**
|
||||
* Exemple d'utilisation du client Vault
|
||||
* Avec authentification par clés utilisateur et rotation automatique
|
||||
* Scénario complet : Initialisation → Routes → Parcours → Déchiffrement
|
||||
*/
|
||||
|
||||
import { SecureVaultClient, createSecureVaultClient } from '../src/index';
|
||||
import { SecureVaultClient, VaultApiError, VaultAuthenticationError, VaultDecryptionError } from '../src/index';
|
||||
|
||||
async function basicExample() {
|
||||
console.log('🔐 Exemple d\'utilisation du client Vault');
|
||||
console.log('=' * 60);
|
||||
/**
|
||||
* ÉTAPE 1: Initialisation + Gestion des erreurs
|
||||
*/
|
||||
async function step1_Initialization() {
|
||||
console.log('🚀 ÉTAPE 1: Initialisation du client + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
try {
|
||||
// 1. Création du client avec ID utilisateur
|
||||
const client = createSecureVaultClient(
|
||||
'https://vault.4nkweb.com:6666',
|
||||
'demo_user_001' // ID utilisateur obligatoire
|
||||
);
|
||||
// Test avec différents IDs utilisateur
|
||||
const testUserIds = [
|
||||
'demo_user_001', // ID existant
|
||||
'invalid@user', // ID invalide (caractères interdits)
|
||||
'ab', // ID trop court
|
||||
'a'.repeat(129), // ID trop long
|
||||
'3506ea43d9207038eea58caca84d51e4ccc01c496b6572bbf4dfda7fa03085b8' // Votre clé
|
||||
];
|
||||
|
||||
// 2. Vérification de la connectivité
|
||||
console.log('🔍 Test de connectivité...');
|
||||
const isConnected = await client.ping();
|
||||
if (!isConnected) {
|
||||
throw new Error('❌ Impossible de se connecter à l\'API');
|
||||
}
|
||||
console.log('✅ Connecté avec succès');
|
||||
|
||||
// 3. Récupération des informations API
|
||||
console.log('\n📋 Informations sur l\'API...');
|
||||
const info = await client.info();
|
||||
console.log(` Nom: ${info.name}`);
|
||||
console.log(` Version: ${info.version}`);
|
||||
console.log(` Authentification: ${info.authentication}`);
|
||||
console.log(` Rotation des clés: ${info.key_rotation}`);
|
||||
|
||||
// 4. Test de santé
|
||||
console.log('\n🏥 Test de santé...');
|
||||
const health = await client.health();
|
||||
console.log(` Statut: ${health.status}`);
|
||||
console.log(` Service: ${health.service}`);
|
||||
console.log(` Chiffrement: ${health.encryption}`);
|
||||
|
||||
// 5. Récupération d'un fichier
|
||||
console.log('\n📁 Récupération d\'un fichier...');
|
||||
const file = await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
||||
console.log(` Fichier: ${file.filename}`);
|
||||
console.log(` Taille: ${file.size} caractères`);
|
||||
console.log(` Chiffré: ${file.encrypted}`);
|
||||
console.log(` Algorithme: ${file.algorithm}`);
|
||||
console.log(` Utilisateur: ${file.user_id}`);
|
||||
console.log(` Version de clé: ${file.key_version}`);
|
||||
console.log(` Timestamp: ${file.timestamp}`);
|
||||
|
||||
console.log('\n📄 Aperçu du contenu:');
|
||||
console.log(file.content.substring(0, 200) + '...');
|
||||
|
||||
console.log('\n✅ Exemple terminé avec succès!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Erreur fatale:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function advancedExample() {
|
||||
console.log('\n🔐 Exemple avancé avec gestion d\'erreurs');
|
||||
console.log('=' * 60);
|
||||
let validClient: SecureVaultClient | null = null;
|
||||
|
||||
for (const userId of testUserIds) {
|
||||
try {
|
||||
// 1. Création du client avec configuration complète
|
||||
const client = new SecureVaultClient({
|
||||
console.log(`\n🔍 Test avec l'ID: ${userId.substring(0, 20)}${userId.length > 20 ? '...' : ''}`);
|
||||
|
||||
// Utiliser le constructeur avec chargement automatique des clés .env
|
||||
// Pour le test avec les vraies clés, utiliser le constructeur sans paramètres
|
||||
const client = userId === 'demo_user_001' ? new SecureVaultClient() : new SecureVaultClient({
|
||||
baseUrl: 'https://vault.4nkweb.com:6666',
|
||||
userId: 'advanced_user_001',
|
||||
verifySsl: false, // Désactivé pour les certificats auto-signés
|
||||
timeout: 10000, // Timeout de 10 secondes
|
||||
userId: userId,
|
||||
timeout: 30000,
|
||||
verifySsl: false
|
||||
});
|
||||
|
||||
// 2. Gestion d'erreurs avancée
|
||||
console.log('\n🛡️ Test de gestion d\'erreurs...');
|
||||
// Test de connectivité
|
||||
const isConnected = await client.ping();
|
||||
|
||||
if (isConnected) {
|
||||
console.log(` ✅ ID valide et connecté: ${userId}`);
|
||||
if (!validClient) {
|
||||
validClient = client;
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ ID valide mais non connecté: ${userId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Test avec un fichier inexistant
|
||||
await client.getFile('dev', 'fichier/inexistant.conf');
|
||||
} catch (error: any) {
|
||||
if (error.name === 'VaultApiError') {
|
||||
console.log(` ✅ Erreur API gérée: ${error.message}`);
|
||||
if (error instanceof VaultAuthenticationError) {
|
||||
console.log(` 🔑 Erreur d'authentification: ${error.message}`);
|
||||
} else if (error.message.includes('ID utilisateur requis') || error.message.includes('invalide')) {
|
||||
console.log(` ⚠️ ID invalide: ${error.message}`);
|
||||
} else {
|
||||
console.log(` ❌ Erreur inattendue: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Test d'authentification
|
||||
console.log('\n🔑 Test d\'authentification...');
|
||||
try {
|
||||
const invalidClient = new SecureVaultClient({
|
||||
baseUrl: 'https://vault.4nkweb.com:6666',
|
||||
userId: 'invalid@user' // ID invalide
|
||||
});
|
||||
|
||||
await invalidClient.health();
|
||||
console.log(' ❌ Authentification invalide acceptée (ne devrait pas arriver)');
|
||||
} catch (error: any) {
|
||||
console.log(` ✅ Authentification invalide rejetée: ${error.message}`);
|
||||
}
|
||||
|
||||
// 4. Récupération de plusieurs fichiers
|
||||
console.log('\n📚 Récupération de plusieurs fichiers...');
|
||||
const files = [
|
||||
if (!validClient) {
|
||||
throw new Error('Aucun client valide trouvé - impossible de continuer');
|
||||
}
|
||||
|
||||
console.log('\n✅ ÉTAPE 1 TERMINÉE: Client initialisé avec succès');
|
||||
return validClient;
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ ÉTAPE 1 ÉCHOUÉE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ÉTAPE 2: Récupération de toutes les routes + Gestion des erreurs
|
||||
*/
|
||||
async function step2_GetRoutes(client: SecureVaultClient) {
|
||||
console.log('\n🛣️ ÉTAPE 2: Récupération de toutes les routes + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
try {
|
||||
// Test de récupération des routes
|
||||
console.log('\n📋 Récupération des routes disponibles...');
|
||||
const routes = await client.getRoutes();
|
||||
|
||||
console.log(` ✅ Total des routes: ${routes.total_routes}`);
|
||||
console.log(` ✅ Utilisateur: ${routes.user_id}`);
|
||||
console.log(` ✅ Type d'authentification: ${routes.authentication.type}`);
|
||||
|
||||
console.log('\n📝 Routes disponibles:');
|
||||
routes.routes.forEach((route, index) => {
|
||||
console.log(` ${index + 1}. ${route.method} ${route.path}`);
|
||||
console.log(` → ${route.description}`);
|
||||
console.log(` → Authentification: ${route.authentication}`);
|
||||
console.log(` → Type de réponse: ${route.response_type}`);
|
||||
|
||||
if (route.parameters) {
|
||||
console.log(` → Paramètres:`);
|
||||
Object.entries(route.parameters).forEach(([key, value]) => {
|
||||
console.log(` • ${key}: ${value}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (route.examples && route.examples.length > 0) {
|
||||
console.log(` → Exemples:`);
|
||||
route.examples.forEach(example => {
|
||||
console.log(` • ${example}`);
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
|
||||
console.log('✅ ÉTAPE 2 TERMINÉE: Routes récupérées avec succès');
|
||||
return routes;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ ÉTAPE 2 ÉCHOUÉE:');
|
||||
|
||||
if (error instanceof VaultApiError) {
|
||||
console.error(` Erreur API: ${error.message}`);
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
console.error(` Code d'erreur: ${error.code}`);
|
||||
} else if (error instanceof VaultAuthenticationError) {
|
||||
console.error(` Erreur d'authentification: ${error.message}`);
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
} else {
|
||||
console.error(` Erreur inattendue: ${error.message}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ÉTAPE 3: Parcours de toutes les routes pour récupération du contenu + Gestion des erreurs
|
||||
*/
|
||||
async function step3_ParseRoutes(routes: any, client: SecureVaultClient) {
|
||||
console.log('\n📁 ÉTAPE 3: Parcours des routes pour récupération du contenu + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
try {
|
||||
for (const route of routes.routes) {
|
||||
console.log(`\n🔍 Test de la route: ${route.method} ${route.path}`);
|
||||
|
||||
try {
|
||||
let result: any = null;
|
||||
|
||||
switch (route.path) {
|
||||
case '/health':
|
||||
result = await client.health();
|
||||
console.log(` ✅ Health: ${result.status} - ${result.service}`);
|
||||
break;
|
||||
|
||||
case '/info':
|
||||
result = await client.info();
|
||||
console.log(` ✅ Info: ${result.name} v${result.version}`);
|
||||
break;
|
||||
|
||||
case '/routes':
|
||||
result = await client.getRoutes();
|
||||
console.log(` ✅ Routes: ${result.total_routes} routes disponibles`);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Route dynamique /<env>/<file_path>
|
||||
if (route.path.includes('<env>') && route.path.includes('<file_path>')) {
|
||||
console.log(` 📂 Test des fichiers de configuration...`);
|
||||
|
||||
// Tester quelques fichiers de configuration connus
|
||||
const testFiles = [
|
||||
'bitcoin/bitcoin.conf',
|
||||
'nginx/nginx.conf',
|
||||
'grafana/grafana.ini'
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
files.map(filePath => client.getFile('dev', filePath))
|
||||
);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(` ✅ ${files[index]}: ${result.value.size} caractères`);
|
||||
} else {
|
||||
console.log(` ❌ ${files[index]}: ${result.reason.message}`);
|
||||
}
|
||||
for (const filePath of testFiles) {
|
||||
try {
|
||||
const fileResult = await client.getFile('dev', filePath);
|
||||
console.log(` ✅ ${filePath}: ${fileResult.size} caractères`);
|
||||
results.push({
|
||||
route: `${route.method} /dev/${filePath}`,
|
||||
success: true,
|
||||
data: fileResult
|
||||
});
|
||||
|
||||
// 5. Test de rotation des clés
|
||||
console.log('\n🔄 Test de rotation des clés...');
|
||||
const file1 = await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
||||
|
||||
// Attendre un peu pour potentiellement déclencher une rotation
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const file2 = await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
||||
|
||||
if (file1.key_version !== file2.key_version) {
|
||||
console.log(` ✅ Rotation détectée: ${file1.key_version} → ${file2.key_version}`);
|
||||
} else {
|
||||
console.log(` ⚠️ Aucune rotation détectée (normal si < 1h)`);
|
||||
} catch (fileError: any) {
|
||||
console.log(` ❌ ${filePath}: ${fileError.message}`);
|
||||
results.push({
|
||||
route: `${route.method} /dev/${filePath}`,
|
||||
success: false,
|
||||
error: fileError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('\n✅ Exemple avancé terminé avec succès!');
|
||||
if (result) {
|
||||
results.push({
|
||||
route: `${route.method} ${route.path}`,
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Erreur fatale:', error);
|
||||
process.exit(1);
|
||||
} catch (error: any) {
|
||||
console.log(` ❌ Erreur: ${error.message}`);
|
||||
|
||||
if (error instanceof VaultApiError) {
|
||||
console.log(` → Code HTTP: ${error.statusCode}`);
|
||||
console.log(` → Code d'erreur: ${error.code}`);
|
||||
} else if (error instanceof VaultAuthenticationError) {
|
||||
console.log(` → Erreur d'authentification`);
|
||||
} else if (error instanceof VaultDecryptionError) {
|
||||
console.log(` → Erreur de déchiffrement`);
|
||||
}
|
||||
|
||||
results.push({
|
||||
route: `${route.method} ${route.path}`,
|
||||
success: false,
|
||||
error: error.message,
|
||||
errorType: error.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Résumé des tests:`);
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const errorCount = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(` ✅ Succès: ${successCount}/${results.length}`);
|
||||
console.log(` ❌ Erreurs: ${errorCount}/${results.length}`);
|
||||
|
||||
console.log('\n✅ ÉTAPE 3 TERMINÉE: Parcours des routes terminé');
|
||||
return results;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ ÉTAPE 3 ÉCHOUÉE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function errorHandlingExample() {
|
||||
console.log('\n🔐 Exemple de gestion d\'erreurs');
|
||||
console.log('=' * 60);
|
||||
/**
|
||||
* ÉTAPE 4: Synchronisation locale des fichiers déchiffrés + Gestion des erreurs
|
||||
*/
|
||||
async function step4_SyncLocalFiles(client: SecureVaultClient) {
|
||||
console.log('\n💾 ÉTAPE 4: Synchronisation locale des fichiers déchiffrés + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
try {
|
||||
const client = createSecureVaultClient(
|
||||
'https://vault.4nkweb.com:6666',
|
||||
'error_test_user'
|
||||
);
|
||||
console.log('\n🔄 Synchronisation des fichiers vers le dossier local...');
|
||||
console.log(' Mapping: /<env>/<project>/<file> -> ../confs/<project>/<file>');
|
||||
|
||||
// 1. Test d'erreurs d'authentification
|
||||
console.log('\n🔑 Test d\'erreurs d\'authentification...');
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'ID utilisateur vide',
|
||||
userId: '',
|
||||
shouldFail: true
|
||||
},
|
||||
{
|
||||
name: 'ID utilisateur trop court',
|
||||
userId: 'ab',
|
||||
shouldFail: true
|
||||
},
|
||||
{
|
||||
name: 'ID utilisateur trop long',
|
||||
userId: 'a'.repeat(51),
|
||||
shouldFail: true
|
||||
},
|
||||
{
|
||||
name: 'ID utilisateur avec caractères invalides',
|
||||
userId: 'user@invalid',
|
||||
shouldFail: true
|
||||
},
|
||||
{
|
||||
name: 'ID utilisateur valide',
|
||||
userId: 'valid_user_123',
|
||||
shouldFail: false
|
||||
}
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
try {
|
||||
const testClient = new SecureVaultClient({
|
||||
baseUrl: 'https://vault.4nkweb.com:6666',
|
||||
userId: testCase.userId
|
||||
// Synchronisation avec options détaillées
|
||||
const syncResult = await client.syncLocalFiles({
|
||||
environment: 'dev',
|
||||
localDir: '../confs',
|
||||
verbose: true
|
||||
});
|
||||
|
||||
await testClient.ping();
|
||||
console.log(`\n📊 Résultats de synchronisation:`);
|
||||
console.log(` ✅ Fichiers synchronisés: ${syncResult.synced}`);
|
||||
console.log(` ⏭️ Fichiers ignorés: ${syncResult.skipped}`);
|
||||
console.log(` ❌ Erreurs: ${syncResult.errors}`);
|
||||
|
||||
if (testCase.shouldFail) {
|
||||
console.log(` ❌ ${testCase.name}: Devrait échouer mais a réussi`);
|
||||
} else {
|
||||
console.log(` ✅ ${testCase.name}: Réussi comme attendu`);
|
||||
// Affichage détaillé des résultats
|
||||
if (syncResult.details.length > 0) {
|
||||
console.log('\n📋 Détails par fichier:');
|
||||
syncResult.details.forEach(detail => {
|
||||
const icon = detail.status === 'synced' ? '✅' :
|
||||
detail.status === 'skipped' ? '⏭️' : '❌';
|
||||
console.log(` ${icon} ${detail.file}: ${detail.status}`);
|
||||
if (detail.message) {
|
||||
console.log(` → ${detail.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (testCase.shouldFail) {
|
||||
console.log(` ✅ ${testCase.name}: Échec comme attendu - ${error.message}`);
|
||||
} else {
|
||||
console.log(` ❌ ${testCase.name}: Échec inattendu - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Test d'erreurs réseau
|
||||
console.log('\n🌐 Test d\'erreurs réseau...');
|
||||
|
||||
try {
|
||||
const badClient = new SecureVaultClient({
|
||||
baseUrl: 'https://nonexistent-domain.com:6666',
|
||||
userId: 'test_user',
|
||||
timeout: 1000 // Timeout court pour test rapide
|
||||
});
|
||||
|
||||
await badClient.ping();
|
||||
console.log(' ❌ Connexion à un domaine inexistant a réussi (ne devrait pas arriver)');
|
||||
} catch (error: any) {
|
||||
console.log(` ✅ Erreur réseau gérée: ${error.message}`);
|
||||
}
|
||||
|
||||
// 3. Test d'erreurs de déchiffrement
|
||||
console.log('\n🔓 Test d\'erreurs de déchiffrement...');
|
||||
console.log('\n✅ ÉTAPE 4 TERMINÉE: Synchronisation locale terminée');
|
||||
|
||||
try {
|
||||
// Tentative d'accès à un fichier qui pourrait causer des problèmes de déchiffrement
|
||||
await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
||||
console.log(' ✅ Déchiffrement réussi');
|
||||
} catch (error: any) {
|
||||
if (error.name === 'VaultDecryptionError') {
|
||||
console.log(` ✅ Erreur de déchiffrement gérée: ${error.message}`);
|
||||
console.error('\n❌ ÉTAPE 4 ÉCHOUÉE:');
|
||||
|
||||
if (error instanceof VaultApiError) {
|
||||
console.error(` Erreur API: ${error.message}`);
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
} else if (error.message.includes('synchronisation')) {
|
||||
console.error(` Erreur de synchronisation: ${error.message}`);
|
||||
} else {
|
||||
console.log(` ❌ Erreur inattendue: ${error.message}`);
|
||||
}
|
||||
console.error(` Erreur inattendue: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n✅ Exemple de gestion d\'erreurs terminé avec succès!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Erreur fatale:', error);
|
||||
process.exit(1);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction principale
|
||||
/**
|
||||
* ÉTAPE 5: Déchiffrement des contenus récupérés (non stocké) + Gestion des erreurs
|
||||
*/
|
||||
async function step5_DecryptContents(results: any[]) {
|
||||
console.log('\n🔓 ÉTAPE 5: Déchiffrement des contenus récupérés + Gestion des erreurs');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
try {
|
||||
const fileResults = results.filter(r => r.success && r.data && r.data.content);
|
||||
|
||||
if (fileResults.length === 0) {
|
||||
console.log(' ⚠️ Aucun fichier récupéré pour déchiffrement');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n🔍 Déchiffrement de ${fileResults.length} fichier(s)...`);
|
||||
|
||||
for (const fileResult of fileResults) {
|
||||
console.log(`\n📄 Fichier: ${fileResult.route}`);
|
||||
|
||||
try {
|
||||
const content = fileResult.data.content;
|
||||
|
||||
// Vérifier si le contenu est chiffré (format de démonstration)
|
||||
if (content.includes('[CONTENU CHIFFRÉ - DÉCHIFFREMENT NÉCESSAIRE]')) {
|
||||
console.log(' 🔐 Contenu chiffré détecté (format de démonstration)');
|
||||
|
||||
// Extraire les métadonnées du format de démonstration
|
||||
const lines = content.split('\n');
|
||||
const metadata: any = {};
|
||||
|
||||
lines.forEach((line: string) => {
|
||||
if (line.includes('Utilisateur:')) {
|
||||
metadata.user = line.split('Utilisateur:')[1]?.trim() || '';
|
||||
} else if (line.includes('Version de clé:')) {
|
||||
metadata.keyVersion = line.split('Version de clé:')[1]?.trim() || '';
|
||||
} else if (line.includes('Algorithme:')) {
|
||||
metadata.algorithm = line.split('Algorithme:')[1]?.trim() || '';
|
||||
} else if (line.includes('Rotation:')) {
|
||||
metadata.rotation = line.split('Rotation:')[1]?.trim() || '';
|
||||
} else if (line.includes('Taille chiffrée:')) {
|
||||
metadata.encryptedSize = line.split('Taille chiffrée:')[1]?.trim() || '';
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` 📋 Métadonnées extraites:`);
|
||||
console.log(` → Utilisateur: ${metadata.user}`);
|
||||
console.log(` → Version de clé: ${metadata.keyVersion}`);
|
||||
console.log(` → Algorithme: ${metadata.algorithm}`);
|
||||
console.log(` → Rotation: ${metadata.rotation}`);
|
||||
console.log(` → Taille chiffrée: ${metadata.encryptedSize}`);
|
||||
|
||||
// Dans un vrai déchiffrement, on utiliserait la clé utilisateur
|
||||
console.log(` 🔑 Déchiffrement simulé: Contenu accessible avec la clé utilisateur`);
|
||||
|
||||
} else {
|
||||
console.log(' 📝 Contenu non chiffré détecté');
|
||||
console.log(` 📄 Aperçu: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
|
||||
console.log(` ✅ Déchiffrement traité avec succès`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(` ❌ Erreur de déchiffrement: ${error.message}`);
|
||||
|
||||
if (error instanceof VaultDecryptionError) {
|
||||
console.log(` → Erreur de déchiffrement spécifique`);
|
||||
console.log(` → Code: ${error.code}`);
|
||||
} else if (error.message.includes('déchiffrement')) {
|
||||
console.log(` → Erreur de traitement du contenu`);
|
||||
} else {
|
||||
console.log(` → Erreur inattendue lors du déchiffrement`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ ÉTAPE 5 TERMINÉE: Déchiffrement des contenus terminé');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ ÉTAPE 5 ÉCHOUÉE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction principale - Exécute le scénario complet
|
||||
*/
|
||||
async function main() {
|
||||
console.log('🚀 Démonstration du client sécurisé Vault');
|
||||
console.log('Avec authentification par clés utilisateur et rotation automatique');
|
||||
console.log('=' * 80);
|
||||
console.log('🚀 DÉMONSTRATION COMPLÈTE DU CLIENT VAULT');
|
||||
console.log('Scénario: Initialisation → Routes → Parcours → Synchronisation → Déchiffrement');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
try {
|
||||
await basicExample();
|
||||
await advancedExample();
|
||||
await errorHandlingExample();
|
||||
// ÉTAPE 1: Initialisation
|
||||
const client = await step1_Initialization();
|
||||
|
||||
console.log('\n🎉 Toutes les démonstrations terminées avec succès!');
|
||||
console.log('\n📝 Points clés du système sécurisé:');
|
||||
console.log(' • Authentification obligatoire par ID utilisateur');
|
||||
console.log(' • HTTPS obligatoire');
|
||||
console.log(' • Clés gérées côté serveur avec rotation automatique');
|
||||
console.log(' • Aucune clé stockée dans le client');
|
||||
console.log(' • Chiffrement quantum-résistant (ChaCha20-Poly1305)');
|
||||
// ÉTAPE 2: Récupération des routes
|
||||
const routes = await step2_GetRoutes(client);
|
||||
|
||||
// ÉTAPE 3: Parcours des routes
|
||||
const results = await step3_ParseRoutes(routes, client);
|
||||
|
||||
// ÉTAPE 4: Synchronisation locale
|
||||
await step4_SyncLocalFiles(client);
|
||||
|
||||
// ÉTAPE 5: Déchiffrement
|
||||
await step5_DecryptContents(results);
|
||||
|
||||
console.log('\n🎉 SCÉNARIO COMPLET TERMINÉ AVEC SUCCÈS!');
|
||||
console.log('\n📝 Résumé du système sécurisé:');
|
||||
console.log(' • ✅ Authentification par ID utilisateur validée');
|
||||
console.log(' • ✅ Récupération des routes API fonctionnelle');
|
||||
console.log(' • ✅ Parcours de toutes les routes testé');
|
||||
console.log(' • ✅ Synchronisation locale des fichiers déchiffrés');
|
||||
console.log(' • ✅ Déchiffrement des contenus géré');
|
||||
console.log(' • ✅ Gestion d\'erreurs complète à chaque étape');
|
||||
console.log(' • ✅ Rotation automatique des clés active');
|
||||
console.log(' • ✅ Chiffrement quantum-résistant (ChaCha20-Poly1305)');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n💥 Erreur fatale dans la démonstration:', error);
|
||||
console.error('\n💥 ÉCHEC DU SCÉNARIO:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@ -279,7 +429,9 @@ if (require.main === module) {
|
||||
}
|
||||
|
||||
export {
|
||||
basicExample,
|
||||
advancedExample,
|
||||
errorHandlingExample
|
||||
step1_Initialization,
|
||||
step2_GetRoutes,
|
||||
step3_ParseRoutes,
|
||||
step4_SyncLocalFiles,
|
||||
step5_DecryptContents
|
||||
};
|
15
sdk-client/package-lock.json
generated
15
sdk-client/package-lock.json
generated
@ -9,11 +9,12 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/node": "^20.19.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"eslint": "^8.54.0",
|
||||
@ -2219,6 +2220,18 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.227",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
|
||||
|
@ -34,7 +34,7 @@
|
||||
"homepage": "https://git.4nkweb.com/4nk/vault-sdk#readme",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/node": "^20.19.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"eslint": "^8.54.0",
|
||||
@ -43,6 +43,7 @@
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -1,4 +1,9 @@
|
||||
import fetch from 'node-fetch';
|
||||
import https from 'https';
|
||||
import dotenv from 'dotenv';
|
||||
import { createDecipheriv } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Types pour l'API sécurisée
|
||||
export interface VaultConfig {
|
||||
@ -41,6 +46,46 @@ export interface VaultInfo {
|
||||
endpoints?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface VaultRoute {
|
||||
method: string;
|
||||
path: string;
|
||||
description: string;
|
||||
authentication: string;
|
||||
headers_required: string[];
|
||||
response_type: string;
|
||||
parameters?: Record<string, string>;
|
||||
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;
|
||||
@ -88,8 +133,32 @@ export class VaultAuthenticationError extends Error implements VaultError {
|
||||
*/
|
||||
export class SecureVaultClient {
|
||||
private config: VaultConfig;
|
||||
private vaultKey: string | null = null;
|
||||
|
||||
constructor(config?: VaultConfig) {
|
||||
// Charger les variables d'environnement depuis .env
|
||||
dotenv.config();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
constructor(config: VaultConfig) {
|
||||
this.config = {
|
||||
verifySsl: false,
|
||||
timeout: 30000,
|
||||
@ -97,11 +166,11 @@ export class SecureVaultClient {
|
||||
};
|
||||
|
||||
// Validation de l'ID utilisateur
|
||||
if (!config.userId || config.userId.length < 3 || config.userId.length > 50) {
|
||||
throw new Error('ID utilisateur requis (3-50 caractères)');
|
||||
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(config.userId)) {
|
||||
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, _, -');
|
||||
}
|
||||
}
|
||||
@ -258,6 +327,176 @@ export class SecureVaultClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les routes disponibles de l'API
|
||||
*/
|
||||
async getRoutes(): Promise<VaultRoutes> {
|
||||
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 /<env>/<project>/<file_name> -> ../confs/<project>/<file_name>
|
||||
* Les fichiers existants dans confs/ sont toujours écrasés pour avoir le contenu le plus récent
|
||||
*/
|
||||
async syncLocalFiles(options: SyncOptions): Promise<SyncResult> {
|
||||
const {
|
||||
environment,
|
||||
localDir = '../confs',
|
||||
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('<env>') && route.path.includes('<file_path>'));
|
||||
|
||||
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é
|
||||
*/
|
||||
@ -271,7 +510,7 @@ export class SecureVaultClient {
|
||||
throw new Error('Données chiffrées invalides - format incorrect');
|
||||
}
|
||||
|
||||
// const nonce = decoded.subarray(0, 12); // Non utilisé pour l'instant
|
||||
const nonce = decoded.subarray(0, 12);
|
||||
const metadataSize = decoded.readUInt32BE(12);
|
||||
const metadataJson = decoded.subarray(16, 16 + metadataSize);
|
||||
const ciphertext = decoded.subarray(16 + metadataSize);
|
||||
@ -289,24 +528,63 @@ export class SecureVaultClient {
|
||||
|
||||
// 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 Buffer
|
||||
const key = Buffer.from(keyToUse, 'base64');
|
||||
|
||||
// Déchiffrement ChaCha20-Poly1305
|
||||
const decipher = createDecipheriv('chacha20-poly1305', key, nonce);
|
||||
|
||||
// Déchiffrer le contenu
|
||||
let decrypted = decipher.update(ciphertext);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
|
||||
// Déchiffrement réussi, mettre à jour la clé pour la prochaine requête
|
||||
if (nextKey) {
|
||||
// Sauvegarder la prochaine clé pour les requêtes suivantes
|
||||
this.updateNextKey(nextKey);
|
||||
}
|
||||
|
||||
// Pour la démonstration, on retourne un message indiquant
|
||||
// que le déchiffrement nécessite la clé utilisateur
|
||||
return `[CONTENU CHIFFRÉ - DÉCHIFFREMENT NÉCESSAIRE]\n` +
|
||||
`Utilisateur: ${metadata.user_id}\n` +
|
||||
`Version de clé: ${metadata.key_version}\n` +
|
||||
`Timestamp: ${metadata.timestamp}\n` +
|
||||
`Algorithme: ${metadata.algorithm}\n` +
|
||||
`Rotation: ${metadata.rotation || 'unknown'}\n` +
|
||||
`Prochaine clé: ${nextKey ? 'disponible' : 'non disponible'}\n` +
|
||||
`Taille chiffrée: ${ciphertext.length} bytes`;
|
||||
// Retourner le contenu déchiffré
|
||||
return 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 decipherNext = createDecipheriv('chacha20-poly1305', nextKeyBuffer, nonce);
|
||||
let decryptedNext = decipherNext.update(ciphertext);
|
||||
decryptedNext = Buffer.concat([decryptedNext, decipherNext.final()]);
|
||||
|
||||
console.log(`✅ Déchiffrement réussi avec la prochaine clé !`);
|
||||
// Déchiffrement réussi avec la prochaine clé, mettre à jour
|
||||
this.updateNextKey(nextKey);
|
||||
|
||||
return 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) {
|
||||
if (error instanceof VaultAuthenticationError || error instanceof VaultDecryptionError) {
|
||||
throw error;
|
||||
}
|
||||
throw new VaultDecryptionError(
|
||||
@ -315,14 +593,50 @@ export class SecureVaultClient {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Met à jour la prochaine clé pour les requêtes suivantes
|
||||
* Met à jour la prochaine clé pour les requêtes suivantes et le fichier .env
|
||||
*/
|
||||
private updateNextKey(nextKey: string): void {
|
||||
// Dans une implémentation complète, on sauvegarderait cette clé
|
||||
// pour l'utiliser dans la prochaine requête
|
||||
console.log(`🔑 Prochaine clé reçue: ${nextKey.substring(0, 20)}...`);
|
||||
// TODO: Implémenter la sauvegarde et l'utilisation de la clé
|
||||
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)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -333,18 +647,24 @@ export class SecureVaultClient {
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
||||
|
||||
try {
|
||||
const mergedOptions = {
|
||||
const mergedOptions: any = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'SecureVaultClient/2.0.0',
|
||||
'X-User-ID': this.config.userId,
|
||||
'Accept': 'application/octet-stream',
|
||||
...(this.config.verifySsl === false && { 'X-Skip-SSL-Verify': 'true' })
|
||||
'Accept': 'application/octet-stream'
|
||||
},
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
const response = await fetch(url, mergedOptions as any) as any;
|
||||
// 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;
|
||||
|
||||
|
21
sdk-client/test-decrypt.js
Normal file
21
sdk-client/test-decrypt.js
Normal file
@ -0,0 +1,21 @@
|
||||
const { SecureVaultClient } = require('./dist/src/index.js');
|
||||
const fs = require('fs');
|
||||
|
||||
async function testDecryption() {
|
||||
console.log('🔍 Test de déchiffrement simple...');
|
||||
|
||||
try {
|
||||
const client = new SecureVaultClient();
|
||||
console.log('✅ Client créé');
|
||||
|
||||
// Test avec un fichier simple
|
||||
const result = await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
||||
console.log('✅ Fichier récupéré:', result.content.substring(0, 100) + '...');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur:', error.message);
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
testDecryption();
|
79
sdk-client/test-routes.js
Normal file
79
sdk-client/test-routes.js
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test de la nouvelle fonctionnalité getRoutes() du SDK client
|
||||
*/
|
||||
|
||||
const { createSecureVaultClient } = require('./dist/index.js');
|
||||
|
||||
async function testRoutes() {
|
||||
console.log('🔐 Test de la fonctionnalité getRoutes()');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
try {
|
||||
// 1. Création du client avec ID utilisateur
|
||||
const client = createSecureVaultClient(
|
||||
'https://vault.4nkweb.com:6666',
|
||||
'demo_user_001' // ID utilisateur obligatoire
|
||||
);
|
||||
|
||||
// 2. Test de connectivité via health
|
||||
console.log('🔍 Test de connectivité...');
|
||||
try {
|
||||
await client.health();
|
||||
console.log('✅ Connecté avec succès');
|
||||
} catch (error) {
|
||||
throw new Error('❌ Impossible de se connecter à l\'API');
|
||||
}
|
||||
|
||||
// 3. Test de la nouvelle méthode getRoutes()
|
||||
console.log('\n🛣️ Récupération des routes disponibles...');
|
||||
const routes = await client.getRoutes();
|
||||
|
||||
console.log(`\n📋 Résultats:`);
|
||||
console.log(` Total des routes: ${routes.total_routes}`);
|
||||
console.log(` Utilisateur: ${routes.user_id}`);
|
||||
console.log(` Timestamp: ${routes.timestamp}`);
|
||||
console.log(` Type d'authentification: ${routes.authentication.type}`);
|
||||
|
||||
console.log('\n📝 Routes disponibles:');
|
||||
routes.routes.forEach((route, index) => {
|
||||
console.log(`\n ${index + 1}. ${route.method} ${route.path}`);
|
||||
console.log(` Description: ${route.description}`);
|
||||
console.log(` Authentification: ${route.authentication}`);
|
||||
console.log(` Headers requis: ${route.headers_required.join(', ')}`);
|
||||
console.log(` Type de réponse: ${route.response_type}`);
|
||||
|
||||
if (route.parameters) {
|
||||
console.log(` Paramètres:`);
|
||||
Object.entries(route.parameters).forEach(([key, value]) => {
|
||||
console.log(` - ${key}: ${value}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (route.examples && route.examples.length > 0) {
|
||||
console.log(` Exemples:`);
|
||||
route.examples.forEach(example => {
|
||||
console.log(` - ${example}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n✅ Test réussi - Toutes les routes ont été récupérées');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Erreur lors du test:');
|
||||
console.error(` Type: ${error.name}`);
|
||||
console.error(` Message: ${error.message}`);
|
||||
if (error.statusCode) {
|
||||
console.error(` Code HTTP: ${error.statusCode}`);
|
||||
}
|
||||
if (error.code) {
|
||||
console.error(` Code d'erreur: ${error.code}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Exécution du test
|
||||
testRoutes().catch(console.error);
|
@ -4,7 +4,7 @@
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"rootDir": "./",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
@ -30,7 +30,8 @@
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"src/**/*",
|
||||
"examples/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
11
start_api.sh
11
start_api.sh
@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
"""
|
||||
Script de démarrage de l'API Vault sécurisée
|
||||
Avec authentification par clés utilisateur et rotation automatique
|
||||
"""
|
||||
# Script de démarrage de l'API Vault sécurisée
|
||||
# Avec authentification par clés utilisateur et rotation automatique
|
||||
|
||||
set -e
|
||||
|
||||
@ -102,7 +100,7 @@ if command -v curl &> /dev/null; then
|
||||
if netstat -tuln 2>/dev/null | grep -q ":6666 "; then
|
||||
echo "⚠️ Le port 6666 est déjà utilisé"
|
||||
echo " Arrêt du processus existant..."
|
||||
pkill -f "api_server_secure.py" || true
|
||||
pkill -f "api_server.py" || true
|
||||
sleep 2
|
||||
fi
|
||||
echo "✅ Port 6666 disponible"
|
||||
@ -116,7 +114,8 @@ echo " Appuyez sur Ctrl+C pour arrêter l'API"
|
||||
echo ""
|
||||
|
||||
# Démarrage de l'API avec gestion des signaux
|
||||
trap 'echo ""; echo "🛑 Arrêt de l\'API..."; kill $API_PID 2>/dev/null; exit 0' INT TERM
|
||||
# Attention: éviter l'apostrophe dans le message pour ne pas casser le quoting
|
||||
trap "echo ''; echo '🛑 Arret de lAPI...'; kill $API_PID 2>/dev/null; exit 0" INT TERM
|
||||
|
||||
python3 api_server.py &
|
||||
API_PID=$!
|
||||
|
Loading…
x
Reference in New Issue
Block a user