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

247 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from core.crypto import CryptoContext
from core.device import Device
from core.keystore import KeyStore
from core.registry import DeviceRegistry
from log.manager import LogManager
from utils.display import Display
from proto.c2_pb2 import Command, AgentMessage, AgentMsgType
# Forward declaration for type hinting to avoid circular import
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cli.cli import CLI
class Transport:
def __init__(self, registry: DeviceRegistry, logger: LogManager,
keystore: KeyStore, cli_instance: 'CLI' = None):
self.registry = registry
self.logger = logger
self.keystore = keystore
self.cli = cli_instance
self.command_responses = {}
self.hp_store = None
self.hp_commander = None
# Cache of CryptoContext per device_id (HKDF derivation is expensive)
self._crypto_cache: dict[str, CryptoContext] = {}
def set_cli(self, cli_instance: 'CLI'):
self.cli = cli_instance
def _get_crypto(self, device_id: str) -> CryptoContext | None:
"""Get or create a CryptoContext for the given device."""
if device_id in self._crypto_cache:
return self._crypto_cache[device_id]
master_key = self.keystore.get(device_id)
if master_key is None:
return None
ctx = CryptoContext(master_key, device_id)
self._crypto_cache[device_id] = ctx
return ctx
# ==================================================
# RX (ESP → C2)
# ==================================================
def handle_incoming(self, sock, addr, raw_data: bytes):
"""
raw_data = device_id:BASE64( nonce[12] || ChaCha20-Poly1305( Protobuf ) || tag[16] )
"""
# 1) Parse device_id prefix
raw_str = raw_data
if b":" not in raw_str:
Display.error(f"No device_id prefix in message from {addr}")
return
device_id_bytes, b64_payload = raw_str.split(b":", 1)
device_id = device_id_bytes.decode(errors="ignore").strip()
if not device_id:
Display.error(f"Empty device_id from {addr}")
return
# 2) Lookup crypto key for this device
crypto = self._get_crypto(device_id)
if crypto is None:
Display.error(f"Unknown device '{device_id}' from {addr} no key in keystore")
return
# 3) Base64 decode
try:
encrypted = crypto.b64_decode(b64_payload)
except Exception as e:
Display.error(f"Base64 decode failed from {device_id}@{addr}: {e}")
return
# 4) Decrypt + verify (AEAD)
try:
protobuf_bytes = crypto.decrypt(encrypted)
except Exception as e:
Display.error(f"Decrypt/auth failed from {device_id}@{addr}: {e}")
return
# 5) Protobuf decode → AgentMessage
try:
msg = AgentMessage.FromString(protobuf_bytes)
except Exception as e:
Display.error(f"Protobuf decode failed from {device_id}@{addr}: {e}")
return
if not msg.device_id:
msg.device_id = device_id
self._dispatch(sock, addr, msg)
# ==================================================
# DISPATCH
# ==================================================
def _dispatch(self, sock, addr, msg: AgentMessage):
device = self.registry.get(msg.device_id)
is_new_device = False
if not device:
device = Device(
id=msg.device_id,
sock=sock,
address=addr
)
self.registry.add(device)
Display.device_event(device.id, f"Connected from {addr[0]}")
is_new_device = True
else:
# Device reconnected with new socket - update connection info
if device.sock != sock:
try:
device.sock.close()
except Exception:
pass
device.sock = sock
device.address = addr
Display.device_event(device.id, f"Reconnected from {addr[0]}:{addr[1]}")
device.touch()
self._handle_agent_message(device, msg)
# Auto-query system_info on new device connection
if is_new_device:
self._auto_query_system_info(device)
def _auto_query_system_info(self, device: Device):
"""Send system_info command automatically when device connects."""
try:
cmd = Command()
cmd.device_id = device.id
cmd.command_name = "system_info"
cmd.request_id = f"auto-sysinfo-{device.id}"
self.send_command(device.sock, cmd, device.id)
except Exception as e:
Display.error(f"Auto system_info failed for {device.id}: {e}")
def _parse_system_info(self, device: Device, payload: str):
"""Parse system_info response and update device info."""
# Format: chip=esp32 cores=2 flash=external heap=4310096 uptime=7s modules=network,fakeap
try:
for part in payload.split():
if "=" in part:
key, value = part.split("=", 1)
if key == "chip":
device.chip = value
elif key == "modules":
device.modules = value
# Notify TUI about device info update
Display.device_event(device.id, f"INFO: {payload}")
# Send special message to update TUI title
from utils.display import Display as Disp
if Disp._tui_mode:
from tui.bridge import tui_bridge, TUIMessage, MessageType
tui_bridge.post_message(TUIMessage(
msg_type=MessageType.DEVICE_INFO_UPDATED,
device_id=device.id,
payload=device.modules
))
except Exception as e:
Display.error(f"Failed to parse system_info: {e}")
# ==================================================
# AGENT MESSAGE HANDLER
# ==================================================
def _handle_agent_message(self, device: Device, msg: AgentMessage):
payload_str = ""
if msg.payload:
try:
payload_str = msg.payload.decode(errors="ignore")
except Exception:
payload_str = repr(msg.payload)
if msg.type == AgentMsgType.AGENT_CMD_RESULT:
# Check if this is auto system_info response
if msg.request_id and msg.request_id.startswith("auto-sysinfo-"):
self._parse_system_info(device, payload_str)
elif msg.request_id and msg.request_id.startswith("hp-") and self.hp_commander:
# Route honeypot dashboard command responses
self.hp_commander.handle_response(msg.request_id, device.id, payload_str, msg.eof)
elif msg.request_id and self.cli:
self.cli.handle_command_response(msg.request_id, device.id, payload_str, msg.eof)
else:
Display.device_event(device.id, f"Command result (no request_id or CLI not set): {payload_str}")
elif msg.type == AgentMsgType.AGENT_INFO:
# Check for system_info response (format: chip=... modules=...)
if "chip=" in payload_str and "modules=" in payload_str:
self._parse_system_info(device, payload_str)
return
# Check for MLAT data (format: MLAT:x;y;rssi)
elif payload_str.startswith("MLAT:") and self.cli:
mlat_data = payload_str[5:] # Remove "MLAT:" prefix
if self.cli.mlat_engine.parse_mlat_message(device.id, mlat_data):
# Recalculate position if we have enough scanners
state = self.cli.mlat_engine.get_state()
if state["scanners_count"] >= 3:
self.cli.mlat_engine.calculate_position()
else:
Display.device_event(device.id, f"MLAT: Invalid data format: {mlat_data}")
else:
Display.device_event(device.id, f"INFO: {payload_str}")
elif msg.type == AgentMsgType.AGENT_ERROR:
Display.device_event(device.id, f"ERROR: {payload_str}")
elif msg.type == AgentMsgType.AGENT_LOG:
Display.device_event(device.id, f"LOG: {payload_str}")
elif msg.type == AgentMsgType.AGENT_DATA:
# Route honeypot events to hp_store
if payload_str.startswith("HP|") and self.hp_store:
self.hp_store.parse_and_store(device.id, payload_str)
Display.device_event(device.id, f"DATA: {payload_str}")
else:
Display.device_event(device.id, f"UNKNOWN Message Type ({AgentMsgType.Name(msg.type)}): {payload_str}")
# ==================================================
# TX (C2 → ESP)
# ==================================================
def send_command(self, sock, cmd: Command, device_id: str = None):
"""
Command → Protobuf → ChaCha20-Poly1305 → Base64 → \\n
"""
target_id = device_id or cmd.device_id
crypto = self._get_crypto(target_id)
if crypto is None:
Display.error(f"Cannot send to '{target_id}' no key in keystore")
return
try:
proto = cmd.SerializeToString()
# Encrypt (AEAD)
encrypted = crypto.encrypt(proto)
# Base64
b64 = crypto.b64_encode(encrypted)
sock.sendall(b64 + b"\n")
except Exception as e:
Display.error(f"Failed to send command to {target_id}: {e}")