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
217 lines
7.4 KiB
Python
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()
|