refactor: Nettoyage et simplification du projet
- ✅ Suppression des fichiers redondants (api_server.py, start_api.sh, test_api.py) - ✅ Renommage des fichiers sécurisés vers les noms standards - ✅ Consolidation des exemples SDK en un seul fichier usage.ts - ✅ Suppression du client SDK déprécié - ✅ Mise à jour de la documentation principale - ✅ Simplification de la structure du projet - 🔒 Conservation de la sécurité (authentification par clés utilisateur) - 📁 Respect du stockage en lecture seule (storage/)
This commit is contained in:
parent
b13c8745e3
commit
74624711a0
235
README.md
235
README.md
@ -1,135 +1,154 @@
|
|||||||
# 4NK Vault - Documentation
|
# 4NK Vault API - Système Sécurisé
|
||||||
|
|
||||||
## Vue d'ensemble
|
API HTTPS sécurisée avec authentification par clés utilisateur et chiffrement quantique résistant.
|
||||||
|
|
||||||
Le projet 4NK Vault est un système de stockage sécurisé qui fournit une API HTTPS avec chiffrement quantique résistant pour servir des fichiers de configuration avec traitement automatique des variables d'environnement composites.
|
## 🔐 Sécurité Avancée
|
||||||
|
|
||||||
## Architecture
|
- **HTTPS obligatoire** sur le port 6666
|
||||||
|
- **Authentification par ID utilisateur** (header `X-User-ID`)
|
||||||
|
- **Clés individuelles par utilisateur ET par environnement**
|
||||||
|
- **Rotation automatique des clés** (toutes les heures)
|
||||||
|
- **Chiffrement quantique résistant** (ChaCha20-Poly1305)
|
||||||
|
- **Stockage sécurisé** dans `storage/<env>/_keys/`
|
||||||
|
|
||||||
|
## 🚀 Fonctionnalités
|
||||||
|
|
||||||
|
### Authentification
|
||||||
|
- **ID utilisateur requis** pour tous les accès
|
||||||
|
- **Validation stricte** : 3-50 caractères alphanumériques + `_` et `-`
|
||||||
|
- **Clés uniques** par combinaison utilisateur/environnement
|
||||||
|
|
||||||
|
### Gestion des clés
|
||||||
|
- **Génération automatique** de clés de 32 bytes
|
||||||
|
- **Rotation automatique** toutes les heures
|
||||||
|
- **Sauvegarde de l'ancienne clé** pour compatibilité
|
||||||
|
- **Isolation par environnement** (dev, prod, etc.)
|
||||||
|
|
||||||
|
## 📋 Endpoints
|
||||||
|
|
||||||
|
### `GET /health`
|
||||||
|
Contrôle de santé avec authentification.
|
||||||
|
|
||||||
|
**Headers requis :**
|
||||||
```
|
```
|
||||||
┌─────────────────┐ HTTPS ┌─────────────────┐ ┌─────────────────┐
|
X-User-ID: your_user_id
|
||||||
│ Client SDK │ ──────────► │ API Vault │ ──► │ Storage │
|
|
||||||
│ TypeScript │ Port 6666 │ Python Flask │ │ Files │
|
|
||||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Variables │
|
|
||||||
│ .env │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Composants
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "vault-api-secure",
|
||||||
|
"encryption": "quantum-resistant",
|
||||||
|
"algorithm": "X25519-ChaCha20-Poly1305",
|
||||||
|
"authentication": "user-key-based",
|
||||||
|
"key_rotation": "automatic",
|
||||||
|
"user_id": "your_user_id",
|
||||||
|
"timestamp": "2024-01-01T00:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 1. API Vault Server (`api_server.py`)
|
### `GET /info`
|
||||||
- **Port** : 6666
|
Informations sur l'API avec authentification.
|
||||||
- **Protocole** : HTTPS uniquement
|
|
||||||
- **Domaine** : vault.4nkweb.com
|
|
||||||
- **Chiffrement** : ChaCha20-Poly1305 (quantique résistant)
|
|
||||||
|
|
||||||
### 2. SDK Client TypeScript (`sdk-client/`)
|
### `GET /<env>/<file>`
|
||||||
- Client type-safe pour interagir avec l'API
|
Sert un fichier chiffré avec authentification utilisateur.
|
||||||
- Déchiffrement côté client
|
|
||||||
- Gestion d'erreurs avancée
|
|
||||||
|
|
||||||
### 3. Stockage (`storage/`)
|
**Headers requis :**
|
||||||
- Structure : `storage/<env>/<file>`
|
```
|
||||||
- Variables composites depuis `.env`
|
X-User-ID: your_user_id
|
||||||
- Sécurité : accès restreint au répertoire storage
|
```
|
||||||
|
|
||||||
## Fonctionnalités principales
|
|
||||||
|
|
||||||
### 🔐 Sécurité
|
|
||||||
- **Chiffrement quantique résistant** : ChaCha20-Poly1305
|
|
||||||
- **HTTPS obligatoire** : Communication sécurisée
|
|
||||||
- **Variables composites** : Résolution récursive des variables d'environnement
|
|
||||||
- **Validation des chemins** : Protection contre les accès hors du répertoire
|
|
||||||
|
|
||||||
### 📁 Service de fichiers
|
|
||||||
- **Endpoint** : `GET /<env>/<file>`
|
|
||||||
- **Variables** : Traitement automatique depuis `.env`
|
|
||||||
- **Types** : Support des fichiers texte et binaires
|
|
||||||
- **Encodage** : Base64 pour les fichiers binaires
|
|
||||||
|
|
||||||
### 🌐 API REST
|
|
||||||
- **Santé** : `GET /health`
|
|
||||||
- **Informations** : `GET /info`
|
|
||||||
- **Monitoring** : Logs détaillés et métriques
|
|
||||||
|
|
||||||
## Installation et utilisation
|
|
||||||
|
|
||||||
### Démarrage de l'API
|
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
```bash
|
```bash
|
||||||
# Installation des dépendances
|
curl -k -H "X-User-ID: demo_user_001" https://vault.4nkweb.com:6666/dev/bitcoin/bitcoin.conf
|
||||||
pip install -r requirements.txt
|
```
|
||||||
|
|
||||||
# Démarrage
|
## 🔧 Installation et utilisation
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
- Python 3.8+
|
||||||
|
- pip
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
# Cloner le repository
|
||||||
|
git clone https://git.4nkweb.com:4nk/4NK_vault.git
|
||||||
|
cd 4NK_vault
|
||||||
|
|
||||||
|
# Créer l'environnement virtuel
|
||||||
|
python3 -m venv venv_api
|
||||||
|
source venv_api/bin/activate
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Démarrage
|
||||||
|
```bash
|
||||||
|
# Démarrer l'API sécurisée
|
||||||
./start_api.sh
|
./start_api.sh
|
||||||
# ou directement
|
|
||||||
|
# Ou manuellement
|
||||||
|
source venv_api/bin/activate
|
||||||
python3 api_server.py
|
python3 api_server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Utilisation du SDK
|
### Test
|
||||||
|
|
||||||
```bash
|
|
||||||
cd sdk-client
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Exemples
|
|
||||||
node dist/examples/basic-usage.js
|
|
||||||
node dist/examples/advanced-usage.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Variables d'environnement
|
|
||||||
Les variables sont définies dans `/home/debian/4NK_vault/storage/dev/.env` et peuvent être composites :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DOMAIN=4nkweb.com
|
|
||||||
HOST=dev4.$DOMAIN
|
|
||||||
ROOT_URL=https://$HOST
|
|
||||||
LECOFFRE_FRONT_URL=$ROOT_URL/lecoffre
|
|
||||||
```
|
|
||||||
|
|
||||||
### Certificats SSL
|
|
||||||
L'API génère automatiquement des certificats auto-signés pour le développement. En production, utilisez des certificats valides.
|
|
||||||
|
|
||||||
## Documentation détaillée
|
|
||||||
|
|
||||||
- [API Specification](api-specification.md) - Spécification complète de l'API REST
|
|
||||||
- [SDK Documentation](sdk-documentation.md) - Documentation du SDK TypeScript
|
|
||||||
- [Security Model](security-model.md) - Modèle de sécurité et chiffrement
|
|
||||||
- [Deployment Guide](deployment-guide.md) - Guide de déploiement
|
|
||||||
- [API Reference](api-reference.md) - Référence complète des endpoints
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
### Tests de l'API
|
|
||||||
```bash
|
```bash
|
||||||
|
# Tester l'API sécurisée
|
||||||
python3 test_api.py
|
python3 test_api.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tests du SDK
|
## 🔐 Sécurité
|
||||||
```bash
|
|
||||||
cd sdk-client
|
### Authentification
|
||||||
npm test
|
- **ID utilisateur obligatoire** pour tous les accès
|
||||||
|
- **Validation stricte** des caractères autorisés
|
||||||
|
- **Rejet automatique** des requêtes non authentifiées
|
||||||
|
|
||||||
|
### Chiffrement
|
||||||
|
- **Clés individuelles** par utilisateur et environnement
|
||||||
|
- **ChaCha20-Poly1305** (quantum-résistant)
|
||||||
|
- **Rotation automatique** des clés
|
||||||
|
- **Métadonnées sécurisées** dans le payload
|
||||||
|
|
||||||
|
### Stockage
|
||||||
|
- **Isolation par environnement** : `storage/<env>/_keys/`
|
||||||
|
- **Fichiers protégés** dans `.gitignore`
|
||||||
|
- **Aucune clé côté client**
|
||||||
|
|
||||||
|
## 📁 Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
4NK_vault/
|
||||||
|
├── api_server.py # Serveur sécurisé principal
|
||||||
|
├── start_api.sh # Script de démarrage sécurisé
|
||||||
|
├── test_api.py # Tests de l'API sécurisée
|
||||||
|
├── requirements.txt # Dépendances Python
|
||||||
|
├── SECURITY_NOTICE.md # Avertissements de sécurité
|
||||||
|
├── storage/ # Fichiers de configuration (lecture seule)
|
||||||
|
│ ├── dev/
|
||||||
|
│ │ ├── .env # Variables d'environnement
|
||||||
|
│ │ ├── _keys/ # Clés utilisateur (auto-créé)
|
||||||
|
│ │ └── bitcoin/
|
||||||
|
│ └── prod/
|
||||||
|
│ ├── _keys/ # Clés utilisateur (auto-créé)
|
||||||
|
│ └── bitcoin/
|
||||||
|
├── sdk-client/ # Client TypeScript
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── index.ts # Client original (déprécié)
|
||||||
|
│ │ └── secure-client.ts # Client sécurisé
|
||||||
|
│ └── examples/
|
||||||
|
└── docs/ # Documentation complète
|
||||||
```
|
```
|
||||||
|
|
||||||
## Support et contribution
|
## 🌐 Domaine
|
||||||
|
|
||||||
- **Issues** : [Git Issues](https://git.4nkweb.com/4nk/vault/issues)
|
L'API est configurée pour fonctionner sur le domaine `vault.4nkweb.com` avec des certificats SSL auto-signés pour la démonstration.
|
||||||
- **Documentation** : [Wiki](https://git.4nkweb.com/4nk/vault/wiki)
|
|
||||||
- **Email** : support@4nkweb.com
|
|
||||||
|
|
||||||
## Licence
|
En production, utilisez des certificats SSL valides et implémentez un échange de clés sécurisé.
|
||||||
|
|
||||||
MIT License - Voir le fichier `LICENSE` pour plus de détails.
|
## ⚠️ Avertissements
|
||||||
|
|
||||||
---
|
Voir `SECURITY_NOTICE.md` pour les détails sur les limitations de sécurité en mode démonstration.
|
||||||
|
|
||||||
**Version** : 1.0.0
|
|
||||||
**API** : vault.4nkweb.com:6666
|
|
||||||
**Chiffrement** : ChaCha20-Poly1305 (quantique résistant)
|
|
673
api_server.py
673
api_server.py
@ -1,117 +1,165 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
API HTTPS sécurisée avec chiffrement quantique résistant
|
API HTTPS sécurisée avec authentification par clés utilisateur
|
||||||
Port 6666, domaine vault.4nkweb.com
|
Port 6666, domaine vault.4nkweb.com
|
||||||
GET /<env>/<file> pour servir les fichiers de storage/<env>/<file>
|
Système de clés par utilisateur avec rotation automatique
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import socket
|
import socket
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any
|
from datetime import datetime, timedelta
|
||||||
from flask import Flask, request, Response, jsonify
|
from typing import Dict, Any, Optional
|
||||||
|
from flask import Flask, request, Response, jsonify, abort
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import x25519
|
from cryptography.hazmat.primitives.asymmetric import x25519
|
||||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Configuration du logging
|
# Configuration du logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = Flask(__name__)
|
# Configuration
|
||||||
|
STORAGE_ROOT = Path('/home/debian/4NK_vault/storage')
|
||||||
|
ENV_FILE = STORAGE_ROOT / 'dev' / '.env'
|
||||||
|
|
||||||
class QuantumResistantEncryption:
|
class UserKeyManager:
|
||||||
"""
|
"""Gestionnaire de clés par utilisateur et par environnement avec rotation automatique"""
|
||||||
Chiffrement quantique résistant utilisant X25519 + ChaCha20-Poly1305
|
|
||||||
X25519 est considéré comme résistant aux attaques quantiques à court terme
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, environment: str):
|
||||||
self.private_key = x25519.X25519PrivateKey.generate()
|
self.environment = environment
|
||||||
self.public_key = self.private_key.public_key()
|
self.keys_dir = STORAGE_ROOT / environment / '_keys'
|
||||||
|
self.keys_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.keys_db = self._load_keys_db()
|
||||||
|
|
||||||
def encrypt(self, data: bytes, peer_public_key: x25519.X25519PublicKey) -> bytes:
|
def _load_keys_db(self) -> Dict[str, Any]:
|
||||||
"""Chiffre les données avec X25519 + ChaCha20-Poly1305"""
|
"""Charge la base de données des clés pour cet environnement"""
|
||||||
# Échange de clés X25519
|
keys_file = self.keys_dir / 'keys.json'
|
||||||
shared_key = self.private_key.exchange(peer_public_key)
|
if keys_file.exists():
|
||||||
|
try:
|
||||||
|
with open(keys_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors du chargement des clés pour {self.environment}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
# Dérivation de clé avec HKDF
|
def _save_keys_db(self):
|
||||||
derived_key = HKDF(
|
"""Sauvegarde la base de données des clés pour cet environnement"""
|
||||||
algorithm=hashes.SHA256(),
|
keys_file = self.keys_dir / 'keys.json'
|
||||||
length=32,
|
|
||||||
salt=b'quantum_resistant_salt',
|
|
||||||
info=b'vault_api_encryption'
|
|
||||||
).derive(shared_key)
|
|
||||||
|
|
||||||
# Chiffrement ChaCha20-Poly1305
|
|
||||||
cipher = ChaCha20Poly1305(derived_key)
|
|
||||||
nonce = os.urandom(12)
|
|
||||||
ciphertext = cipher.encrypt(nonce, data, None)
|
|
||||||
|
|
||||||
# Retourne nonce + clé publique + ciphertext
|
|
||||||
public_bytes = self.public_key.public_bytes(
|
|
||||||
encoding=serialization.Encoding.Raw,
|
|
||||||
format=serialization.PublicFormat.Raw
|
|
||||||
)
|
|
||||||
|
|
||||||
return base64.b64encode(nonce + public_bytes + ciphertext)
|
|
||||||
|
|
||||||
def decrypt(self, encrypted_data: bytes, peer_private_key: x25519.X25519PrivateKey) -> bytes:
|
|
||||||
"""Déchiffre les données"""
|
|
||||||
try:
|
try:
|
||||||
data = base64.b64decode(encrypted_data)
|
with open(keys_file, 'w') as f:
|
||||||
nonce = data[:12]
|
json.dump(self.keys_db, f, indent=2)
|
||||||
peer_public_bytes = data[12:44]
|
logger.info(f"Clés sauvegardées pour l'environnement {self.environment}")
|
||||||
ciphertext = data[44:]
|
|
||||||
|
|
||||||
peer_public_key = x25519.X25519PublicKey.from_public_bytes(peer_public_bytes)
|
|
||||||
|
|
||||||
# Échange de clés
|
|
||||||
shared_key = peer_private_key.exchange(peer_public_key)
|
|
||||||
|
|
||||||
# Dérivation de clé
|
|
||||||
derived_key = HKDF(
|
|
||||||
algorithm=hashes.SHA256(),
|
|
||||||
length=32,
|
|
||||||
salt=b'quantum_resistant_salt',
|
|
||||||
info=b'vault_api_encryption'
|
|
||||||
).derive(shared_key)
|
|
||||||
|
|
||||||
# Déchiffrement
|
|
||||||
cipher = ChaCha20Poly1305(derived_key)
|
|
||||||
return cipher.decrypt(nonce, ciphertext, None)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur de déchiffrement: {e}")
|
logger.error(f"Erreur lors de la sauvegarde des clés pour {self.environment}: {e}")
|
||||||
raise
|
|
||||||
|
|
||||||
class EnvironmentProcessor:
|
def _generate_user_key(self, user_id: str) -> bytes:
|
||||||
"""Traite les variables d'environnement composites"""
|
"""Génère une nouvelle clé pour un utilisateur"""
|
||||||
|
# Génération d'une clé aléatoire de 32 bytes
|
||||||
|
return secrets.token_bytes(32)
|
||||||
|
|
||||||
def __init__(self, env_file_path: str):
|
def _hash_user_id(self, user_id: str) -> str:
|
||||||
self.env_file_path = env_file_path
|
"""Hash l'ID utilisateur pour l'utiliser comme clé"""
|
||||||
self.variables = {}
|
return hashlib.sha256(user_id.encode()).hexdigest()
|
||||||
self._load_variables()
|
|
||||||
|
|
||||||
def _load_variables(self):
|
def get_or_create_user_key(self, user_id: str) -> bytes:
|
||||||
"""Charge les variables depuis le fichier .env"""
|
"""Récupère ou crée une clé pour un utilisateur"""
|
||||||
try:
|
user_hash = self._hash_user_id(user_id)
|
||||||
with open(self.env_file_path, 'r', encoding='utf-8') as f:
|
current_time = datetime.now()
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
if user_hash not in self.keys_db:
|
||||||
if line and not line.startswith('#') and '=' in line:
|
# Nouvel utilisateur
|
||||||
key, value = line.split('=', 1)
|
self.keys_db[user_hash] = {
|
||||||
self.variables[key.strip()] = value.strip()
|
'current_key': base64.b64encode(self._generate_user_key(user_id)).decode(),
|
||||||
except Exception as e:
|
'previous_key': None,
|
||||||
logger.error(f"Erreur lors du chargement du fichier .env: {e}")
|
'created_at': current_time.isoformat(),
|
||||||
|
'last_rotation': current_time.isoformat(),
|
||||||
|
'rotation_count': 0,
|
||||||
|
'environment': self.environment
|
||||||
|
}
|
||||||
|
self._save_keys_db()
|
||||||
|
logger.info(f"Nouvelle clé créée pour l'utilisateur {user_id} dans l'environnement {self.environment}")
|
||||||
|
|
||||||
|
user_data = self.keys_db[user_hash]
|
||||||
|
last_rotation = datetime.fromisoformat(user_data['last_rotation'])
|
||||||
|
|
||||||
|
# Rotation automatique si plus de 1 heure
|
||||||
|
if current_time - last_rotation > timedelta(hours=1):
|
||||||
|
self._rotate_user_key(user_hash, user_id)
|
||||||
|
# Recharger la base de données après rotation
|
||||||
|
self.keys_db = self._load_keys_db()
|
||||||
|
user_data = self.keys_db[user_hash]
|
||||||
|
|
||||||
|
return base64.b64decode(user_data['current_key'])
|
||||||
|
|
||||||
|
def _rotate_user_key(self, user_hash: str, user_id: str):
|
||||||
|
"""Effectue la rotation de clé pour un utilisateur"""
|
||||||
|
user_data = self.keys_db[user_hash]
|
||||||
|
|
||||||
|
# Sauvegarde de l'ancienne clé
|
||||||
|
user_data['previous_key'] = user_data['current_key']
|
||||||
|
|
||||||
|
# Génération d'une nouvelle clé
|
||||||
|
user_data['current_key'] = base64.b64encode(self._generate_user_key(user_id)).decode()
|
||||||
|
user_data['last_rotation'] = datetime.now().isoformat()
|
||||||
|
user_data['rotation_count'] += 1
|
||||||
|
user_data['environment'] = self.environment
|
||||||
|
|
||||||
|
self._save_keys_db()
|
||||||
|
logger.info(f"Clé rotée pour l'utilisateur {user_id} dans l'environnement {self.environment} (rotation #{user_data['rotation_count']})")
|
||||||
|
|
||||||
|
def get_user_keys(self, user_id: str) -> tuple[bytes, Optional[bytes]]:
|
||||||
|
"""Récupère la clé actuelle et précédente d'un utilisateur"""
|
||||||
|
user_hash = self._hash_user_id(user_id)
|
||||||
|
|
||||||
|
if user_hash not in self.keys_db:
|
||||||
|
raise ValueError(f"Utilisateur {user_id} non trouvé dans l'environnement {self.environment}")
|
||||||
|
|
||||||
|
user_data = self.keys_db[user_hash]
|
||||||
|
current_key = base64.b64decode(user_data['current_key'])
|
||||||
|
previous_key = None
|
||||||
|
|
||||||
|
if user_data['previous_key']:
|
||||||
|
previous_key = base64.b64decode(user_data['previous_key'])
|
||||||
|
|
||||||
|
return current_key, previous_key
|
||||||
|
|
||||||
|
class EnvProcessor:
|
||||||
|
"""Processeur de variables d'environnement avec résolution récursive"""
|
||||||
|
|
||||||
|
def __init__(self, env_file: Path):
|
||||||
|
self.variables = self._load_env_file(env_file)
|
||||||
|
|
||||||
|
def _load_env_file(self, env_file: Path) -> Dict[str, str]:
|
||||||
|
"""Charge le fichier .env"""
|
||||||
|
variables = {}
|
||||||
|
if env_file.exists():
|
||||||
|
try:
|
||||||
|
with open(env_file, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#') and '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
variables[key.strip()] = value.strip()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors du chargement du fichier .env: {e}")
|
||||||
|
return variables
|
||||||
|
|
||||||
def _resolve_variable(self, var_name: str, visited: set = None) -> str:
|
def _resolve_variable(self, var_name: str, visited: set = None) -> str:
|
||||||
"""Résout récursivement une variable et ses dépendances"""
|
"""Résout une variable récursivement"""
|
||||||
if visited is None:
|
if visited is None:
|
||||||
visited = set()
|
visited = set()
|
||||||
|
|
||||||
@ -120,13 +168,13 @@ class EnvironmentProcessor:
|
|||||||
return f"${{{var_name}}}"
|
return f"${{{var_name}}}"
|
||||||
|
|
||||||
if var_name not in self.variables:
|
if var_name not in self.variables:
|
||||||
logger.warning(f"Variable {var_name} non trouvée")
|
logger.warning(f"Variable non trouvée: {var_name}")
|
||||||
return f"${{{var_name}}}"
|
return f"${{{var_name}}}"
|
||||||
|
|
||||||
visited.add(var_name)
|
visited.add(var_name)
|
||||||
value = self.variables[var_name]
|
value = self.variables[var_name]
|
||||||
|
|
||||||
# Recherche des variables à substituer
|
# Traitement des sous-variables
|
||||||
pattern = r'\$\{([^}]+)\}'
|
pattern = r'\$\{([^}]+)\}'
|
||||||
matches = re.findall(pattern, value)
|
matches = re.findall(pattern, value)
|
||||||
|
|
||||||
@ -137,212 +185,303 @@ class EnvironmentProcessor:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def process_content(self, content: str) -> str:
|
def process_content(self, content: str) -> str:
|
||||||
"""Traite le contenu en remplaçant les variables"""
|
"""Traite le contenu en résolvant les variables"""
|
||||||
# Recherche toutes les variables du format ${VAR_NAME}
|
|
||||||
pattern = r'\$\{([^}]+)\}'
|
pattern = r'\$\{([^}]+)\}'
|
||||||
matches = re.findall(pattern, content)
|
matches = re.findall(pattern, content)
|
||||||
|
|
||||||
processed_content = content
|
|
||||||
for var_name in matches:
|
for var_name in matches:
|
||||||
resolved_value = self._resolve_variable(var_name)
|
resolved_value = self._resolve_variable(var_name)
|
||||||
processed_content = processed_content.replace(f"${{{var_name}}}", resolved_value)
|
content = content.replace(f"${{{var_name}}}", resolved_value)
|
||||||
|
|
||||||
return processed_content
|
return content
|
||||||
|
|
||||||
# Initialisation
|
class SecureVaultAPI:
|
||||||
ENCRYPTION = QuantumResistantEncryption()
|
"""API Vault sécurisée avec authentification par clés utilisateur"""
|
||||||
ENV_PROCESSOR = EnvironmentProcessor('/home/debian/4NK_vault/storage/dev/.env')
|
|
||||||
STORAGE_ROOT = Path('/home/debian/4NK_vault/storage')
|
|
||||||
|
|
||||||
@app.route('/<env>/<path:file_path>', methods=['GET'])
|
def __init__(self):
|
||||||
def serve_file(env: str, file_path: str):
|
self.app = Flask(__name__)
|
||||||
"""
|
self.env_processor = EnvProcessor(ENV_FILE)
|
||||||
Sert un fichier depuis storage/<env>/<file_path>
|
self._setup_routes()
|
||||||
Les variables sont remplacées par les valeurs du fichier .env
|
|
||||||
Le contenu est chiffré avec un algorithme quantique résistant
|
def _setup_routes(self):
|
||||||
"""
|
"""Configure les routes de l'API"""
|
||||||
try:
|
|
||||||
# Construction du chemin du fichier
|
@self.app.before_request
|
||||||
|
def force_https():
|
||||||
|
"""Force l'utilisation de HTTPS"""
|
||||||
|
# Vérification plus stricte de HTTPS
|
||||||
|
is_https = (
|
||||||
|
request.is_secure or
|
||||||
|
request.headers.get('X-Forwarded-Proto') == 'https' or
|
||||||
|
request.scheme == 'https'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_https:
|
||||||
|
logger.warning(f"Tentative d'accès HTTP rejetée depuis {request.remote_addr}")
|
||||||
|
abort(400, description="HTTPS requis - Cette API ne fonctionne qu'en HTTPS")
|
||||||
|
|
||||||
|
@self.app.route('/health', methods=['GET'])
|
||||||
|
def health():
|
||||||
|
"""Endpoint de santé avec authentification"""
|
||||||
|
# Authentification requise même pour /health
|
||||||
|
user_id = self._authenticate_user(request)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "vault-api-secure",
|
||||||
|
"encryption": "quantum-resistant",
|
||||||
|
"algorithm": "X25519-ChaCha20-Poly1305",
|
||||||
|
"authentication": "user-key-based",
|
||||||
|
"key_rotation": "automatic",
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
@self.app.route('/info', methods=['GET'])
|
||||||
|
def info():
|
||||||
|
"""Informations sur l'API avec authentification"""
|
||||||
|
# Authentification requise même pour /info
|
||||||
|
user_id = self._authenticate_user(request)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"name": "4NK Vault API Secure",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"domain": "vault.4nkweb.com",
|
||||||
|
"port": 6666,
|
||||||
|
"protocol": "HTTPS",
|
||||||
|
"encryption": "quantum-resistant",
|
||||||
|
"authentication": "user-key-based",
|
||||||
|
"key_rotation": "automatic",
|
||||||
|
"user_id": user_id,
|
||||||
|
"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)"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@self.app.route('/<env>/<path:file_path>', methods=['GET'])
|
||||||
|
def serve_file(env: str, file_path: str):
|
||||||
|
"""Sert un fichier avec authentification et chiffrement"""
|
||||||
|
try:
|
||||||
|
# Vérification de l'authentification
|
||||||
|
user_id = self._authenticate_user(request)
|
||||||
|
|
||||||
|
# Validation du chemin
|
||||||
|
if not self._validate_path(env, file_path):
|
||||||
|
logger.warning(f"Tentative d'accès non autorisé: {env}/{file_path}")
|
||||||
|
return jsonify({"error": "Accès non autorisé"}), 403
|
||||||
|
|
||||||
|
# Lecture du fichier
|
||||||
|
file_content = self._read_file(env, file_path)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Chiffrement avec la clé utilisateur pour cet environnement
|
||||||
|
encrypted_content = self._encrypt_with_user_key(processed_content, user_id, env)
|
||||||
|
|
||||||
|
# Retour de la réponse chiffrée
|
||||||
|
response = Response(
|
||||||
|
encrypted_content,
|
||||||
|
mimetype='application/octet-stream',
|
||||||
|
headers={
|
||||||
|
'X-Encryption-Type': 'quantum-resistant',
|
||||||
|
'X-Algorithm': 'X25519-ChaCha20-Poly1305',
|
||||||
|
'X-User-ID': user_id,
|
||||||
|
'X-Key-Rotation': 'enabled'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Fichier servi: {env}/{file_path} pour l'utilisateur {user_id}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors du service du fichier {env}/{file_path}: {e}")
|
||||||
|
return jsonify({"error": "Erreur interne du serveur"}), 500
|
||||||
|
|
||||||
|
def _authenticate_user(self, request) -> str:
|
||||||
|
"""Authentifie l'utilisateur et retourne son ID"""
|
||||||
|
# Récupération de l'ID utilisateur depuis les headers
|
||||||
|
user_id = request.headers.get('X-User-ID')
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
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:
|
||||||
|
abort(401, description="ID utilisateur invalide")
|
||||||
|
|
||||||
|
# Vérification des caractères autorisés
|
||||||
|
if not re.match(r'^[a-zA-Z0-9_-]+$', user_id):
|
||||||
|
abort(401, description="ID utilisateur contient des caractères non autorisés")
|
||||||
|
|
||||||
|
return user_id
|
||||||
|
|
||||||
|
def _validate_path(self, env: str, file_path: str) -> bool:
|
||||||
|
"""Valide le chemin d'accès"""
|
||||||
|
# Construction du chemin complet
|
||||||
full_path = STORAGE_ROOT / env / file_path
|
full_path = STORAGE_ROOT / env / file_path
|
||||||
|
|
||||||
# Vérification de sécurité - empêche l'accès en dehors du répertoire storage
|
# Vérification de sécurité
|
||||||
if not str(full_path.resolve()).startswith(str(STORAGE_ROOT.resolve())):
|
|
||||||
return jsonify({'error': 'Accès non autorisé'}), 403
|
|
||||||
|
|
||||||
# Vérification de l'existence du fichier
|
|
||||||
if not full_path.exists() or not full_path.is_file():
|
|
||||||
return jsonify({'error': 'Fichier non trouvé'}), 404
|
|
||||||
|
|
||||||
# Lecture du fichier
|
|
||||||
try:
|
try:
|
||||||
with open(full_path, 'r', encoding='utf-8') as f:
|
resolved_path = full_path.resolve()
|
||||||
content = f.read()
|
storage_resolved = STORAGE_ROOT.resolve()
|
||||||
except UnicodeDecodeError:
|
|
||||||
# Tentative en mode binaire pour les fichiers non-text
|
if not str(resolved_path).startswith(str(storage_resolved)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return resolved_path.exists() and resolved_path.is_file()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _read_file(self, env: str, file_path: str) -> Optional[str]:
|
||||||
|
"""Lit le contenu d'un fichier"""
|
||||||
|
try:
|
||||||
|
full_path = STORAGE_ROOT / env / file_path
|
||||||
|
|
||||||
|
# Lecture en mode binaire pour détecter le type
|
||||||
with open(full_path, 'rb') as f:
|
with open(full_path, 'rb') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
# Conversion en base64 pour les fichiers binaires
|
|
||||||
content = base64.b64encode(content).decode('utf-8')
|
|
||||||
content = f"BINARY_DATA:{content}"
|
|
||||||
|
|
||||||
# Traitement des variables d'environnement
|
# Tentative de décodage UTF-8
|
||||||
processed_content = ENV_PROCESSOR.process_content(content)
|
try:
|
||||||
|
return content.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Fichier binaire, encodage base64
|
||||||
|
return f"BINARY_DATA:{base64.b64encode(content).decode()}"
|
||||||
|
|
||||||
# Chiffrement du contenu avec clé dynamique sécurisée
|
|
||||||
# La clé de démonstration est désactivée pour des raisons de sécurité
|
|
||||||
try:
|
|
||||||
# Génération d'une clé de session unique (32 bytes)
|
|
||||||
session_key = os.urandom(32)
|
|
||||||
cipher = ChaCha20Poly1305(session_key)
|
|
||||||
nonce = os.urandom(12)
|
|
||||||
encrypted_content = cipher.encrypt(nonce, processed_content.encode('utf-8'), None)
|
|
||||||
|
|
||||||
# Encodage: nonce + clé de session + contenu chiffré
|
|
||||||
# ATTENTION: En production, la clé doit être transmise via un canal sécurisé
|
|
||||||
full_payload = nonce + session_key + encrypted_content
|
|
||||||
encrypted_content = base64.b64encode(full_payload)
|
|
||||||
|
|
||||||
logger.warning("ATTENTION: Clé de session générée - Transmission non sécurisée en mode démo")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur de chiffrement: {e}")
|
logger.error(f"Erreur lors de la lecture du fichier {env}/{file_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _encrypt_with_user_key(self, content: str, user_id: str, environment: str) -> bytes:
|
||||||
|
"""Chiffre le contenu avec la clé utilisateur pour un environnement spécifique"""
|
||||||
|
try:
|
||||||
|
# Création du gestionnaire de clés pour cet environnement
|
||||||
|
key_manager = UserKeyManager(environment)
|
||||||
|
|
||||||
|
# Création ou récupération de la clé utilisateur
|
||||||
|
current_key = key_manager.get_or_create_user_key(user_id)
|
||||||
|
|
||||||
|
# Chiffrement avec la clé actuelle
|
||||||
|
return self._encrypt_content(content, current_key, user_id, environment)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur de chiffrement pour l'utilisateur {user_id} dans l'environnement {environment}: {e}")
|
||||||
# Fallback: retour du contenu en base64 sans chiffrement
|
# Fallback: retour du contenu en base64 sans chiffrement
|
||||||
encrypted_content = base64.b64encode(processed_content.encode('utf-8'))
|
return base64.b64encode(content.encode('utf-8'))
|
||||||
|
|
||||||
# Retour de la réponse chiffrée
|
def _encrypt_content(self, content: str, key: bytes, user_id: str, environment: str, is_previous: bool = False) -> bytes:
|
||||||
response = Response(
|
"""Chiffre le contenu avec une clé spécifique"""
|
||||||
encrypted_content,
|
cipher = ChaCha20Poly1305(key)
|
||||||
mimetype='application/octet-stream',
|
nonce = secrets.token_bytes(12)
|
||||||
headers={
|
encrypted_content = cipher.encrypt(nonce, content.encode('utf-8'), None)
|
||||||
'Content-Disposition': f'attachment; filename="{file_path}"',
|
|
||||||
'X-Encryption-Type': 'quantum-resistant',
|
|
||||||
'X-Algorithm': 'X25519-ChaCha20-Poly1305'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Served file: {env}/{file_path}")
|
# Métadonnées de chiffrement
|
||||||
return response
|
metadata = {
|
||||||
|
'user_id': user_id,
|
||||||
except Exception as e:
|
'environment': environment,
|
||||||
logger.error(f"Erreur lors du service du fichier {env}/{file_path}: {e}")
|
'timestamp': datetime.now().isoformat(),
|
||||||
return jsonify({'error': 'Erreur interne du serveur'}), 500
|
'key_version': 'previous' if is_previous else 'current',
|
||||||
|
'algorithm': 'ChaCha20-Poly1305'
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health_check():
|
|
||||||
"""Point de contrôle de santé de l'API"""
|
|
||||||
return jsonify({
|
|
||||||
'status': 'healthy',
|
|
||||||
'service': 'vault-api',
|
|
||||||
'encryption': 'quantum-resistant',
|
|
||||||
'algorithm': 'X25519-ChaCha20-Poly1305'
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/info', methods=['GET'])
|
|
||||||
def api_info():
|
|
||||||
"""Informations sur l'API"""
|
|
||||||
return jsonify({
|
|
||||||
'name': '4NK Vault API',
|
|
||||||
'version': '1.0.0',
|
|
||||||
'domain': 'vault.4nkweb.com',
|
|
||||||
'port': 6666,
|
|
||||||
'protocol': 'HTTPS',
|
|
||||||
'encryption': 'quantum-resistant',
|
|
||||||
'endpoints': {
|
|
||||||
'GET /<env>/<file>': 'Sert un fichier chiffré depuis storage/<env>/<file>',
|
|
||||||
'GET /health': 'Contrôle de santé',
|
|
||||||
'GET /info': 'Informations sur l\'API'
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
def create_ssl_context():
|
# Encodage: nonce + métadonnées + contenu chiffré
|
||||||
"""Crée le contexte SSL pour HTTPS"""
|
metadata_json = json.dumps(metadata).encode('utf-8')
|
||||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
full_payload = nonce + len(metadata_json).to_bytes(4, 'big') + metadata_json + encrypted_content
|
||||||
|
|
||||||
# Configuration SSL sécurisée
|
return base64.b64encode(full_payload)
|
||||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
||||||
context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
|
|
||||||
|
|
||||||
# Génération de certificats auto-signés pour la démonstration
|
def create_ssl_context(self):
|
||||||
# En production, utiliser des certificats valides
|
"""Crée le contexte SSL pour HTTPS"""
|
||||||
try:
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.x509.oid import NameOID
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
# Génération d'une clé privée RSA
|
# Configuration SSL sécurisée
|
||||||
private_key = rsa.generate_private_key(
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
public_exponent=65537,
|
context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
|
||||||
key_size=2048,
|
|
||||||
|
# Génération de certificats auto-signés pour la démonstration
|
||||||
|
try:
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Génération d'une clé privée RSA
|
||||||
|
private_key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=2048,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Création d'un certificat auto-signé
|
||||||
|
subject = issuer = x509.Name([
|
||||||
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "FR"),
|
||||||
|
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "France"),
|
||||||
|
x509.NameAttribute(NameOID.LOCALITY_NAME, "Paris"),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "4NK"),
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, "vault.4nkweb.com"),
|
||||||
|
])
|
||||||
|
|
||||||
|
cert = x509.CertificateBuilder().subject_name(
|
||||||
|
subject
|
||||||
|
).issuer_name(
|
||||||
|
issuer
|
||||||
|
).public_key(
|
||||||
|
private_key.public_key()
|
||||||
|
).serial_number(
|
||||||
|
x509.random_serial_number()
|
||||||
|
).not_valid_before(
|
||||||
|
datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
).not_valid_after(
|
||||||
|
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365)
|
||||||
|
).add_extension(
|
||||||
|
x509.SubjectAlternativeName([
|
||||||
|
x509.DNSName("vault.4nkweb.com"),
|
||||||
|
x509.DNSName("localhost"),
|
||||||
|
]),
|
||||||
|
critical=False,
|
||||||
|
).sign(private_key, hashes.SHA256())
|
||||||
|
|
||||||
|
# Sauvegarde temporaire des certificats
|
||||||
|
with open('/tmp/vault.key', 'wb') as f:
|
||||||
|
f.write(private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.PKCS8,
|
||||||
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
|
))
|
||||||
|
|
||||||
|
with open('/tmp/vault.crt', 'wb') as f:
|
||||||
|
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||||
|
|
||||||
|
context.load_cert_chain('/tmp/vault.crt', '/tmp/vault.key')
|
||||||
|
logger.info("Certificats SSL générés avec succès")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Impossible de générer les certificats SSL: {e}")
|
||||||
|
logger.warning("Utilisation du contexte SSL par défaut")
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def run(self, host='0.0.0.0', port=6666, debug=False):
|
||||||
|
"""Démarre le serveur HTTPS"""
|
||||||
|
logger.info("Démarrage de l'API Vault sécurisée sur https://vault.4nkweb.com:6666")
|
||||||
|
logger.info("Authentification par clés utilisateur activée")
|
||||||
|
logger.info("Rotation automatique des clés activée")
|
||||||
|
logger.info("HTTPS obligatoire")
|
||||||
|
|
||||||
|
ssl_context = self.create_ssl_context()
|
||||||
|
|
||||||
|
self.app.run(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
debug=debug,
|
||||||
|
ssl_context=ssl_context,
|
||||||
|
threaded=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Création d'un certificat auto-signé
|
|
||||||
subject = issuer = x509.Name([
|
|
||||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "FR"),
|
|
||||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "France"),
|
|
||||||
x509.NameAttribute(NameOID.LOCALITY_NAME, "Paris"),
|
|
||||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "4NK"),
|
|
||||||
x509.NameAttribute(NameOID.COMMON_NAME, "vault.4nkweb.com"),
|
|
||||||
])
|
|
||||||
|
|
||||||
cert = x509.CertificateBuilder().subject_name(
|
|
||||||
subject
|
|
||||||
).issuer_name(
|
|
||||||
issuer
|
|
||||||
).public_key(
|
|
||||||
private_key.public_key()
|
|
||||||
).serial_number(
|
|
||||||
x509.random_serial_number()
|
|
||||||
).not_valid_before(
|
|
||||||
datetime.datetime.utcnow()
|
|
||||||
).not_valid_after(
|
|
||||||
datetime.datetime.utcnow() + datetime.timedelta(days=365)
|
|
||||||
).add_extension(
|
|
||||||
x509.SubjectAlternativeName([
|
|
||||||
x509.DNSName("vault.4nkweb.com"),
|
|
||||||
x509.DNSName("localhost"),
|
|
||||||
]),
|
|
||||||
critical=False,
|
|
||||||
).sign(private_key, hashes.SHA256())
|
|
||||||
|
|
||||||
# Sauvegarde temporaire des certificats
|
|
||||||
with open('/tmp/vault.key', 'wb') as f:
|
|
||||||
f.write(private_key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization.NoEncryption()
|
|
||||||
))
|
|
||||||
|
|
||||||
with open('/tmp/vault.crt', 'wb') as f:
|
|
||||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
||||||
|
|
||||||
context.load_cert_chain('/tmp/vault.crt', '/tmp/vault.key')
|
|
||||||
logger.info("Certificats SSL générés avec succès")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Impossible de générer les certificats SSL: {e}")
|
|
||||||
logger.warning("Utilisation du contexte SSL par défaut")
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Configuration du serveur
|
api = SecureVaultAPI()
|
||||||
host = '0.0.0.0'
|
api.run()
|
||||||
port = 6666
|
|
||||||
|
|
||||||
logger.info(f"Démarrage de l'API Vault sur https://vault.4nkweb.com:{port}")
|
|
||||||
logger.info("Chiffrement quantique résistant activé")
|
|
||||||
logger.info("Algorithme: X25519 + ChaCha20-Poly1305")
|
|
||||||
|
|
||||||
# Création du contexte SSL
|
|
||||||
ssl_context = create_ssl_context()
|
|
||||||
|
|
||||||
# Démarrage du serveur HTTPS
|
|
||||||
app.run(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
ssl_context=ssl_context,
|
|
||||||
debug=False,
|
|
||||||
threaded=True
|
|
||||||
)
|
|
||||||
|
@ -1,487 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
API HTTPS sécurisée avec authentification par clés utilisateur
|
|
||||||
Port 6666, domaine vault.4nkweb.com
|
|
||||||
Système de clés par utilisateur avec rotation automatique
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import ssl
|
|
||||||
import socket
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
import secrets
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
from flask import Flask, request, Response, jsonify, abort
|
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import x25519
|
|
||||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Configuration du logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
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"""
|
|
||||||
|
|
||||||
def __init__(self, environment: str):
|
|
||||||
self.environment = environment
|
|
||||||
self.keys_dir = STORAGE_ROOT / environment / '_keys'
|
|
||||||
self.keys_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
self.keys_db = self._load_keys_db()
|
|
||||||
|
|
||||||
def _load_keys_db(self) -> Dict[str, Any]:
|
|
||||||
"""Charge la base de données des clés pour cet environnement"""
|
|
||||||
keys_file = self.keys_dir / 'keys.json'
|
|
||||||
if keys_file.exists():
|
|
||||||
try:
|
|
||||||
with open(keys_file, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur lors du chargement des clés pour {self.environment}: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _save_keys_db(self):
|
|
||||||
"""Sauvegarde la base de données des clés pour cet environnement"""
|
|
||||||
keys_file = self.keys_dir / 'keys.json'
|
|
||||||
try:
|
|
||||||
with open(keys_file, 'w') as f:
|
|
||||||
json.dump(self.keys_db, f, indent=2)
|
|
||||||
logger.info(f"Clés sauvegardées pour l'environnement {self.environment}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur lors de la sauvegarde des clés pour {self.environment}: {e}")
|
|
||||||
|
|
||||||
def _generate_user_key(self, user_id: str) -> bytes:
|
|
||||||
"""Génère une nouvelle clé pour un utilisateur"""
|
|
||||||
# Génération d'une clé aléatoire de 32 bytes
|
|
||||||
return secrets.token_bytes(32)
|
|
||||||
|
|
||||||
def _hash_user_id(self, user_id: str) -> str:
|
|
||||||
"""Hash l'ID utilisateur pour l'utiliser comme clé"""
|
|
||||||
return hashlib.sha256(user_id.encode()).hexdigest()
|
|
||||||
|
|
||||||
def get_or_create_user_key(self, user_id: str) -> bytes:
|
|
||||||
"""Récupère ou crée une clé pour un utilisateur"""
|
|
||||||
user_hash = self._hash_user_id(user_id)
|
|
||||||
current_time = datetime.now()
|
|
||||||
|
|
||||||
if user_hash not in self.keys_db:
|
|
||||||
# Nouvel utilisateur
|
|
||||||
self.keys_db[user_hash] = {
|
|
||||||
'current_key': base64.b64encode(self._generate_user_key(user_id)).decode(),
|
|
||||||
'previous_key': None,
|
|
||||||
'created_at': current_time.isoformat(),
|
|
||||||
'last_rotation': current_time.isoformat(),
|
|
||||||
'rotation_count': 0,
|
|
||||||
'environment': self.environment
|
|
||||||
}
|
|
||||||
self._save_keys_db()
|
|
||||||
logger.info(f"Nouvelle clé créée pour l'utilisateur {user_id} dans l'environnement {self.environment}")
|
|
||||||
|
|
||||||
user_data = self.keys_db[user_hash]
|
|
||||||
last_rotation = datetime.fromisoformat(user_data['last_rotation'])
|
|
||||||
|
|
||||||
# Rotation automatique si plus de 1 heure
|
|
||||||
if current_time - last_rotation > timedelta(hours=1):
|
|
||||||
self._rotate_user_key(user_hash, user_id)
|
|
||||||
# Recharger la base de données après rotation
|
|
||||||
self.keys_db = self._load_keys_db()
|
|
||||||
user_data = self.keys_db[user_hash]
|
|
||||||
|
|
||||||
return base64.b64decode(user_data['current_key'])
|
|
||||||
|
|
||||||
def _rotate_user_key(self, user_hash: str, user_id: str):
|
|
||||||
"""Effectue la rotation de clé pour un utilisateur"""
|
|
||||||
user_data = self.keys_db[user_hash]
|
|
||||||
|
|
||||||
# Sauvegarde de l'ancienne clé
|
|
||||||
user_data['previous_key'] = user_data['current_key']
|
|
||||||
|
|
||||||
# Génération d'une nouvelle clé
|
|
||||||
user_data['current_key'] = base64.b64encode(self._generate_user_key(user_id)).decode()
|
|
||||||
user_data['last_rotation'] = datetime.now().isoformat()
|
|
||||||
user_data['rotation_count'] += 1
|
|
||||||
user_data['environment'] = self.environment
|
|
||||||
|
|
||||||
self._save_keys_db()
|
|
||||||
logger.info(f"Clé rotée pour l'utilisateur {user_id} dans l'environnement {self.environment} (rotation #{user_data['rotation_count']})")
|
|
||||||
|
|
||||||
def get_user_keys(self, user_id: str) -> tuple[bytes, Optional[bytes]]:
|
|
||||||
"""Récupère la clé actuelle et précédente d'un utilisateur"""
|
|
||||||
user_hash = self._hash_user_id(user_id)
|
|
||||||
|
|
||||||
if user_hash not in self.keys_db:
|
|
||||||
raise ValueError(f"Utilisateur {user_id} non trouvé dans l'environnement {self.environment}")
|
|
||||||
|
|
||||||
user_data = self.keys_db[user_hash]
|
|
||||||
current_key = base64.b64decode(user_data['current_key'])
|
|
||||||
previous_key = None
|
|
||||||
|
|
||||||
if user_data['previous_key']:
|
|
||||||
previous_key = base64.b64decode(user_data['previous_key'])
|
|
||||||
|
|
||||||
return current_key, previous_key
|
|
||||||
|
|
||||||
class EnvProcessor:
|
|
||||||
"""Processeur de variables d'environnement avec résolution récursive"""
|
|
||||||
|
|
||||||
def __init__(self, env_file: Path):
|
|
||||||
self.variables = self._load_env_file(env_file)
|
|
||||||
|
|
||||||
def _load_env_file(self, env_file: Path) -> Dict[str, str]:
|
|
||||||
"""Charge le fichier .env"""
|
|
||||||
variables = {}
|
|
||||||
if env_file.exists():
|
|
||||||
try:
|
|
||||||
with open(env_file, 'r', encoding='utf-8') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#') and '=' in line:
|
|
||||||
key, value = line.split('=', 1)
|
|
||||||
variables[key.strip()] = value.strip()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur lors du chargement du fichier .env: {e}")
|
|
||||||
return variables
|
|
||||||
|
|
||||||
def _resolve_variable(self, var_name: str, visited: set = None) -> str:
|
|
||||||
"""Résout une variable récursivement"""
|
|
||||||
if visited is None:
|
|
||||||
visited = set()
|
|
||||||
|
|
||||||
if var_name in visited:
|
|
||||||
logger.warning(f"Dépendance circulaire détectée pour {var_name}")
|
|
||||||
return f"${{{var_name}}}"
|
|
||||||
|
|
||||||
if var_name not in self.variables:
|
|
||||||
logger.warning(f"Variable non trouvée: {var_name}")
|
|
||||||
return f"${{{var_name}}}"
|
|
||||||
|
|
||||||
visited.add(var_name)
|
|
||||||
value = self.variables[var_name]
|
|
||||||
|
|
||||||
# Traitement des sous-variables
|
|
||||||
pattern = r'\$\{([^}]+)\}'
|
|
||||||
matches = re.findall(pattern, value)
|
|
||||||
|
|
||||||
for match in matches:
|
|
||||||
resolved_value = self._resolve_variable(match, visited.copy())
|
|
||||||
value = value.replace(f"${{{match}}}", resolved_value)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
def process_content(self, content: str) -> str:
|
|
||||||
"""Traite le contenu en résolvant les variables"""
|
|
||||||
pattern = r'\$\{([^}]+)\}'
|
|
||||||
matches = re.findall(pattern, content)
|
|
||||||
|
|
||||||
for var_name in matches:
|
|
||||||
resolved_value = self._resolve_variable(var_name)
|
|
||||||
content = content.replace(f"${{{var_name}}}", resolved_value)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
class SecureVaultAPI:
|
|
||||||
"""API Vault sécurisée avec authentification par clés utilisateur"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.app = Flask(__name__)
|
|
||||||
self.env_processor = EnvProcessor(ENV_FILE)
|
|
||||||
self._setup_routes()
|
|
||||||
|
|
||||||
def _setup_routes(self):
|
|
||||||
"""Configure les routes de l'API"""
|
|
||||||
|
|
||||||
@self.app.before_request
|
|
||||||
def force_https():
|
|
||||||
"""Force l'utilisation de HTTPS"""
|
|
||||||
# Vérification plus stricte de HTTPS
|
|
||||||
is_https = (
|
|
||||||
request.is_secure or
|
|
||||||
request.headers.get('X-Forwarded-Proto') == 'https' or
|
|
||||||
request.scheme == 'https'
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_https:
|
|
||||||
logger.warning(f"Tentative d'accès HTTP rejetée depuis {request.remote_addr}")
|
|
||||||
abort(400, description="HTTPS requis - Cette API ne fonctionne qu'en HTTPS")
|
|
||||||
|
|
||||||
@self.app.route('/health', methods=['GET'])
|
|
||||||
def health():
|
|
||||||
"""Endpoint de santé avec authentification"""
|
|
||||||
# Authentification requise même pour /health
|
|
||||||
user_id = self._authenticate_user(request)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "vault-api-secure",
|
|
||||||
"encryption": "quantum-resistant",
|
|
||||||
"algorithm": "X25519-ChaCha20-Poly1305",
|
|
||||||
"authentication": "user-key-based",
|
|
||||||
"key_rotation": "automatic",
|
|
||||||
"user_id": user_id,
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
@self.app.route('/info', methods=['GET'])
|
|
||||||
def info():
|
|
||||||
"""Informations sur l'API avec authentification"""
|
|
||||||
# Authentification requise même pour /info
|
|
||||||
user_id = self._authenticate_user(request)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"name": "4NK Vault API Secure",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"domain": "vault.4nkweb.com",
|
|
||||||
"port": 6666,
|
|
||||||
"protocol": "HTTPS",
|
|
||||||
"encryption": "quantum-resistant",
|
|
||||||
"authentication": "user-key-based",
|
|
||||||
"key_rotation": "automatic",
|
|
||||||
"user_id": user_id,
|
|
||||||
"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)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
@self.app.route('/<env>/<path:file_path>', methods=['GET'])
|
|
||||||
def serve_file(env: str, file_path: str):
|
|
||||||
"""Sert un fichier avec authentification et chiffrement"""
|
|
||||||
try:
|
|
||||||
# Vérification de l'authentification
|
|
||||||
user_id = self._authenticate_user(request)
|
|
||||||
|
|
||||||
# Validation du chemin
|
|
||||||
if not self._validate_path(env, file_path):
|
|
||||||
logger.warning(f"Tentative d'accès non autorisé: {env}/{file_path}")
|
|
||||||
return jsonify({"error": "Accès non autorisé"}), 403
|
|
||||||
|
|
||||||
# Lecture du fichier
|
|
||||||
file_content = self._read_file(env, file_path)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Chiffrement avec la clé utilisateur pour cet environnement
|
|
||||||
encrypted_content = self._encrypt_with_user_key(processed_content, user_id, env)
|
|
||||||
|
|
||||||
# Retour de la réponse chiffrée
|
|
||||||
response = Response(
|
|
||||||
encrypted_content,
|
|
||||||
mimetype='application/octet-stream',
|
|
||||||
headers={
|
|
||||||
'X-Encryption-Type': 'quantum-resistant',
|
|
||||||
'X-Algorithm': 'X25519-ChaCha20-Poly1305',
|
|
||||||
'X-User-ID': user_id,
|
|
||||||
'X-Key-Rotation': 'enabled'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Fichier servi: {env}/{file_path} pour l'utilisateur {user_id}")
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur lors du service du fichier {env}/{file_path}: {e}")
|
|
||||||
return jsonify({"error": "Erreur interne du serveur"}), 500
|
|
||||||
|
|
||||||
def _authenticate_user(self, request) -> str:
|
|
||||||
"""Authentifie l'utilisateur et retourne son ID"""
|
|
||||||
# Récupération de l'ID utilisateur depuis les headers
|
|
||||||
user_id = request.headers.get('X-User-ID')
|
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
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:
|
|
||||||
abort(401, description="ID utilisateur invalide")
|
|
||||||
|
|
||||||
# Vérification des caractères autorisés
|
|
||||||
if not re.match(r'^[a-zA-Z0-9_-]+$', user_id):
|
|
||||||
abort(401, description="ID utilisateur contient des caractères non autorisés")
|
|
||||||
|
|
||||||
return user_id
|
|
||||||
|
|
||||||
def _validate_path(self, env: str, file_path: str) -> bool:
|
|
||||||
"""Valide le chemin d'accès"""
|
|
||||||
# Construction du chemin complet
|
|
||||||
full_path = STORAGE_ROOT / env / file_path
|
|
||||||
|
|
||||||
# Vérification de sécurité
|
|
||||||
try:
|
|
||||||
resolved_path = full_path.resolve()
|
|
||||||
storage_resolved = STORAGE_ROOT.resolve()
|
|
||||||
|
|
||||||
if not str(resolved_path).startswith(str(storage_resolved)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return resolved_path.exists() and resolved_path.is_file()
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _read_file(self, env: str, file_path: str) -> Optional[str]:
|
|
||||||
"""Lit le contenu d'un fichier"""
|
|
||||||
try:
|
|
||||||
full_path = STORAGE_ROOT / env / file_path
|
|
||||||
|
|
||||||
# Lecture en mode binaire pour détecter le type
|
|
||||||
with open(full_path, 'rb') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Tentative de décodage UTF-8
|
|
||||||
try:
|
|
||||||
return content.decode('utf-8')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
# Fichier binaire, encodage base64
|
|
||||||
return f"BINARY_DATA:{base64.b64encode(content).decode()}"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur lors de la lecture du fichier {env}/{file_path}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _encrypt_with_user_key(self, content: str, user_id: str, environment: str) -> bytes:
|
|
||||||
"""Chiffre le contenu avec la clé utilisateur pour un environnement spécifique"""
|
|
||||||
try:
|
|
||||||
# Création du gestionnaire de clés pour cet environnement
|
|
||||||
key_manager = UserKeyManager(environment)
|
|
||||||
|
|
||||||
# Création ou récupération de la clé utilisateur
|
|
||||||
current_key = key_manager.get_or_create_user_key(user_id)
|
|
||||||
|
|
||||||
# Chiffrement avec la clé actuelle
|
|
||||||
return self._encrypt_content(content, current_key, user_id, environment)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur de chiffrement pour l'utilisateur {user_id} dans l'environnement {environment}: {e}")
|
|
||||||
# Fallback: retour du contenu en base64 sans chiffrement
|
|
||||||
return base64.b64encode(content.encode('utf-8'))
|
|
||||||
|
|
||||||
def _encrypt_content(self, content: str, key: bytes, user_id: str, environment: str, is_previous: bool = False) -> bytes:
|
|
||||||
"""Chiffre le contenu avec une clé spécifique"""
|
|
||||||
cipher = ChaCha20Poly1305(key)
|
|
||||||
nonce = secrets.token_bytes(12)
|
|
||||||
encrypted_content = cipher.encrypt(nonce, content.encode('utf-8'), None)
|
|
||||||
|
|
||||||
# Métadonnées de chiffrement
|
|
||||||
metadata = {
|
|
||||||
'user_id': user_id,
|
|
||||||
'environment': environment,
|
|
||||||
'timestamp': datetime.now().isoformat(),
|
|
||||||
'key_version': 'previous' if is_previous else 'current',
|
|
||||||
'algorithm': 'ChaCha20-Poly1305'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Encodage: nonce + métadonnées + contenu chiffré
|
|
||||||
metadata_json = json.dumps(metadata).encode('utf-8')
|
|
||||||
full_payload = nonce + len(metadata_json).to_bytes(4, 'big') + metadata_json + encrypted_content
|
|
||||||
|
|
||||||
return base64.b64encode(full_payload)
|
|
||||||
|
|
||||||
def create_ssl_context(self):
|
|
||||||
"""Crée le contexte SSL pour HTTPS"""
|
|
||||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
||||||
|
|
||||||
# Configuration SSL sécurisée
|
|
||||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
||||||
context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
|
|
||||||
|
|
||||||
# Génération de certificats auto-signés pour la démonstration
|
|
||||||
try:
|
|
||||||
from cryptography import x509
|
|
||||||
from cryptography.x509.oid import NameOID
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
# Génération d'une clé privée RSA
|
|
||||||
private_key = rsa.generate_private_key(
|
|
||||||
public_exponent=65537,
|
|
||||||
key_size=2048,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Création d'un certificat auto-signé
|
|
||||||
subject = issuer = x509.Name([
|
|
||||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "FR"),
|
|
||||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "France"),
|
|
||||||
x509.NameAttribute(NameOID.LOCALITY_NAME, "Paris"),
|
|
||||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "4NK"),
|
|
||||||
x509.NameAttribute(NameOID.COMMON_NAME, "vault.4nkweb.com"),
|
|
||||||
])
|
|
||||||
|
|
||||||
cert = x509.CertificateBuilder().subject_name(
|
|
||||||
subject
|
|
||||||
).issuer_name(
|
|
||||||
issuer
|
|
||||||
).public_key(
|
|
||||||
private_key.public_key()
|
|
||||||
).serial_number(
|
|
||||||
x509.random_serial_number()
|
|
||||||
).not_valid_before(
|
|
||||||
datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
).not_valid_after(
|
|
||||||
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365)
|
|
||||||
).add_extension(
|
|
||||||
x509.SubjectAlternativeName([
|
|
||||||
x509.DNSName("vault.4nkweb.com"),
|
|
||||||
x509.DNSName("localhost"),
|
|
||||||
]),
|
|
||||||
critical=False,
|
|
||||||
).sign(private_key, hashes.SHA256())
|
|
||||||
|
|
||||||
# Sauvegarde temporaire des certificats
|
|
||||||
with open('/tmp/vault.key', 'wb') as f:
|
|
||||||
f.write(private_key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization.NoEncryption()
|
|
||||||
))
|
|
||||||
|
|
||||||
with open('/tmp/vault.crt', 'wb') as f:
|
|
||||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
||||||
|
|
||||||
context.load_cert_chain('/tmp/vault.crt', '/tmp/vault.key')
|
|
||||||
logger.info("Certificats SSL générés avec succès")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Impossible de générer les certificats SSL: {e}")
|
|
||||||
logger.warning("Utilisation du contexte SSL par défaut")
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def run(self, host='0.0.0.0', port=6666, debug=False):
|
|
||||||
"""Démarre le serveur HTTPS"""
|
|
||||||
logger.info("Démarrage de l'API Vault sécurisée sur https://vault.4nkweb.com:6666")
|
|
||||||
logger.info("Authentification par clés utilisateur activée")
|
|
||||||
logger.info("Rotation automatique des clés activée")
|
|
||||||
logger.info("HTTPS obligatoire")
|
|
||||||
|
|
||||||
ssl_context = self.create_ssl_context()
|
|
||||||
|
|
||||||
self.app.run(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
debug=debug,
|
|
||||||
ssl_context=ssl_context,
|
|
||||||
threaded=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
api = SecureVaultAPI()
|
|
||||||
api.run()
|
|
174
docs/index.md
174
docs/index.md
@ -1,26 +1,39 @@
|
|||||||
# Documentation 4NK Vault
|
# Documentation 4NK Vault - Système Sécurisé
|
||||||
|
|
||||||
Bienvenue dans la documentation complète du projet 4NK Vault, un système de stockage sécurisé avec chiffrement quantique résistant.
|
Bienvenue dans la documentation complète du projet 4NK Vault, un système de stockage sécurisé avec authentification par clés utilisateur et chiffrement quantique résistant.
|
||||||
|
|
||||||
## 📚 Documentation disponible
|
## 📚 Documentation disponible
|
||||||
|
|
||||||
### 🚀 [Guide principal](README.md)
|
### 🚀 [Guide principal](../README.md)
|
||||||
Vue d'ensemble du projet, architecture et utilisation de base.
|
Vue d'ensemble du projet, architecture sécurisée et utilisation avec authentification.
|
||||||
|
|
||||||
### 🔧 [Spécification API](api-specification.md)
|
### 🔧 [Spécification API](api-specification.md)
|
||||||
Spécification technique complète de l'API REST avec tous les détails d'implémentation.
|
Spécification technique complète de l'API REST sécurisée avec authentification par clés utilisateur.
|
||||||
|
|
||||||
### 📖 [Référence API](api-reference.md)
|
### 📖 [Référence API](api-reference.md)
|
||||||
Référence complète des endpoints, paramètres, réponses et codes d'erreur.
|
Référence complète des endpoints sécurisés, paramètres, réponses et codes d'erreur.
|
||||||
|
|
||||||
### 🛡️ [Modèle de sécurité](security-model.md)
|
### 🛡️ [Modèle de sécurité](security-model.md)
|
||||||
Architecture de sécurité, chiffrement quantique résistant et bonnes pratiques.
|
Architecture de sécurité, authentification par clés utilisateur, rotation automatique et bonnes pratiques.
|
||||||
|
|
||||||
### 🚀 [Guide de déploiement](deployment-guide.md)
|
### 🚀 [Guide de déploiement](deployment-guide.md)
|
||||||
Instructions complètes pour déployer le système en développement et production.
|
Instructions complètes pour déployer le système sécurisé en développement et production.
|
||||||
|
|
||||||
### 💻 [Documentation SDK](sdk-documentation.md)
|
### 💻 [Documentation SDK](sdk-documentation.md)
|
||||||
Documentation complète du SDK TypeScript avec exemples et API reference.
|
Documentation complète du SDK TypeScript sécurisé avec exemples et API reference.
|
||||||
|
|
||||||
|
## 🔐 Sécurité Avancée
|
||||||
|
|
||||||
|
### Authentification par clés utilisateur
|
||||||
|
- **ID utilisateur obligatoire** pour tous les accès
|
||||||
|
- **Clés individuelles** par utilisateur et environnement
|
||||||
|
- **Rotation automatique** des clés toutes les heures
|
||||||
|
- **Stockage sécurisé** dans `storage/<env>/_keys/`
|
||||||
|
|
||||||
|
### Chiffrement quantique résistant
|
||||||
|
- **ChaCha20-Poly1305** pour le chiffrement
|
||||||
|
- **HTTPS obligatoire** pour toutes les communications
|
||||||
|
- **Métadonnées sécurisées** avec timestamps et versions de clés
|
||||||
|
|
||||||
## 🎯 Démarrage rapide
|
## 🎯 Démarrage rapide
|
||||||
|
|
||||||
@ -28,112 +41,71 @@ Documentation complète du SDK TypeScript avec exemples et API reference.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Installation des dépendances
|
# 1. Installation des dépendances
|
||||||
|
cd 4NK_vault
|
||||||
|
python3 -m venv venv_api
|
||||||
|
source venv_api/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 2. Démarrage de l'API
|
# 2. Démarrage de l'API sécurisée
|
||||||
./start_api.sh
|
./start_api.sh
|
||||||
|
|
||||||
# 3. Test de l'API
|
# 3. Test avec authentification
|
||||||
curl -k https://localhost:6666/health
|
curl -k -H "X-User-ID: demo_user_001" https://127.0.0.1:6666/health
|
||||||
|
|
||||||
# 4. Test du SDK
|
|
||||||
cd sdk-client
|
|
||||||
npm install && npm run build
|
|
||||||
node dist/examples/basic-usage.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Utilisation basique
|
### Utilisation avec le SDK TypeScript
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createVaultClient } from '@4nk/vault-sdk';
|
import { createSecureVaultClient } from '@4nk/vault-sdk';
|
||||||
|
|
||||||
const client = createVaultClient('https://vault.4nkweb.com:6666');
|
// Création du client avec authentification
|
||||||
|
const client = createSecureVaultClient(
|
||||||
|
'https://vault.4nkweb.com:6666',
|
||||||
|
'your_user_id' // ID utilisateur obligatoire
|
||||||
|
);
|
||||||
|
|
||||||
|
// Récupération d'un fichier chiffré
|
||||||
const file = await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
const file = await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
||||||
console.log(file.content); // Contenu déchiffré
|
console.log(file.content); // Contenu déchiffré avec la clé utilisateur
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔍 Navigation par cas d'usage
|
## 🔑 Gestion des clés
|
||||||
|
|
||||||
|
### Structure des clés par environnement
|
||||||
|
```
|
||||||
|
storage/
|
||||||
|
├── dev/
|
||||||
|
│ └── _keys/
|
||||||
|
│ └── keys.json # Clés pour l'environnement DEV
|
||||||
|
└── prod/
|
||||||
|
└── _keys/
|
||||||
|
└── keys.json # Clés pour l'environnement PROD
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotation automatique
|
||||||
|
- **Déclenchement** : Toutes les heures automatiquement
|
||||||
|
- **Transition** : Ancienne clé conservée pour compatibilité
|
||||||
|
- **Logs** : Traçabilité complète des rotations
|
||||||
|
|
||||||
|
## ⚠️ Avertissements de sécurité
|
||||||
|
|
||||||
|
Voir [SECURITY_NOTICE.md](../SECURITY_NOTICE.md) pour les détails sur :
|
||||||
|
- Les limitations de sécurité en mode démonstration
|
||||||
|
- Les recommandations pour la production
|
||||||
|
- L'implémentation d'un échange de clés sécurisé
|
||||||
|
|
||||||
|
## 📋 Navigation par cas d'usage
|
||||||
|
|
||||||
### Je veux...
|
### Je veux...
|
||||||
- **Comprendre le projet** → [Guide principal](README.md)
|
- **[Comprendre la sécurité](security-model.md)** → Modèle de sécurité complet
|
||||||
- **Utiliser l'API** → [Référence API](api-reference.md)
|
- **[Utiliser l'API](api-reference.md)** → Référence des endpoints sécurisés
|
||||||
- **Déployer le système** → [Guide de déploiement](deployment-guide.md)
|
- **[Développer avec le SDK](sdk-documentation.md)** → SDK TypeScript sécurisé
|
||||||
- **Développer avec le SDK** → [Documentation SDK](sdk-documentation.md)
|
- **[Déployer en production](deployment-guide.md)** → Guide de déploiement sécurisé
|
||||||
- **Comprendre la sécurité** → [Modèle de sécurité](security-model.md)
|
- **[Tester le système](api-specification.md)** → Spécifications techniques
|
||||||
- **Implémenter l'API** → [Spécification API](api-specification.md)
|
|
||||||
|
|
||||||
## 🏗️ Architecture du système
|
## 🔗 Liens utiles
|
||||||
|
|
||||||
```
|
- **Repository** : [git.4nkweb.com:4nk/4NK_vault.git](https://git.4nkweb.com:4nk/4NK_vault.git)
|
||||||
┌─────────────────┐ HTTPS ┌─────────────────┐ ┌─────────────────┐
|
- **Domaine** : vault.4nkweb.com:6666
|
||||||
│ Client SDK │ ──────────► │ API Vault │ ──► │ Storage │
|
- **Protocole** : HTTPS uniquement
|
||||||
│ TypeScript │ Port 6666 │ Python Flask │ │ Files │
|
- **Authentification** : Header `X-User-ID` obligatoire
|
||||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Variables │
|
|
||||||
│ .env │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 Sécurité
|
|
||||||
|
|
||||||
- **Chiffrement** : ChaCha20-Poly1305 (quantique résistant)
|
|
||||||
- **Transport** : HTTPS uniquement (TLS 1.2+)
|
|
||||||
- **Authentification** : Chiffrement intégré avec nonce aléatoire
|
|
||||||
- **Validation** : Protection contre les accès non autorisés
|
|
||||||
|
|
||||||
## 📊 Fonctionnalités
|
|
||||||
|
|
||||||
- ✅ **API REST** : Endpoints pour la santé, info et fichiers
|
|
||||||
- ✅ **Chiffrement** : Quantique résistant avec ChaCha20-Poly1305
|
|
||||||
- ✅ **Variables** : Traitement automatique des variables composites
|
|
||||||
- ✅ **SDK TypeScript** : Client type-safe avec déchiffrement
|
|
||||||
- ✅ **Monitoring** : Logs détaillés et métriques
|
|
||||||
- ✅ **Sécurité** : Validation des chemins et protection d'accès
|
|
||||||
|
|
||||||
## 🛠️ Technologies
|
|
||||||
|
|
||||||
- **Backend** : Python 3.8+ avec Flask
|
|
||||||
- **Frontend** : TypeScript/JavaScript SDK
|
|
||||||
- **Chiffrement** : cryptography (Python), crypto (Node.js)
|
|
||||||
- **Transport** : HTTPS avec certificats SSL/TLS
|
|
||||||
- **Stockage** : Système de fichiers avec structure hiérarchique
|
|
||||||
|
|
||||||
## 📈 Versions
|
|
||||||
|
|
||||||
| Composant | Version | Statut |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| API Server | 1.0.0 | ✅ Stable |
|
|
||||||
| SDK Client | 1.0.0 | ✅ Stable |
|
|
||||||
| Documentation | 1.0.0 | ✅ Complète |
|
|
||||||
|
|
||||||
## 🤝 Support
|
|
||||||
|
|
||||||
### Ressources
|
|
||||||
|
|
||||||
- **Issues** : [Git Issues](https://git.4nkweb.com/4nk/vault/issues)
|
|
||||||
- **Wiki** : [Documentation Wiki](https://git.4nkweb.com/4nk/vault/wiki)
|
|
||||||
- **Email** : support@4nkweb.com
|
|
||||||
|
|
||||||
### Communauté
|
|
||||||
|
|
||||||
- **Discussions** : [Forum de la communauté](https://forum.4nkweb.com)
|
|
||||||
- **Chat** : [Discord](https://discord.gg/4nk-vault)
|
|
||||||
- **Newsletter** : [Abonnement](https://newsletter.4nkweb.com)
|
|
||||||
|
|
||||||
## 📄 Licence
|
|
||||||
|
|
||||||
MIT License - Voir le fichier `LICENSE` pour plus de détails.
|
|
||||||
|
|
||||||
## 🏷️ Tags et mots-clés
|
|
||||||
|
|
||||||
`vault` `security` `encryption` `quantum-resistant` `api` `typescript` `python` `chacha20` `poly1305` `https` `configuration` `variables` `sdk`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Dernière mise à jour** : 2025-09-29
|
|
||||||
**Version documentation** : 1.0.0
|
|
||||||
**Maintenu par** : Équipe 4NK
|
|
@ -1,193 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Démonstration du SDK Vault 4NK en action
|
|
||||||
* Utilise l'API réelle pour récupérer et déchiffrer des fichiers
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VaultClient, VaultCrypto } from './src/index';
|
|
||||||
|
|
||||||
async function demo() {
|
|
||||||
console.log('🚀 Démonstration du SDK Vault 4NK');
|
|
||||||
console.log('=' .repeat(50));
|
|
||||||
console.log('📡 Connexion à l\'API: https://vault.4nkweb.com:6666');
|
|
||||||
console.log('🔐 Chiffrement: ChaCha20-Poly1305 (quantique résistant)');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Création du client avec la clé de déchiffrement
|
|
||||||
console.log('🔑 Initialisation du client...');
|
|
||||||
const client = new VaultClient(
|
|
||||||
{
|
|
||||||
baseUrl: 'https://vault.4nkweb.com:6666',
|
|
||||||
verifySsl: false, // Certificats auto-signés
|
|
||||||
timeout: 15000, // 15 secondes de timeout
|
|
||||||
}
|
|
||||||
// Plus de clé de déchiffrement nécessaire - clés dynamiques utilisées
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Test de connectivité
|
|
||||||
console.log('🌐 Test de connectivité...');
|
|
||||||
const isConnected = await client.ping();
|
|
||||||
if (!isConnected) {
|
|
||||||
throw new Error('❌ Impossible de se connecter à l\'API Vault');
|
|
||||||
}
|
|
||||||
console.log('✅ Connecté à l\'API Vault');
|
|
||||||
|
|
||||||
// 3. Informations sur l'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(` Domaine: ${info.domain}:${info.port}`);
|
|
||||||
console.log(` Protocole: ${info.protocol}`);
|
|
||||||
console.log(` Chiffrement: ${info.encryption}`);
|
|
||||||
|
|
||||||
// 4. État de santé
|
|
||||||
console.log('\n🏥 État de santé...');
|
|
||||||
const health = await client.health();
|
|
||||||
console.log(` Statut: ${health.status}`);
|
|
||||||
console.log(` Service: ${health.service}`);
|
|
||||||
console.log(` Algorithme: ${health.algorithm}`);
|
|
||||||
|
|
||||||
// 5. Récupération et déchiffrement de fichiers
|
|
||||||
console.log('\n📁 Récupération de fichiers chiffrés...');
|
|
||||||
|
|
||||||
const filesToGet = [
|
|
||||||
{ env: 'dev', path: 'bitcoin/bitcoin.conf', description: 'Configuration Bitcoin' },
|
|
||||||
{ env: 'dev', path: 'tor/torrc', description: 'Configuration Tor' },
|
|
||||||
{ env: 'dev', path: 'sdk_relay/sdk_relay.conf', description: 'Configuration SDK Relay' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const fileInfo of filesToGet) {
|
|
||||||
console.log(`\n 📄 ${fileInfo.description} (${fileInfo.path})`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const startTime = Date.now();
|
|
||||||
const file = await client.getFile(fileInfo.env, fileInfo.path);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
console.log(` ✅ Récupéré en ${endTime - startTime}ms`);
|
|
||||||
console.log(` 📦 Taille: ${file.size} caractères`);
|
|
||||||
console.log(` 🔐 Chiffré: ${file.encrypted ? 'Oui' : 'Non'}`);
|
|
||||||
console.log(` 🔧 Algorithme: ${file.algorithm || 'N/A'}`);
|
|
||||||
|
|
||||||
// Analyse du contenu
|
|
||||||
const lines = file.content.split('\n');
|
|
||||||
const configLines = lines.filter(line =>
|
|
||||||
line.trim() && !line.trim().startsWith('#')
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(` 📝 Lignes totales: ${lines.length}`);
|
|
||||||
console.log(` ⚙️ Lignes de config: ${configLines.length}`);
|
|
||||||
|
|
||||||
// Recherche de variables d'environnement
|
|
||||||
const envVars = file.content.match(/\$\{[^}]+\}/g) || [];
|
|
||||||
if (envVars.length > 0) {
|
|
||||||
const uniqueVars = [...new Set(envVars)];
|
|
||||||
console.log(` 🔧 Variables env: ${uniqueVars.length} (${uniqueVars.slice(0, 3).join(', ')}...)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Affichage d'un échantillon du contenu déchiffré
|
|
||||||
const sample = file.content.substring(0, 150).replace(/\n/g, '\\n');
|
|
||||||
console.log(` 📖 Échantillon: ${sample}...`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Erreur: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Test de récupération parallèle
|
|
||||||
console.log('\n⚡ Test de récupération parallèle...');
|
|
||||||
const parallelStartTime = Date.now();
|
|
||||||
|
|
||||||
const parallelRequests = [
|
|
||||||
{ env: 'dev', filePath: 'bitcoin/bitcoin.conf' },
|
|
||||||
{ env: 'dev', filePath: 'tor/torrc' },
|
|
||||||
{ env: 'dev', filePath: 'sdk_relay/sdk_relay.conf' },
|
|
||||||
{ env: 'dev', filePath: 'supervisor/supervisord.conf' }
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await client.getFiles(parallelRequests);
|
|
||||||
const parallelEndTime = Date.now();
|
|
||||||
|
|
||||||
console.log(` ✅ ${files.length} fichiers récupérés en ${parallelEndTime - parallelStartTime}ms`);
|
|
||||||
console.log(` 📊 Temps moyen: ${Math.round((parallelEndTime - parallelStartTime) / files.length)}ms par fichier`);
|
|
||||||
|
|
||||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
||||||
console.log(` 📦 Taille totale: ${totalSize} caractères`);
|
|
||||||
|
|
||||||
// Statistiques
|
|
||||||
const encryptedCount = files.filter(f => f.encrypted).length;
|
|
||||||
console.log(` 🔐 Fichiers chiffrés: ${encryptedCount}/${files.length}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Erreur récupération parallèle: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Test des utilitaires crypto
|
|
||||||
console.log('\n🔐 Test des utilitaires de chiffrement...');
|
|
||||||
|
|
||||||
// Génération d'une clé aléatoire
|
|
||||||
const randomKey = VaultCrypto.generateKey();
|
|
||||||
console.log(` 🔑 Clé générée: ${randomKey.substring(0, 10)}... (${randomKey.length} chars)`);
|
|
||||||
console.log(` ✅ Clé valide: ${VaultCrypto.validateKey(randomKey) ? 'Oui' : 'Non'}`);
|
|
||||||
|
|
||||||
// Dérivation depuis un mot de passe
|
|
||||||
const password = 'mon-mot-de-passe-secret-demo';
|
|
||||||
const derivedKey = VaultCrypto.hashToKey(password);
|
|
||||||
console.log(` 🔐 Mot de passe: ${password}`);
|
|
||||||
console.log(` 🔑 Clé dérivée: ${derivedKey.substring(0, 10)}...`);
|
|
||||||
console.log(` ✅ Clé dérivée valide: ${VaultCrypto.validateKey(derivedKey) ? 'Oui' : 'Non'}`);
|
|
||||||
|
|
||||||
// 8. Test de gestion d'erreurs
|
|
||||||
console.log('\n🛡️ Test de gestion d\'erreurs...');
|
|
||||||
|
|
||||||
// Fichier inexistant
|
|
||||||
try {
|
|
||||||
await client.getFile('dev', 'fichier-inexistant.conf');
|
|
||||||
console.log(' ❌ Erreur: Devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✅ Fichier inexistant: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environnement inexistant
|
|
||||||
try {
|
|
||||||
await client.getFile('prod', 'bitcoin/bitcoin.conf');
|
|
||||||
console.log(' ❌ Erreur: Devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✅ Environnement inexistant: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎉 Démonstration terminée avec succès!');
|
|
||||||
console.log('');
|
|
||||||
console.log('📋 Résumé:');
|
|
||||||
console.log(' ✅ API Vault opérationnelle');
|
|
||||||
console.log(' ✅ Chiffrement quantique résistant fonctionnel');
|
|
||||||
console.log(' ✅ Déchiffrement côté client réussi');
|
|
||||||
console.log(' ✅ Variables d\'environnement traitées');
|
|
||||||
console.log(' ✅ Récupération parallèle efficace');
|
|
||||||
console.log(' ✅ Gestion d\'erreurs robuste');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ Erreur fatale:', error instanceof Error ? error.message : error);
|
|
||||||
console.error('');
|
|
||||||
console.error('🔍 Vérifications possibles:');
|
|
||||||
console.error(' • L\'API Vault est-elle démarrée ?');
|
|
||||||
console.error(' • Le port 6666 est-il accessible ?');
|
|
||||||
console.error(' • Les certificats SSL sont-ils valides ?');
|
|
||||||
console.error(' • La clé de déchiffrement est-elle correcte ?');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécution de la démonstration
|
|
||||||
if (require.main === module) {
|
|
||||||
demo().catch(error => {
|
|
||||||
console.error('❌ Erreur inattendue:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { demo };
|
|
@ -1,184 +0,0 @@
|
|||||||
/**
|
|
||||||
* Exemple d'utilisation avancée du SDK Vault 4NK
|
|
||||||
* Démontre les fonctionnalités avancées et la gestion d'erreurs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VaultClient, VaultCrypto, VaultApiError, VaultDecryptionError } from '../src/index';
|
|
||||||
|
|
||||||
async function advancedExample() {
|
|
||||||
console.log('🚀 Exemple d\'utilisation avancée du SDK Vault 4NK');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Génération d'une clé de chiffrement personnalisée
|
|
||||||
console.log('🔑 Génération d\'une clé de chiffrement...');
|
|
||||||
const customKey = VaultCrypto.generateKey();
|
|
||||||
console.log(` Clé générée: ${customKey.substring(0, 10)}...`);
|
|
||||||
|
|
||||||
// Validation de la clé
|
|
||||||
const isValid = VaultCrypto.validateKey(customKey);
|
|
||||||
console.log(` Clé valide: ${isValid ? 'Oui' : 'Non'}`);
|
|
||||||
|
|
||||||
// 2. Création du client avec configuration personnalisée
|
|
||||||
console.log('\n⚙️ Configuration du client...');
|
|
||||||
const client = new VaultClient(
|
|
||||||
{
|
|
||||||
baseUrl: 'https://vault.4nkweb.com:6666',
|
|
||||||
verifySsl: false, // Désactivé pour les certificats auto-signés
|
|
||||||
timeout: 10000, // Timeout de 10 secondes
|
|
||||||
},
|
|
||||||
// Plus de clé de déchiffrement nécessaire - clés dynamiques utilisées
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Gestion d'erreurs avancée
|
|
||||||
console.log('\n🛡️ Test de gestion d\'erreurs...');
|
|
||||||
|
|
||||||
// Test avec un fichier inexistant
|
|
||||||
try {
|
|
||||||
await client.getFile('dev', 'fichier-inexistant.conf');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
console.log(` ✅ Erreur API capturée: ${error.message} (${error.statusCode})`);
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ Erreur inattendue: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test avec un environnement inexistant
|
|
||||||
try {
|
|
||||||
await client.getFile('prod', 'bitcoin/bitcoin.conf');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
console.log(` ✅ Erreur environnement: ${error.message} (${error.statusCode})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Récupération de fichiers avec analyse de contenu
|
|
||||||
console.log('\n📊 Analyse de contenu des fichiers...');
|
|
||||||
|
|
||||||
const filePaths = [
|
|
||||||
'bitcoin/bitcoin.conf',
|
|
||||||
'tor/torrc',
|
|
||||||
'sdk_relay/sdk_relay.conf'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
try {
|
|
||||||
const file = await client.getFile('dev', filePath);
|
|
||||||
|
|
||||||
console.log(`\n 📄 ${file.filename}:`);
|
|
||||||
console.log(` Taille: ${file.size} caractères`);
|
|
||||||
console.log(` Chiffré: ${file.encrypted ? 'Oui' : 'Non'}`);
|
|
||||||
|
|
||||||
// Analyse du contenu
|
|
||||||
const lines = file.content.split('\n');
|
|
||||||
const configLines = lines.filter(line =>
|
|
||||||
line.trim() && !line.trim().startsWith('#')
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(` Lignes totales: ${lines.length}`);
|
|
||||||
console.log(` Lignes de config: ${configLines.length}`);
|
|
||||||
|
|
||||||
// Recherche de variables d'environnement
|
|
||||||
const envVars = file.content.match(/\$\{[^}]+\}/g) || [];
|
|
||||||
if (envVars.length > 0) {
|
|
||||||
console.log(` Variables env: ${envVars.length} (${envVars.slice(0, 3).join(', ')}...)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Erreur pour ${filePath}: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Test de performance avec récupération parallèle
|
|
||||||
console.log('\n⚡ Test de performance...');
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const parallelRequests = [
|
|
||||||
{ env: 'dev', filePath: 'bitcoin/bitcoin.conf' },
|
|
||||||
{ env: 'dev', filePath: 'tor/torrc' },
|
|
||||||
{ env: 'dev', filePath: 'sdk_relay/sdk_relay.conf' },
|
|
||||||
{ env: 'dev', filePath: 'supervisor/supervisord.conf' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const files = await client.getFiles(parallelRequests);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
console.log(` Fichiers récupérés: ${files.length}`);
|
|
||||||
console.log(` Temps total: ${endTime - startTime}ms`);
|
|
||||||
console.log(` Temps moyen par fichier: ${Math.round((endTime - startTime) / files.length)}ms`);
|
|
||||||
|
|
||||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
||||||
console.log(` Taille totale: ${totalSize} caractères`);
|
|
||||||
|
|
||||||
// 6. Monitoring et métriques
|
|
||||||
console.log('\n📈 Monitoring et métriques...');
|
|
||||||
|
|
||||||
// Test de connectivité répété
|
|
||||||
const pingTests = 5;
|
|
||||||
const pingResults: boolean[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < pingTests; i++) {
|
|
||||||
const isConnected = await client.ping();
|
|
||||||
pingResults.push(isConnected);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100)); // Délai de 100ms
|
|
||||||
}
|
|
||||||
|
|
||||||
const successRate = (pingResults.filter(r => r).length / pingTests) * 100;
|
|
||||||
console.log(` Tests de connectivité: ${pingTests}`);
|
|
||||||
console.log(` Taux de succès: ${successRate}%`);
|
|
||||||
|
|
||||||
// 7. Utilisation de clés dérivées
|
|
||||||
console.log('\n🔐 Test de clés dérivées...');
|
|
||||||
const password = 'mon-mot-de-passe-secret';
|
|
||||||
const derivedKey = VaultCrypto.hashToKey(password);
|
|
||||||
console.log(` Mot de passe: ${password}`);
|
|
||||||
console.log(` Clé dérivée: ${derivedKey.substring(0, 10)}...`);
|
|
||||||
console.log(` Clé valide: ${VaultCrypto.validateKey(derivedKey) ? 'Oui' : 'Non'}`);
|
|
||||||
|
|
||||||
console.log('\n🎉 Exemple avancé terminé avec succès!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur fatale:', error instanceof Error ? error.message : error);
|
|
||||||
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
console.error(` Code d'erreur: ${error.statusCode}`);
|
|
||||||
console.error(` Endpoint: ${error.endpoint}`);
|
|
||||||
} else if (error instanceof VaultDecryptionError) {
|
|
||||||
console.error(' Type: Erreur de déchiffrement');
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction utilitaire pour les tests de performance
|
|
||||||
async function performanceTest(client: VaultClient, iterations: number = 10) {
|
|
||||||
console.log(`\n🏃 Test de performance (${iterations} itérations)...`);
|
|
||||||
|
|
||||||
const times: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
|
||||||
const start = Date.now();
|
|
||||||
await client.getFile('dev', 'bitcoin/bitcoin.conf');
|
|
||||||
const end = Date.now();
|
|
||||||
times.push(end - start);
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgTime = times.reduce((sum, time) => sum + time, 0) / times.length;
|
|
||||||
const minTime = Math.min(...times);
|
|
||||||
const maxTime = Math.max(...times);
|
|
||||||
|
|
||||||
console.log(` Temps moyen: ${Math.round(avgTime)}ms`);
|
|
||||||
console.log(` Temps min: ${minTime}ms`);
|
|
||||||
console.log(` Temps max: ${maxTime}ms`);
|
|
||||||
|
|
||||||
return { avgTime, minTime, maxTime };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécution de l'exemple
|
|
||||||
if (require.main === module) {
|
|
||||||
advancedExample();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { advancedExample, performanceTest };
|
|
@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* Exemple d'utilisation basique du SDK Vault 4NK
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VaultClient, createVaultClient, VaultCrypto } from '../src/index';
|
|
||||||
|
|
||||||
async function basicExample() {
|
|
||||||
console.log('🚀 Exemple d\'utilisation basique du SDK Vault 4NK');
|
|
||||||
console.log('=' .repeat(50));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Création du client avec la clé de déchiffrement
|
|
||||||
const client = createVaultClient(
|
|
||||||
'https://vault.4nkweb.com:6666',
|
|
||||||
// Plus de clé de déchiffrement nécessaire - clés dynamiques utilisées
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Vérification de la connectivité
|
|
||||||
console.log('🔍 Test de connectivité...');
|
|
||||||
const isConnected = await client.ping();
|
|
||||||
console.log(`✅ Connecté: ${isConnected ? 'Oui' : 'Non'}`);
|
|
||||||
|
|
||||||
if (!isConnected) {
|
|
||||||
throw new Error('Impossible de se connecter à l\'API Vault');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Informations sur l'API
|
|
||||||
console.log('\n📋 Informations API...');
|
|
||||||
const info = await client.info();
|
|
||||||
console.log(` Nom: ${info.name}`);
|
|
||||||
console.log(` Version: ${info.version}`);
|
|
||||||
console.log(` Domaine: ${info.domain}`);
|
|
||||||
console.log(` Port: ${info.port}`);
|
|
||||||
console.log(` Chiffrement: ${info.encryption}`);
|
|
||||||
|
|
||||||
// 4. État de santé
|
|
||||||
console.log('\n🏥 État de santé...');
|
|
||||||
const health = await client.health();
|
|
||||||
console.log(` Statut: ${health.status}`);
|
|
||||||
console.log(` Service: ${health.service}`);
|
|
||||||
console.log(` Algorithme: ${health.algorithm}`);
|
|
||||||
|
|
||||||
// 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 ? 'Oui' : 'Non'}`);
|
|
||||||
console.log(` Algorithme: ${file.algorithm || 'N/A'}`);
|
|
||||||
|
|
||||||
// Affichage d'un échantillon du contenu
|
|
||||||
const sample = file.content.substring(0, 200);
|
|
||||||
console.log(` Échantillon: ${sample}...`);
|
|
||||||
|
|
||||||
// 6. Récupération de plusieurs fichiers
|
|
||||||
console.log('\n📚 Récupération de plusieurs fichiers...');
|
|
||||||
const files = await client.getFiles([
|
|
||||||
{ env: 'dev', filePath: 'tor/torrc' },
|
|
||||||
{ env: 'dev', filePath: 'sdk_relay/sdk_relay.conf' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
|
||||||
console.log(` Fichier ${index + 1}: ${file.filename} (${file.size} chars)`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n🎉 Exemple terminé avec succès!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur:', error instanceof Error ? error.message : error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécution de l'exemple
|
|
||||||
if (require.main === module) {
|
|
||||||
basicExample();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { basicExample };
|
|
@ -1,240 +0,0 @@
|
|||||||
/**
|
|
||||||
* Exemple de gestion d'erreurs avancée avec le SDK Vault 4NK
|
|
||||||
* Démontre les différents types d'erreurs et leur gestion
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VaultClient, VaultApiError, VaultDecryptionError, VaultCrypto } from '../src/index';
|
|
||||||
|
|
||||||
async function errorHandlingExample() {
|
|
||||||
console.log('🛡️ Exemple de gestion d\'erreurs - SDK Vault 4NK');
|
|
||||||
console.log('=' .repeat(55));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = new VaultClient(
|
|
||||||
{ baseUrl: 'https://vault.4nkweb.com:6666' },
|
|
||||||
// Plus de clé de déchiffrement nécessaire - clés dynamiques utilisées
|
|
||||||
);
|
|
||||||
|
|
||||||
// 1. Test des erreurs de connectivité
|
|
||||||
console.log('🌐 Test des erreurs de connectivité...');
|
|
||||||
await testConnectivityErrors(client);
|
|
||||||
|
|
||||||
// 2. Test des erreurs de fichiers
|
|
||||||
console.log('\n📁 Test des erreurs de fichiers...');
|
|
||||||
await testFileErrors(client);
|
|
||||||
|
|
||||||
// 3. Test des erreurs de déchiffrement
|
|
||||||
console.log('\n🔐 Test des erreurs de déchiffrement...');
|
|
||||||
await testDecryptionErrors();
|
|
||||||
|
|
||||||
// 4. Test des erreurs de configuration
|
|
||||||
console.log('\n⚙️ Test des erreurs de configuration...');
|
|
||||||
await testConfigurationErrors();
|
|
||||||
|
|
||||||
// 5. Gestion d'erreurs avec retry
|
|
||||||
console.log('\n🔄 Test de retry automatique...');
|
|
||||||
await testRetryLogic(client);
|
|
||||||
|
|
||||||
console.log('\n✅ Tous les tests de gestion d\'erreurs terminés!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erreur inattendue:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testConnectivityErrors(client: VaultClient) {
|
|
||||||
// Test avec une URL incorrecte
|
|
||||||
const badClient = new VaultClient(
|
|
||||||
{ baseUrl: 'https://api-inexistante.example.com:6666' },
|
|
||||||
'quantum_resistant_demo_key_32_bytes!'
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await badClient.health();
|
|
||||||
console.log(' ❌ Erreur: Devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
console.log(` ✅ Erreur de connectivité capturée: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✅ Erreur réseau capturée: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test de timeout
|
|
||||||
const timeoutClient = new VaultClient(
|
|
||||||
{ baseUrl: 'https://vault.4nkweb.com:6666', timeout: 1 }, // 1ms timeout
|
|
||||||
'quantum_resistant_demo_key_32_bytes!'
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await timeoutClient.health();
|
|
||||||
console.log(' ❌ Erreur: Devrait timeout');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError && error.statusCode === 408) {
|
|
||||||
console.log(` ✅ Timeout capturé: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✅ Erreur timeout: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testFileErrors(client: VaultClient) {
|
|
||||||
// Test avec un fichier inexistant
|
|
||||||
try {
|
|
||||||
await client.getFile('dev', 'fichier-inexistant.txt');
|
|
||||||
console.log(' ❌ Erreur: Fichier inexistant devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError && error.statusCode === 404) {
|
|
||||||
console.log(` ✅ Fichier inexistant: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✅ Erreur fichier: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test avec un environnement inexistant
|
|
||||||
try {
|
|
||||||
await client.getFile('prod', 'bitcoin/bitcoin.conf');
|
|
||||||
console.log(' ❌ Erreur: Environnement inexistant devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
console.log(` ✅ Environnement inexistant: ${error.message} (${error.statusCode})`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✅ Erreur environnement: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test avec un chemin invalide (tentative d'accès hors du répertoire)
|
|
||||||
try {
|
|
||||||
await client.getFile('dev', '../../../etc/passwd');
|
|
||||||
console.log(' ❌ Erreur: Chemin invalide devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError && error.statusCode === 403) {
|
|
||||||
console.log(` ✅ Chemin invalide (sécurité): ${error.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✅ Erreur sécurité: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testDecryptionErrors() {
|
|
||||||
// Test avec une clé de déchiffrement incorrecte
|
|
||||||
const badKeyClient = new VaultClient(
|
|
||||||
{ baseUrl: 'https://vault.4nkweb.com:6666' },
|
|
||||||
'clé-incorrecte-de-32-bytes!' // Clé différente
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await badKeyClient.getFile('dev', 'bitcoin/bitcoin.conf');
|
|
||||||
console.log(' ❌ Erreur: Clé incorrecte devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultDecryptionError) {
|
|
||||||
console.log(` ✅ Erreur de déchiffrement: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✅ Erreur clé: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test avec une clé de taille incorrecte
|
|
||||||
try {
|
|
||||||
new VaultClient(
|
|
||||||
{ baseUrl: 'https://vault.4nkweb.com:6666' },
|
|
||||||
'clé-trop-courte' // Moins de 32 bytes
|
|
||||||
);
|
|
||||||
console.log(' ❌ Erreur: Clé trop courte devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✅ Clé invalide: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testConfigurationErrors() {
|
|
||||||
// Test avec une URL malformée
|
|
||||||
try {
|
|
||||||
new VaultClient(
|
|
||||||
{ baseUrl: 'url-malformée' },
|
|
||||||
// Plus de clé de déchiffrement nécessaire - clés dynamiques utilisées
|
|
||||||
);
|
|
||||||
console.log(' ❌ Erreur: URL malformée devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✅ URL malformée: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test avec un port invalide
|
|
||||||
try {
|
|
||||||
new VaultClient(
|
|
||||||
{ baseUrl: 'https://vault.4nkweb.com:99999' }, // Port invalide
|
|
||||||
// Plus de clé de déchiffrement nécessaire - clés dynamiques utilisées
|
|
||||||
);
|
|
||||||
console.log(' ❌ Erreur: Port invalide devrait échouer');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✅ Port invalide: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testRetryLogic(client: VaultClient) {
|
|
||||||
// Implémentation d'un retry simple
|
|
||||||
async function retryOperation<T>(
|
|
||||||
operation: () => Promise<T>,
|
|
||||||
maxRetries: number = 3,
|
|
||||||
delay: number = 1000
|
|
||||||
): Promise<T> {
|
|
||||||
let lastError: Error;
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
||||||
try {
|
|
||||||
return await operation();
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error as Error;
|
|
||||||
console.log(` 🔄 Tentative ${attempt}/${maxRetries} échouée: ${lastError.message}`);
|
|
||||||
|
|
||||||
if (attempt < maxRetries) {
|
|
||||||
console.log(` ⏳ Attente de ${delay}ms avant retry...`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastError!;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test de retry avec une opération qui peut échouer
|
|
||||||
try {
|
|
||||||
const result = await retryOperation(
|
|
||||||
() => client.getFile('dev', 'bitcoin/bitcoin.conf'),
|
|
||||||
3,
|
|
||||||
500
|
|
||||||
);
|
|
||||||
console.log(` ✅ Retry réussi: ${result.filename} (${result.size} chars)`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ Retry échoué après toutes les tentatives: ${error instanceof Error ? error.message : error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction utilitaire pour logger les erreurs
|
|
||||||
function logError(error: unknown, context: string) {
|
|
||||||
console.log(`\n🚨 Erreur dans ${context}:`);
|
|
||||||
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
console.log(` Type: VaultApiError`);
|
|
||||||
console.log(` Message: ${error.message}`);
|
|
||||||
console.log(` Code: ${error.statusCode}`);
|
|
||||||
console.log(` Endpoint: ${error.endpoint}`);
|
|
||||||
} else if (error instanceof VaultDecryptionError) {
|
|
||||||
console.log(` Type: VaultDecryptionError`);
|
|
||||||
console.log(` Message: ${error.message}`);
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
console.log(` Type: ${error.constructor.name}`);
|
|
||||||
console.log(` Message: ${error.message}`);
|
|
||||||
console.log(` Stack: ${error.stack?.split('\n').slice(0, 3).join('\n')}`);
|
|
||||||
} else {
|
|
||||||
console.log(` Type: Inconnu`);
|
|
||||||
console.log(` Valeur: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécution de l'exemple
|
|
||||||
if (require.main === module) {
|
|
||||||
errorHandlingExample();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { errorHandlingExample, logError };
|
|
@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Exemple d'utilisation du client sécurisé Vault
|
* Exemple d'utilisation du client Vault
|
||||||
* Avec authentification par clés utilisateur et rotation automatique
|
* Avec authentification par clés utilisateur et rotation automatique
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SecureVaultClient, createSecureVaultClient } from '../src/secure-client';
|
import { SecureVaultClient, createSecureVaultClient } from '../src/index';
|
||||||
|
|
||||||
async function secureBasicExample() {
|
async function basicExample() {
|
||||||
console.log('🔐 Exemple d\'utilisation du client sécurisé Vault');
|
console.log('🔐 Exemple d\'utilisation du client Vault');
|
||||||
console.log('=' * 60);
|
console.log('=' * 60);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -61,7 +61,7 @@ async function secureBasicExample() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function secureAdvancedExample() {
|
async function advancedExample() {
|
||||||
console.log('\n🔐 Exemple avancé avec gestion d\'erreurs');
|
console.log('\n🔐 Exemple avancé avec gestion d\'erreurs');
|
||||||
console.log('=' * 60);
|
console.log('=' * 60);
|
||||||
|
|
||||||
@ -145,8 +145,8 @@ async function secureAdvancedExample() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function secureErrorHandlingExample() {
|
async function errorHandlingExample() {
|
||||||
console.log('\n🔐 Exemple de gestion d\'erreurs sécurisée');
|
console.log('\n🔐 Exemple de gestion d\'erreurs');
|
||||||
console.log('=' * 60);
|
console.log('=' * 60);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -255,9 +255,9 @@ async function main() {
|
|||||||
console.log('=' * 80);
|
console.log('=' * 80);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secureBasicExample();
|
await basicExample();
|
||||||
await secureAdvancedExample();
|
await advancedExample();
|
||||||
await secureErrorHandlingExample();
|
await errorHandlingExample();
|
||||||
|
|
||||||
console.log('\n🎉 Toutes les démonstrations terminées avec succès!');
|
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('\n📝 Points clés du système sécurisé:');
|
||||||
@ -279,7 +279,7 @@ if (require.main === module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
secureBasicExample,
|
basicExample,
|
||||||
secureAdvancedExample,
|
advancedExample,
|
||||||
secureErrorHandlingExample
|
errorHandlingExample
|
||||||
};
|
};
|
@ -1,16 +1,11 @@
|
|||||||
/**
|
|
||||||
* SDK Client TypeScript pour l'API Vault 4NK
|
|
||||||
* Permet de récupérer et déchiffrer les fichiers depuis l'API Vault
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createDecipher } from 'crypto';
|
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
// Types TypeScript
|
// Types pour l'API sécurisée
|
||||||
export interface VaultConfig {
|
export interface VaultConfig {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
verifySsl?: boolean;
|
verifySsl?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
userId: string; // ID utilisateur obligatoire
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultFile {
|
export interface VaultFile {
|
||||||
@ -19,6 +14,9 @@ export interface VaultFile {
|
|||||||
size: number;
|
size: number;
|
||||||
encrypted: boolean;
|
encrypted: boolean;
|
||||||
algorithm?: string | undefined;
|
algorithm?: string | undefined;
|
||||||
|
user_id?: string | undefined;
|
||||||
|
key_version?: string | undefined;
|
||||||
|
timestamp?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultHealth {
|
export interface VaultHealth {
|
||||||
@ -26,6 +24,9 @@ export interface VaultHealth {
|
|||||||
service: string;
|
service: string;
|
||||||
encryption: string;
|
encryption: string;
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
|
authentication?: string;
|
||||||
|
key_rotation?: string;
|
||||||
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultInfo {
|
export interface VaultInfo {
|
||||||
@ -35,142 +36,73 @@ export interface VaultInfo {
|
|||||||
port: number;
|
port: number;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
encryption: string;
|
encryption: string;
|
||||||
endpoints: Record<string, string>;
|
authentication?: string;
|
||||||
|
key_rotation?: string;
|
||||||
|
endpoints?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VaultApiError extends Error {
|
// Classes d'erreurs personnalisées
|
||||||
constructor(
|
export interface VaultError {
|
||||||
message: string,
|
message: string;
|
||||||
public statusCode?: number,
|
code?: string | undefined;
|
||||||
public endpoint?: string
|
statusCode?: number | undefined;
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
export class VaultApiError extends Error implements VaultError {
|
||||||
|
public readonly code?: string | undefined;
|
||||||
|
public readonly statusCode?: number | undefined;
|
||||||
|
|
||||||
|
constructor(message: string, code?: string, statusCode?: number) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'VaultApiError';
|
this.name = 'VaultApiError';
|
||||||
|
this.code = code;
|
||||||
|
this.statusCode = statusCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VaultDecryptionError extends Error {
|
export class VaultDecryptionError extends Error implements VaultError {
|
||||||
constructor(message: string) {
|
public readonly code?: string | undefined;
|
||||||
|
|
||||||
|
constructor(message: string, code?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'VaultDecryptionError';
|
this.name = 'VaultDecryptionError';
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VaultAuthenticationError extends Error implements VaultError {
|
||||||
|
public readonly code?: string | undefined;
|
||||||
|
public readonly statusCode?: number | undefined;
|
||||||
|
|
||||||
|
constructor(message: string, code?: string, statusCode?: number) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'VaultAuthenticationError';
|
||||||
|
this.code = code;
|
||||||
|
this.statusCode = statusCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client principal pour l'API Vault 4NK
|
* Client sécurisé pour l'API Vault avec authentification par clés utilisateur
|
||||||
|
* Les clés sont gérées côté serveur avec rotation automatique
|
||||||
*/
|
*/
|
||||||
export class VaultClient {
|
export class SecureVaultClient {
|
||||||
private config: VaultConfig;
|
private config: VaultConfig;
|
||||||
private _decryptionKey: Buffer; // Conservé pour compatibilité mais non utilisé
|
|
||||||
|
|
||||||
constructor(config: VaultConfig, decryptionKey?: string) {
|
constructor(config: VaultConfig) {
|
||||||
this.config = {
|
this.config = {
|
||||||
verifySsl: false,
|
verifySsl: false,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
// La clé de déchiffrement n'est plus nécessaire avec le nouveau système de clés dynamiques
|
// Validation de l'ID utilisateur
|
||||||
// Elle est conservée pour la compatibilité mais n'est plus utilisée
|
if (!config.userId || config.userId.length < 3 || config.userId.length > 50) {
|
||||||
if (decryptionKey) {
|
throw new Error('ID utilisateur requis (3-50 caractères)');
|
||||||
const keyLength = Buffer.byteLength(decryptionKey, 'utf8');
|
|
||||||
if (keyLength !== 32) {
|
|
||||||
throw new Error(`La clé de déchiffrement doit faire exactement 32 bytes (reçu: ${keyLength} bytes)`);
|
|
||||||
}
|
|
||||||
this._decryptionKey = Buffer.from(decryptionKey, 'utf8');
|
|
||||||
} else {
|
|
||||||
// Clé par défaut non utilisée
|
|
||||||
this._decryptionKey = Buffer.alloc(32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppression de l'avertissement TypeScript
|
|
||||||
void this._decryptionKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (!/^[a-zA-Z0-9_-]+$/.test(config.userId)) {
|
||||||
* Effectue une requête HTTPS vers l'API Vault
|
throw new Error('ID utilisateur invalide - caractères autorisés: a-z, A-Z, 0-9, _, -');
|
||||||
*/
|
|
||||||
private async makeRequest<T>(
|
|
||||||
endpoint: string,
|
|
||||||
options: RequestInit = {}
|
|
||||||
): Promise<T> {
|
|
||||||
const url = `${this.config.baseUrl}${endpoint}`;
|
|
||||||
|
|
||||||
const defaultOptions: RequestInit = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json, application/octet-stream',
|
|
||||||
'User-Agent': 'VaultSDK-TypeScript/1.0.0',
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(this.config.timeout!),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergedOptions = { ...defaultOptions, ...options } as any;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, mergedOptions);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur API: ${response.status} ${response.statusText}`,
|
|
||||||
response.status,
|
|
||||||
endpoint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
|
|
||||||
if (contentType?.includes('application/json')) {
|
|
||||||
return await response.json() as T;
|
|
||||||
} else {
|
|
||||||
// Pour les fichiers chiffrés, retourner le buffer
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
return Buffer.from(arrayBuffer) as unknown as T;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.name === 'AbortError') {
|
|
||||||
throw new VaultApiError('Timeout de la requête', 408, endpoint);
|
|
||||||
}
|
|
||||||
throw new VaultApiError(`Erreur réseau: ${error.message}`, undefined, endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new VaultApiError('Erreur inconnue', undefined, endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Déchiffre le contenu d'un fichier
|
|
||||||
*/
|
|
||||||
private decryptContent(encryptedContent: Buffer): string {
|
|
||||||
try {
|
|
||||||
// Décoder le base64
|
|
||||||
const decoded = Buffer.from(encryptedContent.toString(), 'base64');
|
|
||||||
|
|
||||||
// Nouveau format: nonce (12 bytes) + clé de session (32 bytes) + contenu chiffré
|
|
||||||
if (decoded.length < 44) {
|
|
||||||
throw new Error('Données chiffrées invalides - format incorrect');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonce = decoded.subarray(0, 12);
|
|
||||||
const sessionKey = decoded.subarray(12, 44);
|
|
||||||
const ciphertext = decoded.subarray(44);
|
|
||||||
|
|
||||||
// Déchiffrer avec la clé de session dynamique
|
|
||||||
const decipher = createDecipher('chacha20-poly1305', sessionKey, nonce);
|
|
||||||
|
|
||||||
let decrypted = decipher.update(ciphertext, undefined, 'utf8');
|
|
||||||
decrypted += decipher.final('utf8');
|
|
||||||
|
|
||||||
return decrypted;
|
|
||||||
} catch (error) {
|
|
||||||
throw new VaultDecryptionError(
|
|
||||||
`Erreur de déchiffrement: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,124 +110,260 @@ export class VaultClient {
|
|||||||
* Récupère un fichier depuis l'API Vault
|
* Récupère un fichier depuis l'API Vault
|
||||||
*/
|
*/
|
||||||
async getFile(env: string, filePath: string): Promise<VaultFile> {
|
async getFile(env: string, filePath: string): Promise<VaultFile> {
|
||||||
const endpoint = `/${env}/${filePath}`;
|
const url = `${this.config.baseUrl}/${env}/${filePath}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encryptedContent = await this.makeRequest<Buffer>(endpoint);
|
const response = await this._fetchApi(url);
|
||||||
|
|
||||||
// Vérifier si le contenu est en base64 (chiffré)
|
if (!response.ok) {
|
||||||
let content: string;
|
if (response.status === 401) {
|
||||||
let encrypted = true;
|
throw new VaultAuthenticationError(
|
||||||
|
'Authentification échouée - vérifiez votre ID utilisateur',
|
||||||
try {
|
'AUTH_FAILED',
|
||||||
content = this.decryptContent(encryptedContent);
|
response.status
|
||||||
} catch (error) {
|
);
|
||||||
// Si le déchiffrement échoue, essayer de décoder en base64 simple
|
|
||||||
try {
|
|
||||||
content = Buffer.from(encryptedContent.toString(), 'base64').toString('utf8');
|
|
||||||
encrypted = false;
|
|
||||||
} catch {
|
|
||||||
throw new VaultDecryptionError('Impossible de déchiffrer ou décoder le contenu');
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
const decryptedContent = this.decryptContent(Buffer.from(encryptedData));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content: decryptedContent,
|
||||||
filename: filePath.split('/').pop() || filePath,
|
filename: filePath.split('/').pop() || filePath,
|
||||||
size: content.length,
|
size: decryptedContent.length,
|
||||||
encrypted,
|
encrypted: true,
|
||||||
algorithm: encrypted ? 'ChaCha20-Poly1305' : undefined,
|
algorithm,
|
||||||
|
user_id,
|
||||||
|
key_version: keyRotation,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultApiError && error.statusCode === 404) {
|
if (error instanceof VaultApiError || error instanceof VaultAuthenticationError) {
|
||||||
throw new VaultApiError(`Fichier non trouvé: ${env}/${filePath}`, 404, endpoint);
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
throw new VaultApiError(
|
||||||
}
|
`Erreur lors de la récupération du fichier: ${error instanceof Error ? error.message : 'Inconnue'}`
|
||||||
}
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie l'état de santé de l'API
|
|
||||||
*/
|
|
||||||
async health(): Promise<VaultHealth> {
|
|
||||||
return await this.makeRequest<VaultHealth>('/health');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les informations de l'API
|
|
||||||
*/
|
|
||||||
async info(): Promise<VaultInfo> {
|
|
||||||
return await this.makeRequest<VaultInfo>('/info');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Teste la connectivité à l'API
|
|
||||||
*/
|
|
||||||
async ping(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.health();
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère plusieurs fichiers en parallèle
|
* Récupère plusieurs fichiers en parallèle
|
||||||
*/
|
*/
|
||||||
async getFiles(requests: Array<{ env: string; filePath: string }>): Promise<VaultFile[]> {
|
async getFiles(env: string, filePaths: string[]): Promise<VaultFile[]> {
|
||||||
const promises = requests.map(req => this.getFile(req.env, req.filePath));
|
const promises = filePaths.map(filePath => this.getFile(env, filePath));
|
||||||
return await Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recherche des fichiers par pattern dans un environnement
|
* Recherche des fichiers correspondant à un pattern
|
||||||
*/
|
*/
|
||||||
async searchFiles(_env: string, _pattern?: RegExp): Promise<string[]> {
|
async searchFiles(_env: string, _pattern: string): Promise<VaultFile[]> {
|
||||||
// Note: Cette méthode nécessiterait un endpoint de recherche côté serveur
|
// Cette fonctionnalité nécessiterait une implémentation côté serveur
|
||||||
// Pour l'instant, retourne une liste vide
|
// Pour l'instant, retour d'une erreur explicative
|
||||||
console.warn('La recherche de fichiers n\'est pas encore implémentée côté serveur');
|
throw new VaultApiError(
|
||||||
return [];
|
'Recherche de fichiers non implémentée - contactez l\'administrateur',
|
||||||
|
'NOT_IMPLEMENTED'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'état de santé de l'API
|
||||||
|
*/
|
||||||
|
async health(): Promise<VaultHealth> {
|
||||||
|
const url = `${this.config.baseUrl}/health`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this._fetchApi(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new VaultApiError(
|
||||||
|
`Erreur de santé API: ${response.status}`,
|
||||||
|
'HEALTH_CHECK_FAILED',
|
||||||
|
response.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json() as VaultHealth;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof VaultApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new VaultApiError(
|
||||||
|
`Erreur lors du contrôle de santé: ${error instanceof Error ? error.message : 'Inconnue'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les informations sur l'API
|
||||||
|
*/
|
||||||
|
async info(): Promise<VaultInfo> {
|
||||||
|
const url = `${this.config.baseUrl}/info`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this._fetchApi(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new VaultApiError(
|
||||||
|
`Erreur info API: ${response.status}`,
|
||||||
|
'INFO_ERROR',
|
||||||
|
response.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json() as VaultInfo;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof VaultApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new VaultApiError(
|
||||||
|
`Erreur lors de la récupération des informations: ${error instanceof Error ? error.message : 'Inconnue'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test de connectivité simple
|
||||||
|
*/
|
||||||
|
async ping(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.health();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déchiffre le contenu avec les métadonnées utilisateur
|
||||||
|
*/
|
||||||
|
private decryptContent(encryptedData: Buffer): string {
|
||||||
|
try {
|
||||||
|
// Décoder le base64
|
||||||
|
const decoded = Buffer.from(encryptedData.toString(), 'base64');
|
||||||
|
|
||||||
|
// Nouveau format: nonce (12 bytes) + taille_métadonnées (4 bytes) + métadonnées + contenu chiffré
|
||||||
|
if (decoded.length < 16) {
|
||||||
|
throw new Error('Données chiffrées invalides - format incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// const nonce = decoded.subarray(0, 12); // Non utilisé pour l'instant
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Le déchiffrement nécessiterait la clé utilisateur
|
||||||
|
// qui est gérée côté serveur. Dans cette implémentation,
|
||||||
|
// nous simulons le déchiffrement pour la démonstration.
|
||||||
|
// En production, il faudrait implémenter un échange de clés sécurisé.
|
||||||
|
|
||||||
|
// Pour la démonstration, on retourne un message indiquant
|
||||||
|
// que le déchiffrement nécessite une clé côté serveur
|
||||||
|
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` +
|
||||||
|
`Taille chiffrée: ${ciphertext.length} bytes`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof VaultAuthenticationError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new VaultDecryptionError(
|
||||||
|
`Erreur de déchiffrement: ${error instanceof Error ? error.message : 'Inconnue'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effectue une requête vers l'API avec authentification
|
||||||
|
*/
|
||||||
|
private async _fetchApi(url: string): Promise<Response> {
|
||||||
|
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',
|
||||||
|
...(this.config.verifySsl === false && { 'X-Skip-SSL-Verify': 'true' })
|
||||||
|
},
|
||||||
|
signal: controller.signal
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, mergedOptions as any) as any;
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
return response;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
throw new VaultApiError(
|
||||||
|
`Timeout de la requête (${this.config.timeout}ms)`,
|
||||||
|
'TIMEOUT'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new VaultApiError(
|
||||||
|
`Erreur de connexion: ${error instanceof Error ? error.message : 'Inconnue'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory pour créer une instance du client Vault
|
* Fonction utilitaire pour créer un client sécurisé
|
||||||
*/
|
*/
|
||||||
export function createVaultClient(
|
export function createSecureVaultClient(baseUrl: string, userId: string): SecureVaultClient {
|
||||||
baseUrl: string = 'https://vault.4nkweb.com:6666',
|
return new SecureVaultClient({
|
||||||
decryptionKey: string = 'quantum_resistant_demo_key_32_bytes!'
|
baseUrl,
|
||||||
): VaultClient {
|
userId,
|
||||||
return new VaultClient({ baseUrl }, decryptionKey);
|
verifySsl: false, // Pour les certificats auto-signés en développement
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilitaires pour la gestion des clés de chiffrement
|
* Fonction utilitaire pour créer un client sécurisé avec configuration complète
|
||||||
*/
|
*/
|
||||||
export class VaultCrypto {
|
export function createSecureVaultClientWithConfig(config: VaultConfig): SecureVaultClient {
|
||||||
/**
|
return new SecureVaultClient(config);
|
||||||
* Génère une clé de déchiffrement aléatoire (32 bytes)
|
|
||||||
*/
|
|
||||||
static generateKey(): string {
|
|
||||||
const crypto = require('crypto');
|
|
||||||
return crypto.randomBytes(32).toString('utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie qu'une clé est valide (32 bytes)
|
|
||||||
*/
|
|
||||||
static validateKey(key: string): boolean {
|
|
||||||
return Buffer.byteLength(key, 'utf8') === 32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hache une clé pour générer une clé de 32 bytes
|
|
||||||
*/
|
|
||||||
static hashToKey(password: string): string {
|
|
||||||
const crypto = require('crypto');
|
|
||||||
return crypto.createHash('sha256').update(password).digest('utf8');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export par défaut
|
|
||||||
export default VaultClient;
|
|
||||||
|
@ -1,369 +0,0 @@
|
|||||||
import fetch from 'node-fetch';
|
|
||||||
|
|
||||||
// Types pour l'API sécurisée
|
|
||||||
export interface VaultConfig {
|
|
||||||
baseUrl: string;
|
|
||||||
verifySsl?: boolean;
|
|
||||||
timeout?: number;
|
|
||||||
userId: string; // ID utilisateur obligatoire
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VaultFile {
|
|
||||||
content: string;
|
|
||||||
filename: string;
|
|
||||||
size: number;
|
|
||||||
encrypted: boolean;
|
|
||||||
algorithm?: string | undefined;
|
|
||||||
user_id?: string | undefined;
|
|
||||||
key_version?: string | undefined;
|
|
||||||
timestamp?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VaultHealth {
|
|
||||||
status: string;
|
|
||||||
service: string;
|
|
||||||
encryption: string;
|
|
||||||
algorithm: string;
|
|
||||||
authentication?: string;
|
|
||||||
key_rotation?: string;
|
|
||||||
timestamp?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VaultInfo {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
domain: string;
|
|
||||||
port: number;
|
|
||||||
protocol: string;
|
|
||||||
encryption: string;
|
|
||||||
authentication?: string;
|
|
||||||
key_rotation?: string;
|
|
||||||
endpoints?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classes d'erreurs personnalisées
|
|
||||||
export interface VaultError {
|
|
||||||
message: string;
|
|
||||||
code?: string | undefined;
|
|
||||||
statusCode?: number | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VaultApiError extends Error implements VaultError {
|
|
||||||
public readonly code?: string | undefined;
|
|
||||||
public readonly statusCode?: number | undefined;
|
|
||||||
|
|
||||||
constructor(message: string, code?: string, statusCode?: number) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'VaultApiError';
|
|
||||||
this.code = code;
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VaultDecryptionError extends Error implements VaultError {
|
|
||||||
public readonly code?: string | undefined;
|
|
||||||
|
|
||||||
constructor(message: string, code?: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'VaultDecryptionError';
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VaultAuthenticationError extends Error implements VaultError {
|
|
||||||
public readonly code?: string | undefined;
|
|
||||||
public readonly statusCode?: number | undefined;
|
|
||||||
|
|
||||||
constructor(message: string, code?: string, statusCode?: number) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'VaultAuthenticationError';
|
|
||||||
this.code = code;
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client sécurisé pour l'API Vault avec authentification par clés utilisateur
|
|
||||||
* Les clés sont gérées côté serveur avec rotation automatique
|
|
||||||
*/
|
|
||||||
export class SecureVaultClient {
|
|
||||||
private config: VaultConfig;
|
|
||||||
|
|
||||||
constructor(config: VaultConfig) {
|
|
||||||
this.config = {
|
|
||||||
verifySsl: false,
|
|
||||||
timeout: 30000,
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 (!/^[a-zA-Z0-9_-]+$/.test(config.userId)) {
|
|
||||||
throw new Error('ID utilisateur invalide - caractères autorisés: a-z, A-Z, 0-9, _, -');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère un fichier depuis l'API Vault
|
|
||||||
*/
|
|
||||||
async getFile(env: string, filePath: string): Promise<VaultFile> {
|
|
||||||
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
|
|
||||||
const decryptedContent = this.decryptContent(Buffer.from(encryptedData));
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: decryptedContent,
|
|
||||||
filename: filePath.split('/').pop() || filePath,
|
|
||||||
size: decryptedContent.length,
|
|
||||||
encrypted: true,
|
|
||||||
algorithm,
|
|
||||||
user_id,
|
|
||||||
key_version: keyRotation,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError || error instanceof VaultAuthenticationError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur lors de la récupération du fichier: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère plusieurs fichiers en parallèle
|
|
||||||
*/
|
|
||||||
async getFiles(env: string, filePaths: string[]): Promise<VaultFile[]> {
|
|
||||||
const promises = filePaths.map(filePath => this.getFile(env, filePath));
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche des fichiers correspondant à un pattern
|
|
||||||
*/
|
|
||||||
async searchFiles(_env: string, _pattern: string): Promise<VaultFile[]> {
|
|
||||||
// Cette fonctionnalité nécessiterait une implémentation côté serveur
|
|
||||||
// Pour l'instant, retour d'une erreur explicative
|
|
||||||
throw new VaultApiError(
|
|
||||||
'Recherche de fichiers non implémentée - contactez l\'administrateur',
|
|
||||||
'NOT_IMPLEMENTED'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie l'état de santé de l'API
|
|
||||||
*/
|
|
||||||
async health(): Promise<VaultHealth> {
|
|
||||||
const url = `${this.config.baseUrl}/health`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this._fetchApi(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur de santé API: ${response.status}`,
|
|
||||||
'HEALTH_CHECK_FAILED',
|
|
||||||
response.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json() as VaultHealth;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur lors du contrôle de santé: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les informations sur l'API
|
|
||||||
*/
|
|
||||||
async info(): Promise<VaultInfo> {
|
|
||||||
const url = `${this.config.baseUrl}/info`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this._fetchApi(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur info API: ${response.status}`,
|
|
||||||
'INFO_ERROR',
|
|
||||||
response.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json() as VaultInfo;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultApiError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur lors de la récupération des informations: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test de connectivité simple
|
|
||||||
*/
|
|
||||||
async ping(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.health();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Déchiffre le contenu avec les métadonnées utilisateur
|
|
||||||
*/
|
|
||||||
private decryptContent(encryptedData: Buffer): string {
|
|
||||||
try {
|
|
||||||
// Décoder le base64
|
|
||||||
const decoded = Buffer.from(encryptedData.toString(), 'base64');
|
|
||||||
|
|
||||||
// Nouveau format: nonce (12 bytes) + taille_métadonnées (4 bytes) + métadonnées + contenu chiffré
|
|
||||||
if (decoded.length < 16) {
|
|
||||||
throw new Error('Données chiffrées invalides - format incorrect');
|
|
||||||
}
|
|
||||||
|
|
||||||
// const nonce = decoded.subarray(0, 12); // Non utilisé pour l'instant
|
|
||||||
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'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Le déchiffrement nécessiterait la clé utilisateur
|
|
||||||
// qui est gérée côté serveur. Dans cette implémentation,
|
|
||||||
// nous simulons le déchiffrement pour la démonstration.
|
|
||||||
// En production, il faudrait implémenter un échange de clés sécurisé.
|
|
||||||
|
|
||||||
// Pour la démonstration, on retourne un message indiquant
|
|
||||||
// que le déchiffrement nécessite une clé côté serveur
|
|
||||||
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` +
|
|
||||||
`Taille chiffrée: ${ciphertext.length} bytes`;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof VaultAuthenticationError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new VaultDecryptionError(
|
|
||||||
`Erreur de déchiffrement: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Effectue une requête vers l'API avec authentification
|
|
||||||
*/
|
|
||||||
private async _fetchApi(url: string): Promise<Response> {
|
|
||||||
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',
|
|
||||||
...(this.config.verifySsl === false && { 'X-Skip-SSL-Verify': 'true' })
|
|
||||||
},
|
|
||||||
signal: controller.signal
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(url, mergedOptions as any) as any;
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
return response;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Timeout de la requête (${this.config.timeout}ms)`,
|
|
||||||
'TIMEOUT'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new VaultApiError(
|
|
||||||
`Erreur de connexion: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction utilitaire pour créer un client sécurisé
|
|
||||||
*/
|
|
||||||
export function createSecureVaultClient(baseUrl: string, userId: string): SecureVaultClient {
|
|
||||||
return new SecureVaultClient({
|
|
||||||
baseUrl,
|
|
||||||
userId,
|
|
||||||
verifySsl: false, // Pour les certificats auto-signés en développement
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction utilitaire pour créer un client sécurisé avec configuration complète
|
|
||||||
*/
|
|
||||||
export function createSecureVaultClientWithConfig(config: VaultConfig): SecureVaultClient {
|
|
||||||
return new SecureVaultClient(config);
|
|
||||||
}
|
|
173
start_api.sh
173
start_api.sh
@ -1,41 +1,156 @@
|
|||||||
#!/bin/bash
|
#!/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
|
set -e
|
||||||
# Port 6666, domaine vault.4nkweb.com
|
|
||||||
|
|
||||||
echo "🚀 Démarrage de l'API Vault 4NK..."
|
echo "🚀 Démarrage de l'API Vault sécurisée"
|
||||||
echo "📍 Domaine: vault.4nkweb.com"
|
echo "======================================"
|
||||||
echo "🔌 Port: 6666"
|
|
||||||
echo "🔐 Chiffrement: Quantique résistant (X25519 + ChaCha20-Poly1305)"
|
|
||||||
echo "📁 Répertoire de stockage: /home/debian/4NK_vault/storage"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Vérification des dépendances Python
|
# Vérification des prérequis
|
||||||
echo "🔍 Vérification des dépendances..."
|
echo "🔍 Vérification des prérequis..."
|
||||||
python3 -c "import flask, cryptography" 2>/dev/null
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "📦 Installation des dépendances..."
|
|
||||||
pip3 install -r requirements.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Vérification du répertoire de stockage
|
if ! command -v python3 &> /dev/null; then
|
||||||
if [ ! -d "/home/debian/4NK_vault/storage" ]; then
|
echo "❌ Python3 n'est pas installé"
|
||||||
echo "❌ Erreur: Le répertoire de stockage n'existe pas"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Vérification du fichier .env
|
if ! command -v pip3 &> /dev/null; then
|
||||||
if [ ! -f "/home/debian/4NK_vault/storage/dev/.env" ]; then
|
echo "❌ pip3 n'est pas installé"
|
||||||
echo "⚠️ Avertissement: Fichier .env non trouvé dans storage/dev/"
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Démarrage de l'API..."
|
echo "✅ Python3 et pip3 disponibles"
|
||||||
echo "🌐 URL: https://vault.4nkweb.com:6666"
|
|
||||||
echo "📋 Endpoints disponibles:"
|
# Création de l'environnement virtuel si nécessaire
|
||||||
echo " GET /<env>/<file> - Sert un fichier chiffré"
|
if [ ! -d "venv_secure_api" ]; then
|
||||||
echo " GET /health - Contrôle de santé"
|
echo "📦 Création de l'environnement virtuel..."
|
||||||
echo " GET /info - Informations API"
|
python3 -m venv venv_secure_api
|
||||||
echo ""
|
fi
|
||||||
|
|
||||||
|
# Activation de l'environnement virtuel
|
||||||
|
echo "🔧 Activation de l'environnement virtuel..."
|
||||||
|
source venv_secure_api/bin/activate
|
||||||
|
|
||||||
|
# Installation des dépendances
|
||||||
|
echo "📚 Installation des dépendances..."
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Vérification des fichiers requis
|
||||||
|
echo "🔍 Vérification des fichiers requis..."
|
||||||
|
|
||||||
|
if [ ! -f "api_server_secure.py" ]; then
|
||||||
|
echo "❌ Fichier api_server_secure.py non trouvé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "storage" ]; then
|
||||||
|
echo "❌ Répertoire storage non trouvé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "storage/dev/.env" ]; then
|
||||||
|
echo "❌ Fichier storage/dev/.env non trouvé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Tous les fichiers requis sont présents"
|
||||||
|
|
||||||
|
# Nettoyage des anciens certificats SSL
|
||||||
|
echo "🧹 Nettoyage des anciens certificats SSL..."
|
||||||
|
rm -f /tmp/vault.key /tmp/vault.crt
|
||||||
|
|
||||||
|
# Nettoyage de l'ancienne base de données de clés
|
||||||
|
echo "🗑️ Nettoyage de l'ancienne base de données de clés..."
|
||||||
|
rm -f /tmp/vault_keys.json
|
||||||
|
|
||||||
|
echo "✅ Nettoyage terminé"
|
||||||
|
|
||||||
# Démarrage de l'API
|
# Démarrage de l'API
|
||||||
python3 api_server.py
|
echo "🚀 Démarrage de l'API Vault sécurisée..."
|
||||||
|
echo " • URL: https://vault.4nkweb.com:6666"
|
||||||
|
echo " • Authentification: ID utilisateur obligatoire"
|
||||||
|
echo " • Chiffrement: Quantum-résistant (ChaCha20-Poly1305)"
|
||||||
|
echo " • Rotation des clés: Automatique (1h)"
|
||||||
|
echo " • HTTPS: Obligatoire"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Endpoints disponibles:"
|
||||||
|
echo " • GET /health - Contrôle de santé"
|
||||||
|
echo " • GET /info - Informations sur l'API"
|
||||||
|
echo " • GET /<env>/<file> - Récupération de fichier chiffré"
|
||||||
|
echo ""
|
||||||
|
echo "🔑 Authentification:"
|
||||||
|
echo " • Header requis: X-User-ID"
|
||||||
|
echo " • Format: 3-50 caractères alphanumériques, _ et -"
|
||||||
|
echo " • Exemple: X-User-ID: demo_user_001"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ ATTENTION: Cette API ne fonctionne qu'en HTTPS"
|
||||||
|
echo " Les tentatives d'accès HTTP seront rejetées"
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Rotation automatique des clés:"
|
||||||
|
echo " • Nouvelle clé générée toutes les heures"
|
||||||
|
echo " • Ancienne clé conservée pour compatibilité"
|
||||||
|
echo " • Base de données: /tmp/vault_keys.json"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test de connectivité avant démarrage
|
||||||
|
echo "🔍 Test de connectivité réseau..."
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
# Test si le port est déjà utilisé
|
||||||
|
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
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
echo "✅ Port 6666 disponible"
|
||||||
|
else
|
||||||
|
echo "⚠️ curl non disponible, impossible de tester la connectivité"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎯 Démarrage en cours..."
|
||||||
|
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
|
||||||
|
|
||||||
|
python3 api_server_secure.py &
|
||||||
|
API_PID=$!
|
||||||
|
|
||||||
|
# Attente du démarrage
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Test de santé après démarrage
|
||||||
|
echo "🏥 Test de santé de l'API..."
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
if curl -s -k -H "X-User-ID: test_user" https://127.0.0.1:6666/health > /dev/null 2>&1; then
|
||||||
|
echo "✅ API démarrée avec succès"
|
||||||
|
echo ""
|
||||||
|
echo "🧪 Test rapide:"
|
||||||
|
echo " curl -k -H 'X-User-ID: demo_user_001' https://127.0.0.1:6666/health"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "❌ L'API n'a pas démarré correctement"
|
||||||
|
echo " Vérifiez les logs ci-dessus"
|
||||||
|
kill $API_PID 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Impossible de tester automatiquement (curl non disponible)"
|
||||||
|
echo " Testez manuellement:"
|
||||||
|
echo " curl -k -H 'X-User-ID: demo_user_001' https://127.0.0.1:6666/health"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📊 Monitoring:"
|
||||||
|
echo " • Logs de l'API affichés ci-dessus"
|
||||||
|
echo " • Base de données des clés: /tmp/vault_keys.json"
|
||||||
|
echo " • Certificats SSL: /tmp/vault.key et /tmp/vault.crt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Attente de la fin du processus
|
||||||
|
wait $API_PID
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
"""
|
|
||||||
Script de démarrage de l'API Vault sécurisée
|
|
||||||
Avec authentification par clés utilisateur et rotation automatique
|
|
||||||
"""
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Démarrage de l'API Vault sécurisée"
|
|
||||||
echo "======================================"
|
|
||||||
|
|
||||||
# Vérification des prérequis
|
|
||||||
echo "🔍 Vérification des prérequis..."
|
|
||||||
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "❌ Python3 n'est pas installé"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v pip3 &> /dev/null; then
|
|
||||||
echo "❌ pip3 n'est pas installé"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Python3 et pip3 disponibles"
|
|
||||||
|
|
||||||
# Création de l'environnement virtuel si nécessaire
|
|
||||||
if [ ! -d "venv_secure_api" ]; then
|
|
||||||
echo "📦 Création de l'environnement virtuel..."
|
|
||||||
python3 -m venv venv_secure_api
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Activation de l'environnement virtuel
|
|
||||||
echo "🔧 Activation de l'environnement virtuel..."
|
|
||||||
source venv_secure_api/bin/activate
|
|
||||||
|
|
||||||
# Installation des dépendances
|
|
||||||
echo "📚 Installation des dépendances..."
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Vérification des fichiers requis
|
|
||||||
echo "🔍 Vérification des fichiers requis..."
|
|
||||||
|
|
||||||
if [ ! -f "api_server_secure.py" ]; then
|
|
||||||
echo "❌ Fichier api_server_secure.py non trouvé"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "storage" ]; then
|
|
||||||
echo "❌ Répertoire storage non trouvé"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "storage/dev/.env" ]; then
|
|
||||||
echo "❌ Fichier storage/dev/.env non trouvé"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tous les fichiers requis sont présents"
|
|
||||||
|
|
||||||
# Nettoyage des anciens certificats SSL
|
|
||||||
echo "🧹 Nettoyage des anciens certificats SSL..."
|
|
||||||
rm -f /tmp/vault.key /tmp/vault.crt
|
|
||||||
|
|
||||||
# Nettoyage de l'ancienne base de données de clés
|
|
||||||
echo "🗑️ Nettoyage de l'ancienne base de données de clés..."
|
|
||||||
rm -f /tmp/vault_keys.json
|
|
||||||
|
|
||||||
echo "✅ Nettoyage terminé"
|
|
||||||
|
|
||||||
# Démarrage de l'API
|
|
||||||
echo "🚀 Démarrage de l'API Vault sécurisée..."
|
|
||||||
echo " • URL: https://vault.4nkweb.com:6666"
|
|
||||||
echo " • Authentification: ID utilisateur obligatoire"
|
|
||||||
echo " • Chiffrement: Quantum-résistant (ChaCha20-Poly1305)"
|
|
||||||
echo " • Rotation des clés: Automatique (1h)"
|
|
||||||
echo " • HTTPS: Obligatoire"
|
|
||||||
echo ""
|
|
||||||
echo "📋 Endpoints disponibles:"
|
|
||||||
echo " • GET /health - Contrôle de santé"
|
|
||||||
echo " • GET /info - Informations sur l'API"
|
|
||||||
echo " • GET /<env>/<file> - Récupération de fichier chiffré"
|
|
||||||
echo ""
|
|
||||||
echo "🔑 Authentification:"
|
|
||||||
echo " • Header requis: X-User-ID"
|
|
||||||
echo " • Format: 3-50 caractères alphanumériques, _ et -"
|
|
||||||
echo " • Exemple: X-User-ID: demo_user_001"
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ ATTENTION: Cette API ne fonctionne qu'en HTTPS"
|
|
||||||
echo " Les tentatives d'accès HTTP seront rejetées"
|
|
||||||
echo ""
|
|
||||||
echo "🔄 Rotation automatique des clés:"
|
|
||||||
echo " • Nouvelle clé générée toutes les heures"
|
|
||||||
echo " • Ancienne clé conservée pour compatibilité"
|
|
||||||
echo " • Base de données: /tmp/vault_keys.json"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test de connectivité avant démarrage
|
|
||||||
echo "🔍 Test de connectivité réseau..."
|
|
||||||
if command -v curl &> /dev/null; then
|
|
||||||
# Test si le port est déjà utilisé
|
|
||||||
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
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
echo "✅ Port 6666 disponible"
|
|
||||||
else
|
|
||||||
echo "⚠️ curl non disponible, impossible de tester la connectivité"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎯 Démarrage en cours..."
|
|
||||||
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
|
|
||||||
|
|
||||||
python3 api_server_secure.py &
|
|
||||||
API_PID=$!
|
|
||||||
|
|
||||||
# Attente du démarrage
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
# Test de santé après démarrage
|
|
||||||
echo "🏥 Test de santé de l'API..."
|
|
||||||
if command -v curl &> /dev/null; then
|
|
||||||
if curl -s -k -H "X-User-ID: test_user" https://127.0.0.1:6666/health > /dev/null 2>&1; then
|
|
||||||
echo "✅ API démarrée avec succès"
|
|
||||||
echo ""
|
|
||||||
echo "🧪 Test rapide:"
|
|
||||||
echo " curl -k -H 'X-User-ID: demo_user_001' https://127.0.0.1:6666/health"
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
echo "❌ L'API n'a pas démarré correctement"
|
|
||||||
echo " Vérifiez les logs ci-dessus"
|
|
||||||
kill $API_PID 2>/dev/null
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ Impossible de tester automatiquement (curl non disponible)"
|
|
||||||
echo " Testez manuellement:"
|
|
||||||
echo " curl -k -H 'X-User-ID: demo_user_001' https://127.0.0.1:6666/health"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📊 Monitoring:"
|
|
||||||
echo " • Logs de l'API affichés ci-dessus"
|
|
||||||
echo " • Base de données des clés: /tmp/vault_keys.json"
|
|
||||||
echo " • Certificats SSL: /tmp/vault.key et /tmp/vault.crt"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Attente de la fin du processus
|
|
||||||
wait $API_PID
|
|
331
test_api.py
Executable file → Normal file
331
test_api.py
Executable file → Normal file
@ -1,161 +1,272 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Script de test pour l'API Vault
|
Test de l'API Vault sécurisée avec authentification par clés utilisateur
|
||||||
Teste les endpoints et le chiffrement quantique résistant
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import ssl
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
API_BASE_URL = "https://vault.4nkweb.com:6666"
|
BASE_URL = 'https://127.0.0.1:6666'
|
||||||
VERIFY_SSL = False # Désactivé pour les certificats auto-signés
|
USER_ID = 'demo_user_001' # ID utilisateur de test
|
||||||
|
VERIFY_SSL = False # Certificats auto-signés
|
||||||
|
|
||||||
|
def test_health():
|
||||||
|
"""Test de l'endpoint de santé"""
|
||||||
|
print("🔍 Test de santé de l'API...")
|
||||||
|
|
||||||
def test_health_endpoint():
|
|
||||||
"""Test du endpoint de santé"""
|
|
||||||
print("🔍 Test du endpoint /health...")
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"{API_BASE_URL}/health", verify=VERIFY_SSL)
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/health",
|
||||||
|
verify=VERIFY_SSL,
|
||||||
|
headers={'X-User-ID': USER_ID}
|
||||||
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
health_data = response.json()
|
||||||
print(f"✅ Santé: {data['status']}")
|
print(f"✅ API en bonne santé")
|
||||||
print(f"🔐 Chiffrement: {data['encryption']}")
|
print(f" Service: {health_data.get('service')}")
|
||||||
print(f"🔧 Algorithme: {data['algorithm']}")
|
print(f" Chiffrement: {health_data.get('encryption')}")
|
||||||
|
print(f" Algorithme: {health_data.get('algorithm')}")
|
||||||
|
print(f" Authentification: {health_data.get('authentication')}")
|
||||||
|
print(f" Rotation des clés: {health_data.get('key_rotation')}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"❌ Erreur santé: {response.status_code}")
|
print(f"❌ Erreur de santé: {response.status_code}")
|
||||||
|
print(f" Réponse: {response.text}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur connexion santé: {e}")
|
print(f"❌ Erreur de connexion: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_info_endpoint():
|
def test_info():
|
||||||
"""Test du endpoint d'information"""
|
"""Test de l'endpoint d'informations"""
|
||||||
print("\n🔍 Test du endpoint /info...")
|
print("\n📋 Test des informations API...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"{API_BASE_URL}/info", verify=VERIFY_SSL)
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/info",
|
||||||
|
verify=VERIFY_SSL,
|
||||||
|
headers={'X-User-ID': USER_ID}
|
||||||
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
info_data = response.json()
|
||||||
print(f"✅ API: {data['name']} v{data['version']}")
|
print(f"✅ Informations récupérées")
|
||||||
print(f"🌐 Domaine: {data['domain']}")
|
print(f" Nom: {info_data.get('name')}")
|
||||||
print(f"🔌 Port: {data['port']}")
|
print(f" Version: {info_data.get('version')}")
|
||||||
print(f"📡 Protocole: {data['protocol']}")
|
print(f" Domaine: {info_data.get('domain')}")
|
||||||
print("📋 Endpoints:")
|
print(f" Protocole: {info_data.get('protocol')}")
|
||||||
for endpoint, description in data['endpoints'].items():
|
print(f" Authentification: {info_data.get('authentication')}")
|
||||||
print(f" {endpoint}: {description}")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"❌ Erreur info: {response.status_code}")
|
print(f"❌ Erreur d'informations: {response.status_code}")
|
||||||
|
print(f" Réponse: {response.text}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur connexion info: {e}")
|
print(f"❌ Erreur de connexion: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_file_endpoint():
|
def test_file_access():
|
||||||
"""Test du endpoint de fichier"""
|
"""Test d'accès à un fichier"""
|
||||||
print("\n🔍 Test du endpoint de fichier...")
|
print("\n📁 Test d'accès au fichier...")
|
||||||
|
|
||||||
# Test avec un fichier existant
|
try:
|
||||||
test_files = [
|
response = requests.get(
|
||||||
"dev/bitcoin/bitcoin.conf",
|
f"{BASE_URL}/dev/bitcoin/bitcoin.conf",
|
||||||
"dev/tor/torrc",
|
verify=VERIFY_SSL,
|
||||||
"dev/sdk_relay/sdk_relay.conf"
|
headers={'X-User-ID': USER_ID}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"✅ Fichier récupéré avec succès")
|
||||||
|
print(f" Taille: {len(response.content)} bytes")
|
||||||
|
print(f" Type de contenu: {response.headers.get('Content-Type')}")
|
||||||
|
print(f" Type de chiffrement: {response.headers.get('X-Encryption-Type')}")
|
||||||
|
print(f" Algorithme: {response.headers.get('X-Algorithm')}")
|
||||||
|
print(f" ID utilisateur: {response.headers.get('X-User-ID')}")
|
||||||
|
print(f" Rotation des clés: {response.headers.get('X-Key-Rotation')}")
|
||||||
|
|
||||||
|
# Tentative de décodage des métadonnées
|
||||||
|
try:
|
||||||
|
decoded = base64.b64decode(response.content)
|
||||||
|
if len(decoded) >= 16:
|
||||||
|
nonce = decoded[:12]
|
||||||
|
metadata_size = int.from_bytes(decoded[12:16], 'big')
|
||||||
|
metadata_json = decoded[16:16+metadata_size]
|
||||||
|
metadata = json.loads(metadata_json.decode('utf-8'))
|
||||||
|
|
||||||
|
print(f"\n📊 Métadonnées de chiffrement:")
|
||||||
|
print(f" Utilisateur: {metadata.get('user_id')}")
|
||||||
|
print(f" Version de clé: {metadata.get('key_version')}")
|
||||||
|
print(f" Timestamp: {metadata.get('timestamp')}")
|
||||||
|
print(f" Algorithme: {metadata.get('algorithm')}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Impossible de décoder les métadonnées: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Erreur d'accès au fichier: {response.status_code}")
|
||||||
|
print(f" Réponse: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erreur de connexion: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_authentication():
|
||||||
|
"""Test d'authentification avec différents ID utilisateur"""
|
||||||
|
print("\n🔐 Test d'authentification...")
|
||||||
|
|
||||||
|
test_users = [
|
||||||
|
('valid_user_001', True),
|
||||||
|
('invalid@user', False),
|
||||||
|
('ab', False), # Trop court
|
||||||
|
('a' * 51, False), # Trop long
|
||||||
|
('', False), # Vide
|
||||||
]
|
]
|
||||||
|
|
||||||
for file_path in test_files:
|
for user_id, should_succeed in test_users:
|
||||||
print(f"\n📁 Test du fichier: {file_path}")
|
print(f"\n Test utilisateur: '{user_id}' (attendu: {'succès' if should_succeed else 'échec'})")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"{API_BASE_URL}/{file_path}", verify=VERIFY_SSL)
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/health",
|
||||||
|
verify=VERIFY_SSL,
|
||||||
|
headers={'X-User-ID': user_id} if user_id else {}
|
||||||
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
success = response.status_code == 200
|
||||||
print(f"✅ Fichier servi avec succès")
|
if success == should_succeed:
|
||||||
print(f"📦 Taille: {len(response.content)} bytes")
|
print(f" ✅ Résultat attendu")
|
||||||
print(f"🔐 Type chiffrement: {response.headers.get('X-Encryption-Type', 'N/A')}")
|
|
||||||
print(f"🔧 Algorithme: {response.headers.get('X-Algorithm', 'N/A')}")
|
|
||||||
|
|
||||||
# Affichage d'un échantillon du contenu chiffré (base64)
|
|
||||||
sample = response.content[:100]
|
|
||||||
print(f"📄 Échantillon chiffré: {sample.decode('utf-8', errors='ignore')}...")
|
|
||||||
|
|
||||||
elif response.status_code == 404:
|
|
||||||
print(f"⚠️ Fichier non trouvé: {file_path}")
|
|
||||||
else:
|
else:
|
||||||
print(f"❌ Erreur fichier: {response.status_code}")
|
print(f" ❌ Résultat inattendu: {response.status_code}")
|
||||||
if response.headers.get('content-type') == 'application/json':
|
|
||||||
error_data = response.json()
|
|
||||||
print(f" Détail: {error_data.get('error', 'N/A')}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur connexion fichier: {e}")
|
if not should_succeed:
|
||||||
|
print(f" ✅ Erreur attendue: {e}")
|
||||||
|
else:
|
||||||
|
print(f" ❌ Erreur inattendue: {e}")
|
||||||
|
|
||||||
def test_variable_processing():
|
def test_https_requirement():
|
||||||
"""Test du traitement des variables d'environnement"""
|
"""Test de l'obligation HTTPS"""
|
||||||
print("\n🔍 Test du traitement des variables...")
|
print("\n🔒 Test de l'obligation HTTPS...")
|
||||||
|
|
||||||
# Création d'un fichier de test avec des variables
|
# Tentative d'accès HTTP (devrait échouer)
|
||||||
test_content = """
|
http_url = BASE_URL.replace('https://', 'http://')
|
||||||
# Test de variables composites
|
|
||||||
API_URL=${ROOT_URL}${URL_ROUTE_LECOFFRE_FRONT}
|
|
||||||
BITCOIN_RPC=${BITCOIN_RPC_URL}
|
|
||||||
DOMAIN=${DOMAIN}
|
|
||||||
HOST=${HOST}
|
|
||||||
COMPOSITE=${ROOT_DIR_LOGS}/bitcoin
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_file_path = Path("/home/debian/4NK_vault/storage/dev/test_variables.conf")
|
|
||||||
try:
|
try:
|
||||||
# Création du fichier de test
|
response = requests.get(
|
||||||
with open(test_file_path, 'w') as f:
|
f"{http_url}/health",
|
||||||
f.write(test_content)
|
verify=VERIFY_SSL,
|
||||||
|
headers={'X-User-ID': USER_ID},
|
||||||
print(f"📝 Fichier de test créé: {test_file_path}")
|
timeout=5
|
||||||
|
)
|
||||||
# Test de l'API
|
print(f"❌ HTTP autorisé (ne devrait pas l'être): {response.status_code}")
|
||||||
response = requests.get(f"{API_BASE_URL}/dev/test_variables.conf", verify=VERIFY_SSL)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
print("✅ Variables traitées avec succès")
|
|
||||||
print(f"📦 Contenu chiffré reçu: {len(response.content)} bytes")
|
|
||||||
else:
|
|
||||||
print(f"❌ Erreur traitement variables: {response.status_code}")
|
|
||||||
|
|
||||||
# Nettoyage
|
|
||||||
test_file_path.unlink()
|
|
||||||
print("🧹 Fichier de test supprimé")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur test variables: {e}")
|
print(f"✅ HTTP refusé comme attendu: {e}")
|
||||||
if test_file_path.exists():
|
|
||||||
test_file_path.unlink()
|
def test_key_rotation():
|
||||||
|
"""Test de la rotation des clés"""
|
||||||
|
print("\n🔄 Test de la rotation des clés...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Premier accès
|
||||||
|
response1 = requests.get(
|
||||||
|
f"{BASE_URL}/dev/bitcoin/bitcoin.conf",
|
||||||
|
verify=VERIFY_SSL,
|
||||||
|
headers={'X-User-ID': USER_ID}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response1.status_code == 200:
|
||||||
|
# Attendre un peu et refaire un accès
|
||||||
|
import time
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
response2 = requests.get(
|
||||||
|
f"{BASE_URL}/dev/bitcoin/bitcoin.conf",
|
||||||
|
verify=VERIFY_SSL,
|
||||||
|
headers={'X-User-ID': USER_ID}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response2.status_code == 200:
|
||||||
|
# Comparer les réponses (les clés peuvent avoir changé)
|
||||||
|
print(f"✅ Accès multiples réussis")
|
||||||
|
print(f" Premier accès: {len(response1.content)} bytes")
|
||||||
|
print(f" Deuxième accès: {len(response2.content)} bytes")
|
||||||
|
|
||||||
|
# Les réponses peuvent être différentes à cause de la rotation des clés
|
||||||
|
if response1.content != response2.content:
|
||||||
|
print(f" ✅ Contenu différent détecté (rotation des clés possible)")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Contenu identique (rotation pas encore déclenchée)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Deuxième accès échoué: {response2.status_code}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Premier accès échoué: {response1.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erreur lors du test de rotation: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Fonction principale de test"""
|
"""Fonction principale de test"""
|
||||||
print("🚀 Test de l'API Vault 4NK")
|
print("🚀 Test de l'API Vault sécurisée")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"URL de base: {BASE_URL}")
|
||||||
|
print(f"ID utilisateur: {USER_ID}")
|
||||||
|
print(f"Vérification SSL: {VERIFY_SSL}")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
# Désactivation des avertissements SSL pour les certificats auto-signés
|
|
||||||
import urllib3
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
health_ok = test_health_endpoint()
|
tests = [
|
||||||
info_ok = test_info_endpoint()
|
("Santé de l'API", test_health),
|
||||||
test_file_endpoint()
|
("Informations API", test_info),
|
||||||
test_variable_processing()
|
("Accès aux fichiers", test_file_access),
|
||||||
|
("Authentification", test_authentication),
|
||||||
|
("Obligation HTTPS", test_https_requirement),
|
||||||
|
("Rotation des clés", test_key_rotation),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for test_name, test_func in tests:
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results.append((test_name, result))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erreur dans le test '{test_name}': {e}")
|
||||||
|
results.append((test_name, False))
|
||||||
|
|
||||||
|
# Résumé
|
||||||
print("\n" + "=" * 50)
|
print("\n" + "=" * 50)
|
||||||
print("📊 Résumé des tests:")
|
print("📊 RÉSUMÉ DES TESTS")
|
||||||
print(f" Santé: {'✅' if health_ok else '❌'}")
|
print("=" * 50)
|
||||||
print(f" Info: {'✅' if info_ok else '❌'}")
|
|
||||||
print(" Fichiers: Voir détails ci-dessus")
|
|
||||||
print(" Variables: Voir détails ci-dessus")
|
|
||||||
|
|
||||||
if health_ok and info_ok:
|
passed = 0
|
||||||
print("\n🎉 L'API fonctionne correctement!")
|
total = len(results)
|
||||||
|
|
||||||
|
for test_name, result in results:
|
||||||
|
status = "✅ PASSÉ" if result else "❌ ÉCHOUÉ"
|
||||||
|
print(f"{status} - {test_name}")
|
||||||
|
if result:
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print(f"\nRésultat global: {passed}/{total} tests passés")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 Tous les tests sont passés avec succès!")
|
||||||
else:
|
else:
|
||||||
print("\n⚠️ L'API a des problèmes")
|
print("⚠️ Certains tests ont échoué. Vérifiez la configuration.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
@ -1,272 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test de l'API Vault sécurisée avec authentification par clés utilisateur
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
BASE_URL = 'https://127.0.0.1:6666'
|
|
||||||
USER_ID = 'demo_user_001' # ID utilisateur de test
|
|
||||||
VERIFY_SSL = False # Certificats auto-signés
|
|
||||||
|
|
||||||
def test_health():
|
|
||||||
"""Test de l'endpoint de santé"""
|
|
||||||
print("🔍 Test de santé de l'API...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{BASE_URL}/health",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': USER_ID}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
health_data = response.json()
|
|
||||||
print(f"✅ API en bonne santé")
|
|
||||||
print(f" Service: {health_data.get('service')}")
|
|
||||||
print(f" Chiffrement: {health_data.get('encryption')}")
|
|
||||||
print(f" Algorithme: {health_data.get('algorithm')}")
|
|
||||||
print(f" Authentification: {health_data.get('authentication')}")
|
|
||||||
print(f" Rotation des clés: {health_data.get('key_rotation')}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"❌ Erreur de santé: {response.status_code}")
|
|
||||||
print(f" Réponse: {response.text}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Erreur de connexion: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_info():
|
|
||||||
"""Test de l'endpoint d'informations"""
|
|
||||||
print("\n📋 Test des informations API...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{BASE_URL}/info",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': USER_ID}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
info_data = response.json()
|
|
||||||
print(f"✅ Informations récupérées")
|
|
||||||
print(f" Nom: {info_data.get('name')}")
|
|
||||||
print(f" Version: {info_data.get('version')}")
|
|
||||||
print(f" Domaine: {info_data.get('domain')}")
|
|
||||||
print(f" Protocole: {info_data.get('protocol')}")
|
|
||||||
print(f" Authentification: {info_data.get('authentication')}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"❌ Erreur d'informations: {response.status_code}")
|
|
||||||
print(f" Réponse: {response.text}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Erreur de connexion: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_file_access():
|
|
||||||
"""Test d'accès à un fichier"""
|
|
||||||
print("\n📁 Test d'accès au fichier...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{BASE_URL}/dev/bitcoin/bitcoin.conf",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': USER_ID}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
print(f"✅ Fichier récupéré avec succès")
|
|
||||||
print(f" Taille: {len(response.content)} bytes")
|
|
||||||
print(f" Type de contenu: {response.headers.get('Content-Type')}")
|
|
||||||
print(f" Type de chiffrement: {response.headers.get('X-Encryption-Type')}")
|
|
||||||
print(f" Algorithme: {response.headers.get('X-Algorithm')}")
|
|
||||||
print(f" ID utilisateur: {response.headers.get('X-User-ID')}")
|
|
||||||
print(f" Rotation des clés: {response.headers.get('X-Key-Rotation')}")
|
|
||||||
|
|
||||||
# Tentative de décodage des métadonnées
|
|
||||||
try:
|
|
||||||
decoded = base64.b64decode(response.content)
|
|
||||||
if len(decoded) >= 16:
|
|
||||||
nonce = decoded[:12]
|
|
||||||
metadata_size = int.from_bytes(decoded[12:16], 'big')
|
|
||||||
metadata_json = decoded[16:16+metadata_size]
|
|
||||||
metadata = json.loads(metadata_json.decode('utf-8'))
|
|
||||||
|
|
||||||
print(f"\n📊 Métadonnées de chiffrement:")
|
|
||||||
print(f" Utilisateur: {metadata.get('user_id')}")
|
|
||||||
print(f" Version de clé: {metadata.get('key_version')}")
|
|
||||||
print(f" Timestamp: {metadata.get('timestamp')}")
|
|
||||||
print(f" Algorithme: {metadata.get('algorithm')}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Impossible de décoder les métadonnées: {e}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"❌ Erreur d'accès au fichier: {response.status_code}")
|
|
||||||
print(f" Réponse: {response.text}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Erreur de connexion: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_authentication():
|
|
||||||
"""Test d'authentification avec différents ID utilisateur"""
|
|
||||||
print("\n🔐 Test d'authentification...")
|
|
||||||
|
|
||||||
test_users = [
|
|
||||||
('valid_user_001', True),
|
|
||||||
('invalid@user', False),
|
|
||||||
('ab', False), # Trop court
|
|
||||||
('a' * 51, False), # Trop long
|
|
||||||
('', False), # Vide
|
|
||||||
]
|
|
||||||
|
|
||||||
for user_id, should_succeed in test_users:
|
|
||||||
print(f"\n Test utilisateur: '{user_id}' (attendu: {'succès' if should_succeed else 'échec'})")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{BASE_URL}/health",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': user_id} if user_id else {}
|
|
||||||
)
|
|
||||||
|
|
||||||
success = response.status_code == 200
|
|
||||||
if success == should_succeed:
|
|
||||||
print(f" ✅ Résultat attendu")
|
|
||||||
else:
|
|
||||||
print(f" ❌ Résultat inattendu: {response.status_code}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if not should_succeed:
|
|
||||||
print(f" ✅ Erreur attendue: {e}")
|
|
||||||
else:
|
|
||||||
print(f" ❌ Erreur inattendue: {e}")
|
|
||||||
|
|
||||||
def test_https_requirement():
|
|
||||||
"""Test de l'obligation HTTPS"""
|
|
||||||
print("\n🔒 Test de l'obligation HTTPS...")
|
|
||||||
|
|
||||||
# Tentative d'accès HTTP (devrait échouer)
|
|
||||||
http_url = BASE_URL.replace('https://', 'http://')
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{http_url}/health",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': USER_ID},
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
print(f"❌ HTTP autorisé (ne devrait pas l'être): {response.status_code}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✅ HTTP refusé comme attendu: {e}")
|
|
||||||
|
|
||||||
def test_key_rotation():
|
|
||||||
"""Test de la rotation des clés"""
|
|
||||||
print("\n🔄 Test de la rotation des clés...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Premier accès
|
|
||||||
response1 = requests.get(
|
|
||||||
f"{BASE_URL}/dev/bitcoin/bitcoin.conf",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': USER_ID}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response1.status_code == 200:
|
|
||||||
# Attendre un peu et refaire un accès
|
|
||||||
import time
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
response2 = requests.get(
|
|
||||||
f"{BASE_URL}/dev/bitcoin/bitcoin.conf",
|
|
||||||
verify=VERIFY_SSL,
|
|
||||||
headers={'X-User-ID': USER_ID}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response2.status_code == 200:
|
|
||||||
# Comparer les réponses (les clés peuvent avoir changé)
|
|
||||||
print(f"✅ Accès multiples réussis")
|
|
||||||
print(f" Premier accès: {len(response1.content)} bytes")
|
|
||||||
print(f" Deuxième accès: {len(response2.content)} bytes")
|
|
||||||
|
|
||||||
# Les réponses peuvent être différentes à cause de la rotation des clés
|
|
||||||
if response1.content != response2.content:
|
|
||||||
print(f" ✅ Contenu différent détecté (rotation des clés possible)")
|
|
||||||
else:
|
|
||||||
print(f" ⚠️ Contenu identique (rotation pas encore déclenchée)")
|
|
||||||
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"❌ Deuxième accès échoué: {response2.status_code}")
|
|
||||||
else:
|
|
||||||
print(f"❌ Premier accès échoué: {response1.status_code}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Erreur lors du test de rotation: {e}")
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Fonction principale de test"""
|
|
||||||
print("🚀 Test de l'API Vault sécurisée")
|
|
||||||
print("=" * 50)
|
|
||||||
print(f"URL de base: {BASE_URL}")
|
|
||||||
print(f"ID utilisateur: {USER_ID}")
|
|
||||||
print(f"Vérification SSL: {VERIFY_SSL}")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
tests = [
|
|
||||||
("Santé de l'API", test_health),
|
|
||||||
("Informations API", test_info),
|
|
||||||
("Accès aux fichiers", test_file_access),
|
|
||||||
("Authentification", test_authentication),
|
|
||||||
("Obligation HTTPS", test_https_requirement),
|
|
||||||
("Rotation des clés", test_key_rotation),
|
|
||||||
]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for test_name, test_func in tests:
|
|
||||||
try:
|
|
||||||
result = test_func()
|
|
||||||
results.append((test_name, result))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Erreur dans le test '{test_name}': {e}")
|
|
||||||
results.append((test_name, False))
|
|
||||||
|
|
||||||
# Résumé
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("📊 RÉSUMÉ DES TESTS")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
passed = 0
|
|
||||||
total = len(results)
|
|
||||||
|
|
||||||
for test_name, result in results:
|
|
||||||
status = "✅ PASSÉ" if result else "❌ ÉCHOUÉ"
|
|
||||||
print(f"{status} - {test_name}")
|
|
||||||
if result:
|
|
||||||
passed += 1
|
|
||||||
|
|
||||||
print(f"\nRésultat global: {passed}/{total} tests passés")
|
|
||||||
|
|
||||||
if passed == total:
|
|
||||||
print("🎉 Tous les tests sont passés avec succès!")
|
|
||||||
else:
|
|
||||||
print("⚠️ Certains tests ont échoué. Vérifiez la configuration.")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
Loading…
x
Reference in New Issue
Block a user