espilon-source/tools/flasher/flash.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

519 lines
19 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

#!/usr/bin/env python3
"""
Epsilon Multi-Device Flasher
Automates building and flashing ESP32 devices with custom configurations.
"""
import os
import json
import subprocess
import string
import random
import argparse
from typing import List, Optional
from dataclasses import dataclass
# Configuration
CONFIG_FILE = "devices.json"
SDKCONFIG_FILENAME = "sdkconfig.defaults"
@dataclass
class Device:
"""Represents an ESP32 device configuration"""
device_id: str
port: str
srv_ip: str
srv_port: int = 2626
# Network configuration
network_mode: str = "wifi" # "wifi" or "gprs"
wifi_ssid: Optional[str] = None
wifi_pass: Optional[str] = None
gprs_apn: Optional[str] = "sl2sfr"
# Optional settings
hostname: Optional[str] = None
# Modules
module_network: bool = True
module_recon: bool = False
module_fakeap: bool = False
# Recon settings
recon_camera: bool = False
recon_ble_trilat: bool = False
# Security (master key provisioned separately via provision.py)
def __post_init__(self):
"""Generate hostname if not provided"""
if not self.hostname:
self.hostname = self._generate_hostname()
# Validate network mode
if self.network_mode not in ["wifi", "gprs"]:
raise ValueError(f"Invalid network_mode: {self.network_mode}. Must be 'wifi' or 'gprs'")
# Validate WiFi mode has credentials
if self.network_mode == "wifi" and (not self.wifi_ssid or not self.wifi_pass):
raise ValueError("WiFi mode requires wifi_ssid and wifi_pass")
@staticmethod
def _generate_hostname() -> str:
"""Generate a realistic device hostname"""
hostnames = [
"iPhone",
"Android",
f"DESKTOP-{''.join([random.choice(string.digits + string.ascii_uppercase) for _ in range(7)])}",
# iPhones
"iPhone-15-pro-max", "iPhone-15-pro", "iPhone-15", "iPhone-15-plus",
"iPhone-14-pro-max", "iPhone-14-pro", "iPhone-14", "iPhone-14-plus",
"iPhone-se-3rd-gen", "iPhone-13-pro-max",
# Samsung
"galaxy-s24-ultra", "galaxy-s24", "galaxy-z-fold5", "galaxy-z-flip5", "galaxy-a55",
# Xiaomi
"xiaomi-14-ultra", "xiaomi-14", "redmi-note-13-pro-plus", "redmi-note-13-5g", "poco-f6-pro",
# OnePlus
"oneplus-12", "oneplus-12r", "oneplus-11", "oneplus-nord-3", "oneplus-nord-ce-3-lite",
# Google
"pixel-8-pro", "pixel-8", "pixel-7a", "pixel-fold", "pixel-6a",
# Motorola
"moto-edge-50-ultra", "moto-g-stylus-5g-2024", "moto-g-power-2024",
"razr-50-ultra", "moto-e32",
# Sony
"xperia-1-vi", "xperia-10-vi", "xperia-5-v", "xperia-l5", "xperia-pro-i",
# Oppo
"oppo-find-x6-pro", "oppo-reno9-pro", "oppo-a78", "oppo-f21-pro", "oppo-a17",
# Vivo
"vivo-x90-pro-plus", "vivo-x90-pro", "vivo-y35", "vivo-y75", "vivo-v29e",
# Realme
"realme-11-pro-plus", "realme-10x", "realme-9i", "realme-c33", "realme-11x",
# Asus
"rog-phone-8-pro", "zenfone-10", "rog-phone-7", "rog-phone-6d", "asus-zenfone-9",
# Lenovo
"lenovo-legion-y90", "lenovo-k14-note", "lenovo-k14-plus", "lenovo-tab-m10",
# Honor
"honor-90", "honor-x8a", "honor-70-pro", "honor-magic5-pro", "honor-x7a",
# Huawei
"huawei-p60-pro", "huawei-p50-pro", "huawei-mate-50-pro", "huawei-mate-xs-2", "huawei-nova-11",
# LG
"lg-wing", "lg-velvet", "lg-g8x-thinQ", "lg-v60-thinQ", "lg-k92-5g"
]
return random.choice(hostnames)
@classmethod
def from_dict(cls, data: dict):
"""Create Device from dictionary"""
return cls(
device_id=data["device_id"],
port=data["port"],
srv_ip=data["srv_ip"],
srv_port=data.get("srv_port", 2626),
network_mode=data.get("network_mode", "wifi"),
wifi_ssid=data.get("wifi_ssid"),
wifi_pass=data.get("wifi_pass"),
gprs_apn=data.get("gprs_apn", "sl2sfr"),
hostname=data.get("hostname"),
module_network=data.get("module_network", True),
module_recon=data.get("module_recon", False),
module_fakeap=data.get("module_fakeap", False),
recon_camera=data.get("recon_camera", False),
recon_ble_trilat=data.get("recon_ble_trilat", False)
)
def __str__(self):
return f"{self.device_id} ({self.network_mode.upper()}) on {self.port}"
class FirmwareBuilder:
"""Handles firmware building for ESP32 devices"""
def __init__(self, project_path: str):
self.project_path = project_path
self.sdkconfig_path = os.path.join(self.project_path, SDKCONFIG_FILENAME)
self.build_dir = os.path.join(self.project_path, "build")
self.firmware_dir = os.path.join(self.project_path, "firmware")
os.makedirs(self.firmware_dir, exist_ok=True)
def generate_sdkconfig(self, device: Device):
"""Generate sdkconfig.defaults for a specific device"""
# Backup existing config
if os.path.exists(self.sdkconfig_path):
backup_path = f"{self.sdkconfig_path}.bak"
with open(self.sdkconfig_path, "r") as src, open(backup_path, "w") as dst:
dst.write(src.read())
# Generate new config
config_lines = []
# Device ID
config_lines.append(f'CONFIG_DEVICE_ID="{device.device_id}"')
# Network Mode
if device.network_mode == "wifi":
config_lines.append("CONFIG_NETWORK_WIFI=y")
config_lines.append("CONFIG_NETWORK_GPRS=n")
config_lines.append(f'CONFIG_WIFI_SSID="{device.wifi_ssid}"')
config_lines.append(f'CONFIG_WIFI_PASS="{device.wifi_pass}"')
else: # gprs
config_lines.append("CONFIG_NETWORK_WIFI=n")
config_lines.append("CONFIG_NETWORK_GPRS=y")
config_lines.append(f'CONFIG_GPRS_APN="{device.gprs_apn}"')
# Server
config_lines.append(f'CONFIG_SERVER_IP="{device.srv_ip}"')
config_lines.append(f'CONFIG_SERVER_PORT={device.srv_port}')
# Security (master key provisioned via provision.py into factory NVS)
# Modules
config_lines.append(f'CONFIG_MODULE_NETWORK={"y" if device.module_network else "n"}')
config_lines.append(f'CONFIG_MODULE_RECON={"y" if device.module_recon else "n"}')
config_lines.append(f'CONFIG_MODULE_FAKEAP={"y" if device.module_fakeap else "n"}')
# Recon settings (only if module enabled)
if device.module_recon:
config_lines.append(f'CONFIG_RECON_MODE_CAMERA={"y" if device.recon_camera else "n"}')
config_lines.append(f'CONFIG_RECON_MODE_BLE_TRILAT={"y" if device.recon_ble_trilat else "n"}')
# Crypto: ChaCha20-Poly1305 + HKDF (mbedtls legacy API, ESP-IDF v5.3)
config_lines.append("CONFIG_MBEDTLS_CHACHA20_C=y")
config_lines.append("CONFIG_MBEDTLS_POLY1305_C=y")
config_lines.append("CONFIG_MBEDTLS_CHACHAPOLY_C=y")
config_lines.append("CONFIG_MBEDTLS_HKDF_C=y")
# Partition table (custom with factory NVS)
config_lines.append("CONFIG_PARTITION_TABLE_CUSTOM=y")
config_lines.append('CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"')
config_lines.append("CONFIG_LWIP_IPV4_NAPT=y")
config_lines.append("CONFIG_LWIP_IPV4_NAPT_PORTMAP=y")
config_lines.append("CONFIG_LWIP_IP_FORWARD=y")
config_lines.append(f'CONFIG_LWIP_LOCAL_HOSTNAME="{device.hostname}"')
# Bluetooth (for BLE trilateration)
if device.recon_ble_trilat:
config_lines.append("CONFIG_BT_ENABLED=y")
config_lines.append("CONFIG_BT_BLUEDROID_ENABLED=y")
config_lines.append("CONFIG_BT_BLE_ENABLED=y")
# Write config
with open(self.sdkconfig_path, "w") as f:
f.write("\n".join(config_lines) + "\n")
print(f"✅ Generated sdkconfig.defaults for {device.device_id}")
def build(self, device: Device) -> Optional[str]:
"""Build firmware for a specific device"""
print(f"\n{'='*60}")
print(f"🔧 Building firmware for: {device}")
print(f"{'='*60}")
# Generate config
self.generate_sdkconfig(device)
# Remove old sdkconfig to force reconfiguration
sdkconfig = os.path.join(self.project_path, "sdkconfig")
if os.path.exists(sdkconfig):
os.remove(sdkconfig)
print("🗑️ Removed old sdkconfig")
# Build
try:
print("⚙️ Running idf.py build...")
result = subprocess.run(
["bash", "-c", f". $HOME/esp-idf/export.sh > /dev/null 2>&1 && idf.py -C {self.project_path} -D SDKCONFIG_DEFAULTS={SDKCONFIG_FILENAME} build 2>&1"],
capture_output=True,
text=True,
timeout=300 # 5 minutes timeout
)
if result.returncode != 0:
print(f"❌ Build failed for {device.device_id}")
print("Error output:")
print(result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout)
return None
except subprocess.TimeoutExpired:
print(f"❌ Build timeout for {device.device_id}")
return None
except Exception as e:
print(f"❌ Build error for {device.device_id}: {e}")
return None
# Find binary
bin_name = "epsilon_bot.bin"
bin_path = os.path.join(self.build_dir, bin_name)
if not os.path.exists(bin_path):
print(f"❌ Binary not found: {bin_path}")
return None
# Copy to firmware directory with device ID
output_bin = os.path.join(self.firmware_dir, f"{device.device_id}.bin")
subprocess.run(["cp", bin_path, output_bin], check=True)
print(f"✅ Firmware saved: {output_bin}")
return output_bin
class Flasher:
"""Handles flashing ESP32 devices"""
def __init__(self, project_path: str):
self.project_path = project_path
self.build_dir = os.path.join(project_path, "build")
def flash(self, device: Device, bin_path: str):
"""Flash a device with compiled firmware"""
print(f"\n{'='*60}")
print(f"🚀 Flashing: {device}")
print(f"{'='*60}")
# Locate required files
bootloader = os.path.join(self.build_dir, "bootloader", "bootloader.bin")
partitions = os.path.join(self.build_dir, "partition_table", "partition-table.bin")
# Check all files exist
if not os.path.exists(bootloader):
print(f"❌ Missing bootloader: {bootloader}")
return False
if not os.path.exists(partitions):
print(f"❌ Missing partition table: {partitions}")
return False
if not os.path.exists(bin_path):
print(f"❌ Missing application binary: {bin_path}")
return False
print(f"📁 Bootloader: {bootloader}")
print(f"📁 Partitions: {partitions}")
print(f"📁 Application: {bin_path}")
print(f"🔌 Port: {device.port}")
# Flash
try:
subprocess.run([
"esptool.py",
"--chip", "esp32",
"--port", device.port,
"--baud", "460800",
"write_flash", "-z",
"0x1000", bootloader,
"0x8000", partitions,
"0x10000", bin_path
], check=True)
print(f"✅ Successfully flashed {device.device_id}")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Flash failed for {device.device_id}: {e}")
return False
except Exception as e:
print(f"❌ Unexpected error flashing {device.device_id}: {e}")
return False
def load_devices_from_config(config_path: str) -> tuple[str, List[Device]]:
"""Load devices from JSON config file"""
if not os.path.exists(config_path):
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, "r") as f:
data = json.load(f)
project_path = data.get("project")
if not project_path:
raise ValueError("Missing 'project' field in config")
devices_data = data.get("devices", [])
devices = [Device.from_dict(d) for d in devices_data]
return project_path, devices
def main():
parser = argparse.ArgumentParser(
description="Epsilon ESP32 Multi-Device Flasher",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Flash all devices from config file
python flash.py --config devices.json
# Flash a single device manually (WiFi)
python flash.py --manual \\
--project /home/user/epsilon/espilon_bot \\
--device-id abc12345 \\
--port /dev/ttyUSB0 \\
--srv-ip 192.168.1.100 \\
--wifi-ssid MyWiFi \\
--wifi-pass MyPassword
# Flash a single device manually (GPRS)
python flash.py --manual \\
--project /home/user/epsilon/espilon_bot \\
--device-id def67890 \\
--port /dev/ttyUSB1 \\
--srv-ip 203.0.113.10 \\
--network-mode gprs \\
--gprs-apn sl2sfr
# Build only (no flash)
python flash.py --config devices.json --build-only
"""
)
# Mode selection
mode_group = parser.add_mutually_exclusive_group(required=True)
mode_group.add_argument("--config", type=str, help="Path to devices.json config file")
mode_group.add_argument("--manual", action="store_true", help="Manual device configuration")
# Manual device configuration
parser.add_argument("--project", type=str, help="Path to epsilon_bot project directory")
parser.add_argument("--device-id", type=str, help="Device ID (8 hex chars)")
parser.add_argument("--port", type=str, help="Serial port (e.g., /dev/ttyUSB0)")
parser.add_argument("--srv-ip", type=str, help="C2 server IP address")
parser.add_argument("--srv-port", type=int, default=2626, help="C2 server port (default: 2626)")
# Network configuration
parser.add_argument("--network-mode", choices=["wifi", "gprs"], default="wifi", help="Network mode")
parser.add_argument("--wifi-ssid", type=str, help="WiFi SSID (required for WiFi mode)")
parser.add_argument("--wifi-pass", type=str, help="WiFi password (required for WiFi mode)")
parser.add_argument("--gprs-apn", type=str, default="sl2sfr", help="GPRS APN (default: sl2sfr)")
# Optional settings
parser.add_argument("--hostname", type=str, help="Device hostname (random if not specified)")
# Crypto keys are now provisioned separately via provision.py
# Modules
parser.add_argument("--enable-recon", action="store_true", help="Enable recon module")
parser.add_argument("--enable-fakeap", action="store_true", help="Enable fake AP module")
parser.add_argument("--enable-camera", action="store_true", help="Enable camera reconnaissance")
parser.add_argument("--enable-ble-trilat", action="store_true", help="Enable BLE trilateration")
# Actions
parser.add_argument("--build-only", action="store_true", help="Build firmware without flashing")
parser.add_argument("--flash-only", action="store_true", help="Flash existing firmware without rebuilding")
args = parser.parse_args()
# Load devices
if args.config:
# Load from config file
project_path, devices = load_devices_from_config(args.config)
print(f"📋 Loaded {len(devices)} device(s) from {args.config}")
print(f"📂 Project: {project_path}")
else:
# Manual device
required = ["project", "device_id", "port", "srv_ip"]
missing = [f for f in required if not getattr(args, f.replace("-", "_"))]
if missing:
parser.error(f"--manual mode requires: {', '.join(f'--{f}' for f in missing)}")
# Validate WiFi requirements
if args.network_mode == "wifi" and (not args.wifi_ssid or not args.wifi_pass):
parser.error("WiFi mode requires --wifi-ssid and --wifi-pass")
project_path = args.project
device = Device(
device_id=args.device_id,
port=args.port,
srv_ip=args.srv_ip,
srv_port=args.srv_port,
network_mode=args.network_mode,
wifi_ssid=args.wifi_ssid,
wifi_pass=args.wifi_pass,
gprs_apn=args.gprs_apn,
hostname=args.hostname,
module_network=True,
module_recon=args.enable_recon,
module_fakeap=args.enable_fakeap,
recon_camera=args.enable_camera,
recon_ble_trilat=args.enable_ble_trilat
)
devices = [device]
print(f"📋 Manual device configuration: {device}")
# Validate project path
if not os.path.exists(project_path):
print(f"❌ Project path not found: {project_path}")
return 1
# Initialize tools
builder = FirmwareBuilder(project_path)
flasher = Flasher(project_path)
# Process each device
success_count = 0
fail_count = 0
for i, device in enumerate(devices, 1):
print(f"\n{'#'*60}")
print(f"# Device {i}/{len(devices)}: {device.device_id}")
print(f"{'#'*60}")
try:
# Build
if not args.flash_only:
bin_path = builder.build(device)
if not bin_path:
print(f"⚠️ Skipping flash for {device.device_id} (build failed)")
fail_count += 1
continue
else:
# Use existing binary
bin_path = os.path.join(builder.firmware_dir, f"{device.device_id}.bin")
if not os.path.exists(bin_path):
print(f"❌ Firmware not found: {bin_path}")
fail_count += 1
continue
# Flash
if not args.build_only:
if flasher.flash(device, bin_path):
success_count += 1
else:
fail_count += 1
else:
print(f" Build-only mode, skipping flash")
success_count += 1
except Exception as e:
print(f"❌ Error processing {device.device_id}: {e}")
fail_count += 1
# Summary
print(f"\n{'='*60}")
print(f"📊 SUMMARY")
print(f"{'='*60}")
print(f"✅ Success: {success_count}/{len(devices)}")
print(f"❌ Failed: {fail_count}/{len(devices)}")
print(f"{'='*60}\n")
return 0 if fail_count == 0 else 1
if __name__ == "__main__":
exit(main())