espilon-source/tools/C3PO/web/server.py
Eun0us 8b6c1cd53d ε - ChaCha20-Poly1305 AEAD + HKDF crypto upgrade + C3PO rewrite + docs
Crypto:
- Replace broken ChaCha20 (static nonce) with ChaCha20-Poly1305 AEAD
- HKDF-SHA256 key derivation from per-device factory NVS master keys
- Random 12-byte nonce per message (ESP32 hardware RNG)
- crypto_init/encrypt/decrypt API with mbedtls legacy (ESP-IDF v5.3.2)
- Custom partition table with factory NVS (fctry at 0x10000)

Firmware:
- crypto.c full rewrite, messages.c device_id prefix + AEAD encrypt
- crypto_init() at boot with esp_restart() on failure
- Fix command_t initializations across all modules (sub/help fields)
- Clean CMakeLists dependencies for ESP-IDF v5.3.2

C3PO (C2):
- Rename tools/c2 + tools/c3po -> tools/C3PO
- Per-device CryptoContext with HKDF key derivation
- KeyStore (keys.json) for master key management
- Transport parses device_id:base64(...) wire format

Tools:
- New tools/provisioning/provision.py for factory NVS key generation
- Updated flasher with mbedtls config for v5.3.2

Docs:
- Update all READMEs for new crypto, C3PO paths, provisioning
- Update roadmap, architecture diagrams, security sections
- Update CONTRIBUTING.md project structure
2026-02-10 21:28:45 +01:00

198 lines
6.6 KiB
Python

"""Unified Flask web server for ESPILON C2 dashboard."""
import os
import sys
import logging
import threading
from typing import Optional
from flask import Flask
from werkzeug.serving import make_server
from .mlat import MlatEngine
from .auth import create_auth_decorators
from .routes import (
create_pages_blueprint,
create_devices_blueprint,
create_cameras_blueprint,
create_mlat_blueprint,
create_stats_blueprint,
)
# Make hp_dashboard importable (lives in espilon-honey-pot/tools/)
_HP_TOOLS_DIR = os.environ.get("HP_DASHBOARD_PATH", os.path.normpath(os.path.join(
os.path.dirname(__file__), "..", "..", "..", "..", "espilon-honey-pot", "tools"
)))
if os.path.isdir(_HP_TOOLS_DIR) and _HP_TOOLS_DIR not in sys.path:
sys.path.insert(0, _HP_TOOLS_DIR)
# Disable Flask/Werkzeug request logging
logging.getLogger('werkzeug').setLevel(logging.ERROR)
class UnifiedWebServer:
"""
Unified Flask-based web server for ESPILON C2.
Provides:
- Dashboard: View connected ESP32 devices
- Cameras: View live camera streams with recording
- MLAT: Visualize multilateration positioning
"""
def __init__(self,
host: str = "0.0.0.0",
port: int = 8000,
image_dir: str = "static/streams",
username: str = "admin",
password: str = "admin",
secret_key: str = "change_this_for_prod",
multilat_token: str = "multilat_secret_token",
device_registry=None,
mlat_engine: Optional[MlatEngine] = None,
camera_receiver=None,
hp_store=None,
hp_commander=None,
hp_alerts=None,
hp_geo=None):
"""
Initialize the unified web server.
Args:
host: Host to bind the server
port: Port for the web server
image_dir: Directory containing camera frame images
username: Login username
password: Login password
secret_key: Flask session secret key
multilat_token: Bearer token for MLAT API
device_registry: DeviceRegistry instance for device listing
mlat_engine: MlatEngine instance (created if None)
camera_receiver: UDPReceiver instance for camera control
hp_store: HpStore instance for honeypot event storage
hp_commander: HpCommander instance for honeypot command dispatch
hp_alerts: HpAlertEngine instance for honeypot alert rules
hp_geo: HpGeoLookup instance for geo-IP enrichment
"""
self.host = host
self.port = port
self.image_dir = image_dir
self.username = username
self.password = password
self.secret_key = secret_key
self.multilat_token = multilat_token
self.device_registry = device_registry
self.mlat = mlat_engine or MlatEngine()
self.camera_receiver = camera_receiver
self.hp_store = hp_store
self.hp_commander = hp_commander
self.hp_alerts = hp_alerts
self.hp_geo = hp_geo
# C2 root directory
self.c2_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Ensure image directory exists
full_image_dir = os.path.join(self.c2_root, self.image_dir)
os.makedirs(full_image_dir, exist_ok=True)
self._app = self._create_app()
self._server = None
self._thread = None
def set_camera_receiver(self, receiver):
"""Set the camera receiver after initialization."""
self.camera_receiver = receiver
@property
def is_running(self) -> bool:
return self._thread is not None and self._thread.is_alive()
def _create_app(self) -> Flask:
"""Create and configure the Flask application."""
template_dir = os.path.join(self.c2_root, "templates")
static_dir = os.path.join(self.c2_root, "static")
app = Flask(__name__,
template_folder=template_dir,
static_folder=static_dir)
app.secret_key = self.secret_key
# Create auth decorators
require_login, require_api_auth = create_auth_decorators(
lambda: self.multilat_token
)
# Shared config for blueprints
base_config = {
"c2_root": self.c2_root,
"image_dir": self.image_dir,
"require_login": require_login,
"require_api_auth": require_api_auth,
}
# Register blueprints
app.register_blueprint(create_pages_blueprint({
**base_config,
"username": self.username,
"password": self.password,
}))
app.register_blueprint(create_devices_blueprint({
**base_config,
"get_device_registry": lambda: self.device_registry,
}))
app.register_blueprint(create_cameras_blueprint({
**base_config,
"get_camera_receiver": lambda: self.camera_receiver,
}))
app.register_blueprint(create_mlat_blueprint({
**base_config,
"get_mlat_engine": lambda: self.mlat,
}))
app.register_blueprint(create_stats_blueprint({
**base_config,
"get_device_registry": lambda: self.device_registry,
"get_mlat_engine": lambda: self.mlat,
}))
# Honeypot dashboard (optional — only if hp_store is provided)
if self.hp_store and self.hp_commander:
try:
from hp_dashboard import create_hp_blueprint
app.register_blueprint(create_hp_blueprint({
**base_config,
"hp_store": self.hp_store,
"hp_commander": self.hp_commander,
"hp_alerts": self.hp_alerts,
"hp_geo": self.hp_geo,
}))
except ImportError:
pass # hp_dashboard not available
return app
def start(self) -> bool:
"""Start the web server in a background thread."""
if self.is_running:
return False
self._server = make_server(self.host, self.port, self._app, threaded=True)
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
self._thread.start()
return True
def stop(self):
"""Stop the web server."""
if self._server:
self._server.shutdown()
self._server = None
self._thread = None
def get_url(self) -> str:
"""Get the server URL."""
return f"http://{self.host}:{self.port}"