espilon-source/tools/C3PO/c3po.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

217 lines
7.4 KiB
Python

#!/usr/bin/env python3
import socket
import threading
import re
import sys
import time
import argparse
from core.registry import DeviceRegistry
from core.keystore import KeyStore
from core.transport import Transport
from log.manager import LogManager
from cli.cli import CLI
from commands.registry import CommandRegistry
from commands.reboot import RebootCommand
from core.groups import GroupRegistry
from utils.constant import HOST, PORT
from utils.display import Display
# New wire format: device_id:BASE64 + '\n'
FRAME_RE = re.compile(br'^[A-Za-z0-9_-]+:[A-Za-z0-9+/=]+$')
RX_BUF_SIZE = 4096
MAX_BUFFER_SIZE = 1024 * 1024 # 1MB max buffer to prevent memory exhaustion
DEVICE_TIMEOUT_SECONDS = 300 # Devices are considered inactive after 5 minutes without a heartbeat
HEARTBEAT_CHECK_INTERVAL = 10 # Check every 10 seconds
# ============================================================
# Client handler
# ============================================================
def client_thread(sock: socket.socket, addr, transport: Transport, registry: DeviceRegistry):
Display.system_message(f"Client connected from {addr}")
buffer = b""
device_id = None # To track which device disconnected
try:
while True:
data = sock.recv(RX_BUF_SIZE)
if not data:
break
buffer += data
# Prevent memory exhaustion from malicious clients
if len(buffer) > MAX_BUFFER_SIZE:
Display.error(f"Buffer overflow from {addr}, dropping connection")
break
# Strict framing by '\n' (ESP behavior)
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
line = line.strip()
if not line:
continue
# Validate frame format: device_id:base64
if not FRAME_RE.match(line):
Display.system_message(f"Ignoring invalid frame from {addr}")
continue
try:
# Pass registry to handle_incoming to update device status
transport.handle_incoming(sock, addr, line)
# After successful handling, try to get device_id
# This is a simplification; a more robust solution might pass device_id from transport
# For now, we assume the first message will register the device
if not device_id and registry.get_device_by_sock(sock):
device_id = registry.get_device_by_sock(sock).id
except Exception as e:
Display.error(f"Transport error from {addr}: {e}")
except Exception as e:
Display.error(f"Client error from {addr}: {e}")
finally:
try:
sock.close()
except Exception:
pass
if device_id:
Display.device_event(device_id, "Disconnected")
registry.remove(device_id) # Remove device from registry on disconnect
else:
Display.system_message(f"Client disconnected from {addr}")
# ============================================================
# Main server
# ============================================================
def main():
# Parse arguments
parser = argparse.ArgumentParser(description="C3PO - ESPILON C2 Framework")
parser.add_argument("--tui", action="store_true", help="Launch with TUI interface")
args = parser.parse_args()
header = """
$$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\\
$$ __$$\\ $$ ___$$\\ $$ __$$\\ $$ __$$\\
$$ / \\__|\_/ $$ |$$ | $$ |$$ / $$ |
$$ | $$$$$ / $$$$$$$ |$$ | $$ |
$$ | \\___$$\\ $$ ____/ $$ | $$ |
$$ | $$\\ $$\\ $$ |$$ | $$ | $$ |
\\$$$$$$ |\\$$$$$$ |$$ | $$$$$$ |
\\______/ \\______/ \\__| \\______/
ESPILON C2 Framework - Command and Control Server
"""
if not args.tui:
Display.system_message(header)
Display.system_message("Initializing ESPILON C2 core...")
# ============================
# Core components
# ============================
registry = DeviceRegistry()
logger = LogManager()
keystore = KeyStore("keys.json")
if not args.tui:
Display.system_message(f"Loaded {len(keystore)} device key(s) from {keystore.path}")
# Initialize CLI first, then pass it to Transport
commands = CommandRegistry()
commands.register(RebootCommand())
groups = GroupRegistry()
# Placeholder for CLI, will be properly initialized after Transport
cli_instance = None
transport = Transport(registry, logger, keystore, cli_instance)
cli_instance = CLI(registry, commands, groups, transport)
transport.set_cli(cli_instance) # Set the actual CLI instance in transport
cli = cli_instance # Assign the initialized CLI to 'cli'
# ============================
# TCP server
# ============================
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
server.bind((HOST, PORT))
except OSError as e:
Display.error(f"Failed to bind server to {HOST}:{PORT}: {e}")
sys.exit(1)
server.listen()
if not args.tui:
Display.system_message(f"Server listening on {HOST}:{PORT}")
# Function to periodically check device status
def device_status_checker():
while True:
now = time.time()
for device in registry.all():
if now - device.last_seen > DEVICE_TIMEOUT_SECONDS:
if device.status != "Inactive":
device.status = "Inactive"
Display.device_event(device.id, "Status changed to Inactive (timeout)")
elif device.status == "Inactive" and now - device.last_seen <= DEVICE_TIMEOUT_SECONDS:
device.status = "Connected"
Display.device_event(device.id, "Status changed to Connected (heartbeat received)")
time.sleep(HEARTBEAT_CHECK_INTERVAL)
# Function to accept client connections
def accept_loop():
while True:
try:
sock, addr = server.accept()
threading.Thread(
target=client_thread,
args=(sock, addr, transport, registry),
daemon=True
).start()
except OSError:
break
except Exception as e:
Display.error(f"Server error: {e}")
# Device status checker thread
threading.Thread(target=device_status_checker, daemon=True).start()
# Accept loop thread
threading.Thread(target=accept_loop, daemon=True).start()
# ============================
# TUI or CLI mode
# ============================
if args.tui:
try:
from tui.app import C3POApp
Display.enable_tui_mode()
app = C3POApp(registry=registry, cli=cli)
app.run()
except ImportError as e:
Display.error(f"TUI not available: {e}")
Display.error("Install textual: pip install textual")
sys.exit(1)
except KeyboardInterrupt:
pass
else:
# Classic CLI mode
try:
cli.loop()
except KeyboardInterrupt:
Display.system_message("Shutdown requested. Exiting...")
server.close()
if __name__ == "__main__":
main()