ε - 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
This commit is contained in:
Eun0us 2026-02-10 21:28:45 +01:00
parent 3311626d58
commit 8b6c1cd53d
111 changed files with 2135 additions and 2854 deletions

14
.gitignore vendored
View File

@ -30,18 +30,20 @@ ENV/
.venv
# Tools - Python dependencies
tools/c2/__pycache__/
tools/c3po/__pycache__/
tools/C3PO/__pycache__/
tools/flasher/__pycache__/
*.pyc
# Configuration files with secrets
tools/flasher/devices.json
tools/flasher/devices.*.json
tools/c2/config.json
tools/c3po/config.json
tools/C3PO/config.json
**/config.local.json
# C3PO runtime / secrets
tools/C3PO/keys.json
tools/C3PO/*.db
# Logs
*.log
logs/
@ -49,8 +51,8 @@ espilon_bot/logs/
sdkconfig
# C2 Runtime files (camera streams, recordings)
tools/c2/static/streams/*.jpg
tools/c2/static/recordings/*.avi
tools/C3PO/static/streams/*.jpg
tools/C3PO/static/recordings/*.avi
*.avi
# IDE and Editor

View File

@ -415,7 +415,7 @@ idf.py monitor
**For C2 changes**:
```bash
cd tools/c2
python3 c3po.py --port 2626
python3 c3po.py
# Test with connected ESP32
```
@ -596,8 +596,9 @@ epsilon/
│ │ └── mod_recon/ # Recon module
│ └── main/ # Main application
├── tools/ # Supporting tools
│ ├── c2/ # C2 server (Python)
│ ├── C3PO/ # C2 server (Python)
│ ├── flasher/ # Multi-flasher tool
│ ├── provisioning/ # Device key provisioning
│ └── nan/ # NanoPB tools
├── docs/ # Documentation
│ ├── INSTALL.md

View File

@ -138,7 +138,7 @@ Espilon transforme des microcontrôleurs ESP32 abordables à **~5€** en agents
│ ESP32 Agent │
│ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ WiFi/ │→ │ ChaCha20 │→ │ C2 Protocol │ │
│ │ GPRS │← │ Crypto │← │ (nanoPB/TCP) │ │
│ │ GPRS │← │ Poly1305 │← │ (nanoPB/TCP) │ │
│ └───────────┘ └──────────┘ └─────────────────┘ │
│ ↓ ↓ ↓ │
│ ┌───────────────────────────────────────────────┐ │
@ -157,7 +157,7 @@ Espilon transforme des microcontrôleurs ESP32 abordables à **~5€** en agents
### Composants Clés
- **Core** : Connexion réseau, crypto ChaCha20, protocole nanoPB
- **Core** : Connexion réseau, ChaCha20-Poly1305 AEAD + dérivation HKDF, protocole nanoPB
- **Modules** : Système extensible (Network, FakeAP, Recon, etc.)
- **C2 (C3PO)** : Serveur Python asyncio pour contrôle multi-agents
- **C3PO**: Ancien c2 (serveur web - Trilateration + Front affichage caméra)
@ -259,8 +259,6 @@ python3 flash.py --config devices.json
"module_fakeap": false,
"recon_camera": false,
"recon_ble_trilat": false,
"crypto_key": "testde32chars00000000000000000000",
"crypto_nonce": "noncenonceno"
},
## GPRS AGENT ##
@ -282,22 +280,30 @@ python3 flash.py --config devices.json
Voir [tools/flasher/README.md](tools/flasher/README.md) pour la documentation complète.
### Provisioning des Devices
Chaque device nécessite une master key unique flashée dans sa partition factory NVS :
```bash
cd tools/provisioning
python3 provision.py --device-id mon-device --port /dev/ttyUSB0
```
Génère une clé aléatoire de 32 bytes, l'écrit en factory NVS, et la sauvegarde dans le keystore C2 (`keys.json`).
Voir [tools/provisioning/](tools/provisioning/) pour les détails.
### C2 Server (C3PO)
Serveur de Command & Control :
```bash
cd tools/c2
cd tools/C3PO
pip3 install -r requirements.txt
python3 c3po.py --port 2626
python3 c3po.py
```
**Commandes** :
- `list` : Lister les agents connectés
- `select <id>` : Sélectionner un agent
- `cmd <command>` : Exécuter une commande
- `group` : Gérer les groupes d'agents
Documentation complète et liste des commandes : voir [tools/C3PO/README.md](tools/C3PO/README.md).
---
@ -305,17 +311,13 @@ python3 c3po.py --port 2626
### Chiffrement
- **ChaCha20** pour les communications C2
- **Clés configurables** via menuconfig
- **ChaCha20-Poly1305 AEAD** pour le chiffrement authentifié de toutes les communications C2
- **HKDF-SHA256** dérivation de clé (master key per-device + salt device ID)
- **Nonce aléatoire de 12 bytes** par message (RNG hardware ESP32)
- **Master keys per-device** stockées en partition factory NVS (read-only)
- **Protocol Buffers (nanoPB)** pour la sérialisation
⚠️ **CHANGEZ LES CLÉS PAR DÉFAUT** pour un usage en production :
```bash
# Générer des clés aléatoires
openssl rand -hex 32 # ChaCha20 key (32 bytes)
openssl rand -hex 12 # Nonce (12 bytes)
```
Provisionner chaque device avec une master key unique via `tools/provisioning/provision.py`. Les clés ne sont jamais hardcodées dans le firmware.
### Usage Responsable
@ -356,9 +358,10 @@ Espilon doit être utilisé uniquement pour :
### V2.0 (En cours)
- [x] Upgrade crypto ChaCha20-Poly1305 AEAD + HKDF
- [x] Provisioning per-device factory NVS
- [x] Réécriture C3PO avec crypto per-device
- [ ] Mesh networking (BLE/WiFi)
- [ ] Implémenter Module reccoon dans C3PO
- [ ] Améliorer la Documentations [here](https://docs.espilon.net)
- [ ] OTA updates
- [ ] Multilatération collaborative
- [ ] Optimisation mémoire

View File

@ -138,7 +138,7 @@ Espilon transforms affordable ESP32 microcontrollers (~$5) into powerful network
| ESP32 Agent |
| +-----------+ +----------+ +---------------------+ |
| | WiFi/ |->| ChaCha20 |->| C2 Protocol | |
| | GPRS |<-| Crypto |<-| (nanoPB/TCP) | |
| | GPRS |<-| Poly1305 |<-| (nanoPB/TCP) | |
| +-----------+ +----------+ +---------------------+ |
| | | | |
| +-----------------------------------------------------+|
@ -157,7 +157,7 @@ Espilon transforms affordable ESP32 microcontrollers (~$5) into powerful network
### Key Components
- **Core**: Network connection, ChaCha20 crypto, nanoPB protocol
- **Core**: Network connection, ChaCha20-Poly1305 AEAD + HKDF key derivation, nanoPB protocol
- **Modules**: Extensible system (Network, FakeAP, Recon, etc.)
- **C2 (C3PO)**: Python asyncio server for multi-agent control
- **Flasher**: Automated multi-device flashing tool
@ -257,22 +257,30 @@ python3 flash.py --config devices.json
See [tools/flasher/README.md](tools/flasher/README.md) for complete documentation.
### Device Provisioning
Each device needs a unique master key flashed into its factory NVS partition before first use:
```bash
cd tools/provisioning
python3 provision.py --device-id my-device --port /dev/ttyUSB0
```
This generates a 32-byte random master key, writes it to the factory NVS partition, and saves it to the C2 keystore (`keys.json`).
See [tools/provisioning/](tools/provisioning/) for details.
### C2 Server (C3PO)
Command & Control server:
```bash
cd tools/c2
cd tools/C3PO
pip3 install -r requirements.txt
python3 c3po.py --port 2626
python3 c3po.py
```
**Commands**:
- `list`: List connected agents
- `select <id>`: Select an agent
- `cmd <command>`: Execute a command
- `group`: Manage agent groups
Full C2 documentation and command list: see [tools/C3PO/README.md](tools/C3PO/README.md).
---
@ -280,17 +288,13 @@ python3 c3po.py --port 2626
### Encryption
- **ChaCha20** for C2 communications
- **Configurable keys** via menuconfig
- **ChaCha20-Poly1305 AEAD** for authenticated encryption of all C2 communications
- **HKDF-SHA256** key derivation (per-device master key + device ID salt)
- **Random 12-byte nonce** per message (ESP32 hardware RNG)
- **Per-device master keys** stored in factory NVS partition (read-only)
- **Protocol Buffers (nanoPB)** for serialization
**CHANGE DEFAULT KEYS** for production use:
```bash
# Generate random keys
openssl rand -hex 32 # ChaCha20 key (32 bytes)
openssl rand -hex 12 # Nonce (12 bytes)
```
Provision each device with a unique master key using `tools/provisioning/provision.py`. Keys are never hardcoded in firmware.
### Responsible Use
@ -331,8 +335,10 @@ Espilon should only be used for:
### V2.0 (In Progress)
- [x] ChaCha20-Poly1305 AEAD + HKDF crypto upgrade
- [x] Per-device factory NVS key provisioning
- [x] C3PO C2 rewrite with per-device crypto
- [ ] Mesh networking (BLE/WiFi)
- [ ] Improve documentation
- [ ] OTA updates
- [ ] Collaborative multilateration
- [ ] Memory optimization

View File

@ -28,6 +28,8 @@ typedef esp_err_t (*command_handler_t)(
* ============================================================ */
typedef struct {
const char *name; /* command name */
const char *sub; /* subcommand name (optional) */
const char *help; /* help text (optional) */
int min_args;
int max_args;
command_handler_t handler; /* handler */

View File

@ -30,6 +30,13 @@ static void async_worker(void *arg)
while (1) {
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
/* Recompute argv_ptrs to point into THIS copy's argv buffers.
* xQueueReceive copies the struct by value, so the old
* pointers (set at enqueue time) are now dangling. */
for (int i = 0; i < job.argc; i++) {
job.argv_ptrs[i] = job.argv[i];
}
ESP_LOGI(TAG, "Async exec: %s", job.cmd->name);
job.cmd->handler(

View File

@ -1,11 +1,11 @@
set(PRIV_REQUIRES_LIST
set(PRIV_REQUIRES_LIST
mbedtls
lwip
mod_network
nvs_flash
lwip
mod_network
mod_fakeAP
mod_recon
esp_timer
esp_driver_uart
driver
command
)

View File

@ -16,9 +16,11 @@
#include "c2.pb.h"
#include "pb_decode.h"
#include "freertos/semphr.h"
#include "utils.h"
int sock = -1;
SemaphoreHandle_t sock_mutex = NULL;
#ifdef CONFIG_NETWORK_WIFI
static const char *TAG = "CORE_WIFI";
@ -63,8 +65,8 @@ static bool tcp_connect(void)
{
struct sockaddr_in server_addr = {0};
sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (new_sock < 0) {
ESP_LOGE(TAG, "socket() failed");
return false;
}
@ -73,15 +75,18 @@ static bool tcp_connect(void)
server_addr.sin_port = htons(CONFIG_SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
if (lwip_connect(sock,
if (lwip_connect(new_sock,
(struct sockaddr *)&server_addr,
sizeof(server_addr)) != 0) {
ESP_LOGE(TAG, "connect() failed");
lwip_close(sock);
sock = -1;
lwip_close(new_sock);
return false;
}
xSemaphoreTake(sock_mutex, portMAX_DELAY);
sock = new_sock;
xSemaphoreGive(sock_mutex);
ESP_LOGI(TAG, "Connected to %s:%d",
CONFIG_SERVER_IP,
CONFIG_SERVER_PORT);
@ -94,10 +99,13 @@ static bool tcp_connect(void)
* ========================================================= */
static void handle_frame(const uint8_t *buf, size_t len)
{
char tmp[len + 1];
memcpy(tmp, buf, len);
tmp[len] = '\0';
c2_decode_and_exec(tmp);
if (len == 0 || len >= RX_BUF_SIZE) {
ESP_LOGW(TAG, "Frame too large or empty (%d bytes), dropping", (int)len);
return;
}
/* buf is already null-terminated by strtok in tcp_rx_loop,
and c2_decode_and_exec makes its own 1024-byte copy. */
c2_decode_and_exec((const char *)buf);
}
@ -108,11 +116,19 @@ static void tcp_rx_loop(void)
{
static uint8_t rx_buf[RX_BUF_SIZE];
int len = lwip_recv(sock, rx_buf, sizeof(rx_buf) - 1, 0);
xSemaphoreTake(sock_mutex, portMAX_DELAY);
int current_sock = sock;
xSemaphoreGive(sock_mutex);
if (current_sock < 0) return;
int len = lwip_recv(current_sock, rx_buf, sizeof(rx_buf) - 1, 0);
if (len <= 0) {
ESP_LOGW(TAG, "RX failed / disconnected");
xSemaphoreTake(sock_mutex, portMAX_DELAY);
lwip_close(sock);
sock = -1;
xSemaphoreGive(sock_mutex);
return;
}
@ -131,6 +147,9 @@ static void tcp_rx_loop(void)
* ========================================================= */
void tcp_client_task(void *pvParameters)
{
if (!sock_mutex)
sock_mutex = xSemaphoreCreateMutex();
while (1) {
if (!tcp_connect()) {

View File

@ -17,7 +17,7 @@ bool com_init(void)
xTaskCreatePinnedToCore(
tcp_client_task,
"tcp_client_task",
8192,
12288,
NULL,
1,
NULL,

View File

@ -1,12 +1,18 @@
// crypto.c
// crypto.c ChaCha20-Poly1305 AEAD with HKDF key derivation
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "esp_random.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "mbedtls/chacha20.h"
#include "mbedtls/chachapoly.h"
#include "mbedtls/hkdf.h"
#include "mbedtls/md.h"
#include "mbedtls/base64.h"
#include "mbedtls/platform_util.h"
#include "pb_decode.h"
#include "c2.pb.h"
@ -16,53 +22,186 @@
static const char *TAG = "CRYPTO";
/* ============================================================
* Compile-time security checks
* ============================================================ */
_Static_assert(sizeof(CONFIG_CRYPTO_KEY) - 1 == 32,
"CONFIG_CRYPTO_KEY must be exactly 32 bytes");
_Static_assert(sizeof(CONFIG_CRYPTO_NONCE) - 1 == 12,
"CONFIG_CRYPTO_NONCE must be exactly 12 bytes");
#define NONCE_LEN 12
#define TAG_LEN 16
#define KEY_LEN 32
#define OVERHEAD (NONCE_LEN + TAG_LEN) /* 28 bytes */
static uint8_t derived_key[KEY_LEN];
static bool crypto_ready = false;
/* ============================================================
* ChaCha20 encrypt/decrypt (same function)
* crypto_init read master key from factory NVS, derive via HKDF
* ============================================================ */
unsigned char *chacha_cd(const unsigned char *data, size_t data_len)
bool crypto_init(void)
{
if (!data || data_len == 0) {
ESP_LOGE(TAG, "Invalid input to chacha_cd");
return NULL;
esp_err_t err;
/* 1) Init the factory NVS partition */
err = nvs_flash_init_partition("fctry");
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init_partition(fctry) failed: %s",
esp_err_to_name(err));
return false;
}
unsigned char *out = (unsigned char *)malloc(data_len);
if (!out) {
ESP_LOGE(TAG, "malloc failed in chacha_cd");
return NULL;
/* 2) Open the crypto namespace (read-only) */
nvs_handle_t handle;
err = nvs_open_from_partition(
"fctry",
CONFIG_CRYPTO_FCTRY_NS,
NVS_READONLY,
&handle
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open_from_partition(fctry/%s) failed: %s",
CONFIG_CRYPTO_FCTRY_NS, esp_err_to_name(err));
return false;
}
unsigned char key[32];
unsigned char nonce[12];
uint32_t counter = 0;
/* 3) Read the 32-byte master key blob */
uint8_t master_key[KEY_LEN];
size_t mk_len = sizeof(master_key);
memcpy(key, CONFIG_CRYPTO_KEY, sizeof(key));
memcpy(nonce, CONFIG_CRYPTO_NONCE, sizeof(nonce));
err = nvs_get_blob(handle, CONFIG_CRYPTO_FCTRY_KEY, master_key, &mk_len);
nvs_close(handle);
int ret = mbedtls_chacha20_crypt(
key,
if (err != ESP_OK || mk_len != KEY_LEN) {
ESP_LOGE(TAG, "nvs_get_blob(%s) failed: %s (len=%u)",
CONFIG_CRYPTO_FCTRY_KEY, esp_err_to_name(err),
(unsigned)mk_len);
mbedtls_platform_zeroize(master_key, sizeof(master_key));
return false;
}
/* 4) HKDF-SHA256: derive the encryption key */
const char *info = "espilon-c2-v1";
const char *salt = CONFIG_DEVICE_ID;
int ret = mbedtls_hkdf(
mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
(const uint8_t *)salt, strlen(salt),
master_key, KEY_LEN,
(const uint8_t *)info, strlen(info),
derived_key, KEY_LEN
);
/* Wipe master key from RAM immediately */
mbedtls_platform_zeroize(master_key, sizeof(master_key));
if (ret != 0) {
ESP_LOGE(TAG, "HKDF failed (%d)", ret);
return false;
}
crypto_ready = true;
ESP_LOGI(TAG, "Crypto ready (ChaCha20-Poly1305 + HKDF)");
return true;
}
/* ============================================================
* crypto_encrypt ChaCha20-Poly1305 AEAD
*
* Output layout: nonce[12] || ciphertext[plain_len] || tag[16]
* Returns total output length, or -1 on error.
* ============================================================ */
int crypto_encrypt(const uint8_t *plain, size_t plain_len,
uint8_t *out, size_t out_cap)
{
if (!crypto_ready) {
ESP_LOGE(TAG, "crypto_encrypt: not initialized");
return -1;
}
if (!plain || plain_len == 0 || !out) {
ESP_LOGE(TAG, "crypto_encrypt: invalid args");
return -1;
}
size_t needed = plain_len + OVERHEAD;
if (out_cap < needed) {
ESP_LOGE(TAG, "crypto_encrypt: buffer too small (%u < %u)",
(unsigned)out_cap, (unsigned)needed);
return -1;
}
/* Random nonce in the first 12 bytes */
esp_fill_random(out, NONCE_LEN);
mbedtls_chachapoly_context ctx;
mbedtls_chachapoly_init(&ctx);
mbedtls_chachapoly_setkey(&ctx, derived_key);
int ret = mbedtls_chachapoly_encrypt_and_tag(
&ctx,
plain_len,
out, /* nonce */
NULL, 0, /* no AAD */
plain, /* input */
out + NONCE_LEN, /* output (ciphertext) */
out + NONCE_LEN + plain_len /* tag */
);
mbedtls_chachapoly_free(&ctx);
if (ret != 0) {
ESP_LOGE(TAG, "chachapoly encrypt failed (%d)", ret);
return -1;
}
return (int)needed;
}
/* ============================================================
* crypto_decrypt ChaCha20-Poly1305 AEAD
*
* Input layout: nonce[12] || ciphertext[N] || tag[16]
* Returns plaintext length, or -1 on error / auth failure.
* ============================================================ */
int crypto_decrypt(const uint8_t *in, size_t in_len,
uint8_t *out, size_t out_cap)
{
if (!crypto_ready) {
ESP_LOGE(TAG, "crypto_decrypt: not initialized");
return -1;
}
if (!in || in_len < OVERHEAD || !out) {
ESP_LOGE(TAG, "crypto_decrypt: invalid args (in_len=%u)",
(unsigned)in_len);
return -1;
}
size_t ct_len = in_len - OVERHEAD;
if (out_cap < ct_len) {
ESP_LOGE(TAG, "crypto_decrypt: buffer too small");
return -1;
}
const uint8_t *nonce = in;
const uint8_t *ct = in + NONCE_LEN;
const uint8_t *tag = in + NONCE_LEN + ct_len;
mbedtls_chachapoly_context ctx;
mbedtls_chachapoly_init(&ctx);
mbedtls_chachapoly_setkey(&ctx, derived_key);
int ret = mbedtls_chachapoly_auth_decrypt(
&ctx,
ct_len,
nonce,
counter,
data_len,
data,
NULL, 0, /* no AAD */
tag,
ct,
out
);
mbedtls_chachapoly_free(&ctx);
if (ret != 0) {
ESP_LOGE(TAG, "ChaCha20 failed (%d)", ret);
free(out);
return NULL;
ESP_LOGE(TAG, "AEAD auth/decrypt failed (%d)", ret);
return -1;
}
return out; /* binary-safe */
return (int)ct_len;
}
/* ============================================================
@ -134,7 +273,6 @@ char *base64_decode(const char *input, size_t *output_len)
return NULL;
}
/* Optional null terminator for debug */
out[*output_len] = '\0';
return (char *)out;
}
@ -155,11 +293,10 @@ bool c2_decode_and_exec(const char *frame)
memcpy(tmp, frame, n);
tmp[n] = '\0';
while (n > 0 && (tmp[n - 1] == '\r' || tmp[n - 1] == '\n' || tmp[n - 1] == ' ')) {
tmp[n - 1] = '\0';
n--;
tmp[--n] = '\0';
}
ESP_LOGI(TAG, "C2 RX b64: %s", tmp);
ESP_LOGD(TAG, "C2 RX b64 (%u bytes)", (unsigned)n);
/* 1) Base64 decode */
size_t decoded_len = 0;
@ -170,27 +307,28 @@ bool c2_decode_and_exec(const char *frame)
return false;
}
/* 2) ChaCha decrypt */
unsigned char *plain = chacha_cd((const unsigned char *)decoded, decoded_len);
/* 2) Decrypt + authenticate (AEAD) */
uint8_t plain[1024];
int plain_len = crypto_decrypt(
(const uint8_t *)decoded, decoded_len,
plain, sizeof(plain)
);
free(decoded);
if (!plain) {
ESP_LOGE(TAG, "ChaCha decrypt failed");
if (plain_len < 0) {
ESP_LOGE(TAG, "Decrypt/auth failed tampered or wrong key");
return false;
}
/* 3) Protobuf decode -> c2_Command */
c2_Command cmd = c2_Command_init_zero;
pb_istream_t is = pb_istream_from_buffer(plain, decoded_len);
pb_istream_t is = pb_istream_from_buffer(plain, (size_t)plain_len);
if (!pb_decode(&is, c2_Command_fields, &cmd)) {
ESP_LOGE(TAG, "PB decode error: %s", PB_GET_ERROR(&is));
free(plain);
return false;
}
free(plain);
/* 4) Log + dispatch */
#ifdef CONFIG_ESPILON_LOG_C2_VERBOSE
ESP_LOGI(TAG, "==== C2 COMMAND ====");

View File

@ -8,12 +8,14 @@
#include "pb_encode.h"
#include "c2.pb.h"
#include "utils.h" /* base64_encode, chacha_cd, CONFIG_DEVICE_ID */
#include "freertos/semphr.h"
#include "utils.h" /* crypto_encrypt, base64_encode, CONFIG_DEVICE_ID */
#define TAG "AGENT_MSG"
#define MAX_PROTOBUF_SIZE 512
extern int sock;
extern SemaphoreHandle_t sock_mutex;
/* ============================================================
* TCP helpers
@ -22,12 +24,19 @@ extern int sock;
static bool tcp_send_all(const void *buf, size_t len)
{
#ifdef CONFIG_NETWORK_WIFI
extern int sock;
xSemaphoreTake(sock_mutex, portMAX_DELAY);
int current_sock = sock;
xSemaphoreGive(sock_mutex);
if (current_sock < 0) {
ESP_LOGE(TAG, "socket not connected");
return false;
}
const uint8_t *p = (const uint8_t *)buf;
while (len > 0) {
int sent = lwip_write(sock, p, len);
int sent = lwip_write(current_sock, p, len);
if (sent <= 0) {
ESP_LOGE(TAG, "lwip_write failed");
return false;
@ -54,8 +63,11 @@ static bool send_base64_frame(const uint8_t *data, size_t len)
return false;
}
bool ok = tcp_send_all(b64, strlen(b64)) &&
tcp_send_all("\n", 1);
/* Prepend "device_id:" so the C2 can identify which key to use */
bool ok = tcp_send_all(CONFIG_DEVICE_ID, strlen(CONFIG_DEVICE_ID))
&& tcp_send_all(":", 1)
&& tcp_send_all(b64, strlen(b64))
&& tcp_send_all("\n", 1);
free(b64);
return ok;
@ -67,10 +79,10 @@ static bool send_base64_frame(const uint8_t *data, size_t len)
static bool encode_encrypt_send(c2_AgentMessage *msg)
{
uint8_t buffer[MAX_PROTOBUF_SIZE];
uint8_t pb_buf[MAX_PROTOBUF_SIZE];
pb_ostream_t stream =
pb_ostream_from_buffer(buffer, sizeof(buffer));
pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
if (!pb_encode(&stream, c2_AgentMessage_fields, msg)) {
ESP_LOGE(TAG, "pb_encode failed: %s",
@ -80,16 +92,17 @@ static bool encode_encrypt_send(c2_AgentMessage *msg)
size_t proto_len = stream.bytes_written;
uint8_t *cipher =
(uint8_t *)chacha_cd(buffer, proto_len);
if (!cipher) {
ESP_LOGE(TAG, "chacha_cd failed");
/* nonce[12] + ciphertext + tag[16] */
uint8_t enc_buf[MAX_PROTOBUF_SIZE + 12 + 16];
int enc_len = crypto_encrypt(pb_buf, proto_len,
enc_buf, sizeof(enc_buf));
if (enc_len < 0) {
ESP_LOGE(TAG, "crypto_encrypt failed");
return false;
}
bool ok = send_base64_frame(cipher, proto_len);
free(cipher);
return ok;
return send_base64_frame(enc_buf, (size_t)enc_len);
}
/* ============================================================

View File

@ -18,14 +18,15 @@ void process_command(const c2_Command *cmd)
}
/* -----------------------------------------------------
* Device ID check
* Device ID check allow broadcast (empty device_id)
* ----------------------------------------------------- */
//if (!device_id_matches(CONFIG_DEVICE_ID, cmd->device_id)) {
// ESP_LOGW(TAG,
// "Command not for this device (target=%s)",
// cmd->device_id);
// return;
//}
if (cmd->device_id[0] != '\0' &&
strcmp(CONFIG_DEVICE_ID, cmd->device_id) != 0) {
ESP_LOGW(TAG,
"Command not for this device (target=%s, self=%s)",
cmd->device_id, CONFIG_DEVICE_ID);
return;
}
/* -----------------------------------------------------
* Basic validation

View File

@ -64,14 +64,25 @@ extern int sock;
bool com_init(void);
/* ============================================================
* CRYPTO API
* CRYPTO API (ChaCha20-Poly1305 AEAD + HKDF)
* ============================================================ */
/* Init crypto: read master key from factory NVS, derive via HKDF-SHA256 */
bool crypto_init(void);
/*
* ChaCha20 encrypt/decrypt
* Retourne un buffer malloc()'d free() obligatoire
* Encrypt (AEAD). Output: nonce[12] || ciphertext || tag[16]
* Returns total output length, or -1 on error.
*/
unsigned char *chacha_cd(const unsigned char *data, size_t data_len);
int crypto_encrypt(const uint8_t *plain, size_t plain_len,
uint8_t *out, size_t out_cap);
/*
* Decrypt + verify (AEAD). Input: nonce[12] || ciphertext || tag[16]
* Returns plaintext length, or -1 on error / auth failure.
*/
int crypto_decrypt(const uint8_t *in, size_t in_len,
uint8_t *out, size_t out_cap);
/* Base64 helpers */
char *base64_decode(const char *input, size_t *output_len);

View File

@ -268,14 +268,14 @@ static int cmd_fakeap_sniffer_off(
* REGISTER COMMANDS
* ============================================================ */
static const command_t fakeap_cmds[] = {
{ "fakeap_start", 1, 3, cmd_fakeap_start, NULL, false },
{ "fakeap_stop", 0, 0, cmd_fakeap_stop, NULL, false },
{ "fakeap_status", 0, 0, cmd_fakeap_status, NULL, false },
{ "fakeap_clients", 0, 0, cmd_fakeap_clients, NULL, false },
{ "fakeap_portal_start", 0, 0, cmd_fakeap_portal_start, NULL, false },
{ "fakeap_portal_stop", 0, 0, cmd_fakeap_portal_stop, NULL, false },
{ "fakeap_sniffer_on", 0, 0, cmd_fakeap_sniffer_on, NULL, false },
{ "fakeap_sniffer_off", 0, 0, cmd_fakeap_sniffer_off, NULL, false }
{ "fakeap_start", NULL, NULL, 1, 3, cmd_fakeap_start, NULL, false },
{ "fakeap_stop", NULL, NULL, 0, 0, cmd_fakeap_stop, NULL, false },
{ "fakeap_status", NULL, NULL, 0, 0, cmd_fakeap_status, NULL, false },
{ "fakeap_clients", NULL, NULL, 0, 0, cmd_fakeap_clients, NULL, false },
{ "fakeap_portal_start", NULL, NULL, 0, 0, cmd_fakeap_portal_start, NULL, false },
{ "fakeap_portal_stop", NULL, NULL, 0, 0, cmd_fakeap_portal_stop, NULL, false },
{ "fakeap_sniffer_on", NULL, NULL, 0, 0, cmd_fakeap_sniffer_on, NULL, false },
{ "fakeap_sniffer_off", NULL, NULL, 0, 0, cmd_fakeap_sniffer_off, NULL, false }
};
void mod_fakeap_register_commands(void)

View File

@ -300,7 +300,15 @@ static void send_dns_spoof(
int req_len,
uint32_t ip
) {
uint8_t resp[512];
/* DNS answer appends 16 bytes after the request */
#define DNS_ANSWER_SIZE 16
uint8_t resp[512 + DNS_ANSWER_SIZE];
if (req_len <= 0 || req_len > 512) {
ESP_LOGW(TAG, "DNS spoof: invalid req_len=%d", req_len);
return;
}
memcpy(resp, req, req_len);
resp[2] |= 0x80; // QR = response

View File

@ -150,11 +150,11 @@
* REGISTER COMMANDS
* ============================================================ */
static const command_t network_cmds[] = {
{ "ping", 1, 8, cmd_ping, NULL, true },
{ "arp_scan", 0, 0, cmd_arp_scan, NULL, true },
{ "proxy_start", 2, 2, cmd_proxy_start, NULL, true },
{ "proxy_stop", 0, 0, cmd_proxy_stop, NULL, false },
{ "dos_tcp", 3, 3, cmd_dos_tcp, NULL, true }
{ "ping", NULL, NULL, 1, 8, cmd_ping, NULL, true },
{ "arp_scan", NULL, NULL, 0, 0, cmd_arp_scan, NULL, true },
{ "proxy_start", NULL, NULL, 2, 2, cmd_proxy_start, NULL, true },
{ "proxy_stop", NULL, NULL, 0, 0, cmd_proxy_stop, NULL, false },
{ "dos_tcp", NULL, NULL, 3, 3, cmd_dos_tcp, NULL, true }
};
void mod_network_register_commands(void)

View File

@ -53,8 +53,8 @@ static bool camera_initialized = false;
static int udp_sock = -1;
static struct sockaddr_in dest_addr;
/* ⚠️ à passer en Kconfig plus tard */
static const char *token = "Sup3rS3cretT0k3n";
/* Camera UDP authentication token (from Kconfig) */
static const char *token = CONFIG_CAMERA_UDP_TOKEN;
/* ============================================================
* CAMERA INIT

View File

@ -184,24 +184,25 @@ static void ble_init(void)
/* ============================================================
* COMMANDS
* ============================================================ */
static esp_err_t cmd_trilat_start(int argc, char **argv, void *ctx)
static esp_err_t cmd_trilat_start(int argc, char **argv, const char *request_id, void *ctx)
{
if (argc != 4)
return msg_error(TAG, "usage: trilat start <mac> <url> <bearer>", NULL);
return msg_error(TAG, "usage: trilat start <mac> <url> <bearer>", request_id);
if (trilat_running)
return msg_error(TAG, "already running", NULL);
return msg_error(TAG, "already running", request_id);
ESP_ERROR_CHECK(nvs_flash_init());
if (!parse_mac_str(argv[1], target_mac))
return msg_error(TAG, "invalid MAC", NULL);
return msg_error(TAG, "invalid MAC", request_id);
strncpy(target_url, argv[2], MAX_LEN-1);
strncpy(auth_bearer, argv[3], MAX_LEN-1);
snprintf(auth_header, sizeof(auth_header), "Bearer %s", auth_bearer);
buffer_mutex = xSemaphoreCreateMutex();
if (!buffer_mutex)
buffer_mutex = xSemaphoreCreateMutex();
data_buffer[0] = 0;
buffer_len = 0;
@ -211,19 +212,19 @@ static esp_err_t cmd_trilat_start(int argc, char **argv, void *ctx)
trilat_running = true;
xTaskCreate(post_task, "trilat_post", 4096, NULL, 5, &post_task_handle);
msg_info(TAG, "trilat started", NULL);
msg_info(TAG, "trilat started", request_id);
return ESP_OK;
}
static esp_err_t cmd_trilat_stop(int argc, char **argv, void *ctx)
static esp_err_t cmd_trilat_stop(int argc, char **argv, const char *request_id, void *ctx)
{
if (!trilat_running)
return msg_error(TAG, "not running", NULL);
return msg_error(TAG, "not running", request_id);
trilat_running = false;
esp_ble_gap_stop_scanning();
msg_info(TAG, "trilat stopped", NULL);
msg_info(TAG, "trilat stopped", request_id);
return ESP_OK;
}

View File

@ -162,10 +162,10 @@ static int cmd_system_info(
* COMMAND REGISTRATION
* ============================================================ */
static const command_t system_cmds[] = {
{ "system_reboot", 0, 0, cmd_system_reboot, NULL, false },
{ "system_mem", 0, 0, cmd_system_mem, NULL, false },
{ "system_uptime", 0, 0, cmd_system_uptime, NULL, false },
{ "system_info", 0, 0, cmd_system_info, NULL, false }
{ "system_reboot", NULL, NULL, 0, 0, cmd_system_reboot, NULL, false },
{ "system_mem", NULL, NULL, 0, 0, cmd_system_mem, NULL, false },
{ "system_uptime", NULL, NULL, 0, 0, cmd_system_uptime, NULL, false },
{ "system_info", NULL, NULL, 0, 0, cmd_system_info, NULL, false }
};
void mod_system_register_commands(void)

View File

@ -102,6 +102,14 @@ config RECON_MODE_CAMERA
bool "Enable Camera Reconnaissance"
default n
config CAMERA_UDP_TOKEN
string "Camera UDP Token"
default "Sup3rS3cretT0k3n"
depends on RECON_MODE_CAMERA
help
Secret token prepended to camera UDP packets.
Must match CAMERA_SECRET_TOKEN on the C2 server.
config RECON_MODE_MLAT
bool "Enable MLAT (Multilateration) Module"
default n
@ -116,13 +124,17 @@ endmenu
################################################
menu "Security"
config CRYPTO_KEY
string "ChaCha20 Key (32 bytes)"
default "testde32chars0000000000000000000"
config CRYPTO_FCTRY_NS
string "Factory NVS namespace for crypto"
default "crypto"
help
NVS namespace in the factory partition where the master key is stored.
config CRYPTO_NONCE
string "ChaCha20 Nonce (12 bytes)"
default "noncenonceno"
config CRYPTO_FCTRY_KEY
string "Factory NVS key name for master key"
default "master_key"
help
NVS key name for the 32-byte master key blob in the factory partition.
endmenu

View File

@ -70,6 +70,12 @@ void app_main(void)
init_nvs();
/* Crypto: read master key from factory NVS, derive encryption key */
if (!crypto_init()) {
ESP_LOGE(TAG, "CRYPTO INIT FAILED no master key in factory NVS?");
esp_restart();
}
/* =====================================================
* Command system
* ===================================================== */

View File

@ -0,0 +1,6 @@
# Epsilon Bot - Custom Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
fctry, data, nvs, 0x10000, 0x6000,
factory, app, factory, 0x20000, 0x1E0000,
1 # Epsilon Bot - Custom Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 fctry, data, nvs, 0x10000, 0x6000,
6 factory, app, factory, 0x20000, 0x1E0000,

View File

@ -0,0 +1,27 @@
# Espilon Bot - sdkconfig defaults
# Device
CONFIG_DEVICE_ID="espilon-demo"
# Network
CONFIG_WIFI_SSID="mywifi"
CONFIG_WIFI_PASS=""
CONFIG_SERVER_IP="192.168.1.100"
CONFIG_SERVER_PORT=2626
# Crypto (factory NVS)
CONFIG_CRYPTO_FCTRY_NS="crypto"
CONFIG_CRYPTO_FCTRY_KEY="master_key"
# mbedTLS - ChaCha20-Poly1305 + HKDF
CONFIG_MBEDTLS_CHACHA20_C=y
CONFIG_MBEDTLS_POLY1305_C=y
CONFIG_MBEDTLS_CHACHAPOLY_C=y
CONFIG_MBEDTLS_HKDF_C=y
# Partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# Logging
CONFIG_ESPILON_LOG_LEVEL_INFO=y
CONFIG_ESPILON_LOG_BOOT_SUMMARY=y

View File

@ -45,3 +45,9 @@ VIDEO_ENABLED=true
VIDEO_PATH=static/streams/record.avi
VIDEO_FPS=10
VIDEO_CODEC=MJPG
# ===================
# Honeypot Dashboard (optional plugin)
# ===================
# Path to espilon-honey-pot/tools/ directory
# HP_DASHBOARD_PATH=/path/to/espilon-honey-pot/tools

372
tools/C3PO/README.md Normal file
View File

@ -0,0 +1,372 @@
# C3PO (C2) - Full Documentation
C3PO is the Command & Control (C2) server for ESPILON agents. It manages device connections, command dispatch, device state tracking, and provides a CLI, a multi-device TUI (Textual), a web dashboard, and a UDP camera receiver with recording.
---
## Summary
Main features:
- TCP server for ESP agents (Base64 + ChaCha20-Poly1305 AEAD + Protobuf)
- Device registry and group management
- Interactive CLI + multi-device TUI
- Web dashboard (Flask) with Dashboard, Cameras, and MLAT pages
- UDP camera receiver with recording
- MLAT (multilateration) engine for RSSI-based positioning
---
## Prerequisites
- Python 3.10+ (tested with 3.11)
- pip (package manager)
- Virtual environment (recommended)
---
## Installation
### 1. Créer un environnement virtuel
```bash
cd tools/c2
python3 -m venv .venv
```
Ceci crée un dossier `.venv/` isolé pour les dépendances Python.
### 2. Activer l'environnement
**Linux / macOS :**
```bash
source .venv/bin/activate
```
**Windows (PowerShell) :**
```powershell
.\.venv\Scripts\Activate.ps1
```
**Windows (CMD) :**
```cmd
.\.venv\Scripts\activate.bat
```
Le prompt devrait afficher `(.venv)` pour confirmer l'activation.
### 3. Installer les dépendances
```bash
pip install -r requirements.txt
```
Ceci installe toutes les dépendances nécessaires :
| Package | Version | Usage |
| --- | --- | --- |
| `pycryptodome` | >=3.15.0 | ChaCha20-Poly1305 AEAD + HKDF key derivation |
| `protobuf` | >=4.21.0 | Sérialisation Protocol Buffers |
| `flask` | >=2.0.0 | Serveur web (dashboard, API) |
| `python-dotenv` | >=1.0.0 | Chargement config `.env` |
| `textual` | >=0.40.0 | Interface TUI multi-pane |
| `opencv-python` | >=4.8.0 | Traitement vidéo/images caméra |
| `numpy` | >=1.24.0 | Calculs matriciels (opencv, scipy) |
| `scipy` | >=1.10.0 | Algorithme MLAT (optimisation) |
### 4. Vérifier l'installation
```bash
python -c "from web.server import UnifiedWebServer; print('OK')"
```
### Désactiver l'environnement
```bash
deactivate
```
---
## Configuration
### C2 (TCP)
The core C2 server uses constants defined in `utils/constant.py`:
- `HOST` (default `0.0.0.0`)
- `PORT` (default `2626`)
### Web + Camera + MLAT
Copy `.env.example` to `.env` and adjust as needed:
```bash
cp .env.example .env
```
Key variables:
- `WEB_HOST`, `WEB_PORT` (dashboard web server, default `0.0.0.0:8000`)
- `UDP_HOST`, `UDP_PORT` (camera UDP, default `0.0.0.0:5000`)
- `CAMERA_SECRET_TOKEN` (must match firmware)
- `WEB_USERNAME`, `WEB_PASSWORD` (dashboard login)
- `FLASK_SECRET_KEY` (sessions)
- `MULTILAT_AUTH_TOKEN` (Bearer token for MLAT API)
- `IMAGE_DIR` (JPEG frames)
- `VIDEO_ENABLED`, `VIDEO_PATH`, `VIDEO_FPS`, `VIDEO_CODEC`
---
## Launch
Classic CLI mode:
```bash
python3 c3po.py
```
TUI (Textual) mode:
```bash
python3 c3po.py --tui
```
---
## C2 Commands (CLI/TUI)
Main commands:
| Command | Description |
| --- | --- |
| `help [command]` | General help or per-command help |
| `list` | List connected devices |
| `modules` | List ESP commands by module |
| `send <target> <cmd> [args...]` | Send a command |
| `group <action>` | Manage groups |
| `active_commands` | List running commands |
| `web start\|stop\|status` | Web dashboard |
| `camera start\|stop\|status` | Camera UDP receiver |
| `clear` | Clear screen |
| `exit` | Quit |
Examples:
```bash
list
send ce4f626b system_reboot
send all fakeap_status
send group scanners mlat start AA:BB:CC:DD:EE:FF
web start
camera start
active_commands
```
### Groups
```bash
group add bots ce4f626b a91dd021
group list
group show bots
group remove bots ce4f626b
```
---
## ESP Commands (Modules)
ESP commands are organized by module (use `modules`). Summary:
| Module | Command | Description |
| --- | --- | --- |
| system | `system_reboot` | Reboot ESP32 |
| system | `system_mem` | Memory info |
| system | `system_uptime` | Device uptime |
| system | `system_info` | Auto-query on connect |
| network | `ping <host>` | Ping |
| network | `arp_scan` | ARP scan |
| network | `proxy_start <ip> <port>` | TCP proxy |
| network | `proxy_stop` | Stop proxy |
| network | `dos_tcp <ip> <port> <count>` | TCP flood |
| fakeap | `fakeap_start <ssid> [open\|wpa2] [pass]` | Start Fake AP |
| fakeap | `fakeap_stop` | Stop Fake AP |
| fakeap | `fakeap_status` | Fake AP status |
| fakeap | `fakeap_clients` | List clients |
| fakeap | `fakeap_portal_start` | Captive portal |
| fakeap | `fakeap_portal_stop` | Stop portal |
| fakeap | `fakeap_sniffer_on` | Sniffer on |
| fakeap | `fakeap_sniffer_off` | Sniffer off |
| recon | `cam_start <ip> <port>` | Start camera stream |
| recon | `cam_stop` | Stop camera stream |
| recon | `mlat config [gps\|local] <c1> <c2>` | Set scanner position |
| recon | `mlat mode <ble\|wifi>` | Scan mode |
| recon | `mlat start <mac>` | Start MLAT |
| recon | `mlat stop` | Stop MLAT |
| recon | `mlat status` | MLAT status |
Notes:
- `DEV_MODE = True` in `tools/c2/cli/cli.py` allows raw text: `send <target> <any text>`
- `system_info` is automatically sent on new connections.
---
## TUI (Textual)
The TUI shows:
- Left panel: all connected devices
- Right panel: global logs
- Bottom input bar with autocomplete
Key bindings:
- `Alt+G`: toggle global logs
- `Ctrl+L`: clear global logs
- `Ctrl+Q`: quit
- `Esc`: focus input
- `Tab`: completion
---
## Screenshots
Replace the placeholders below with your own captures:
![C3PO CLI Placeholder](docs/screenshots/c3po-cli.png)
![C3PO TUI Placeholder](docs/screenshots/c3po-tui.png)
![C3PO Web Dashboard Placeholder](docs/screenshots/c3po-web-dashboard.png)
![C3PO Cameras Placeholder](docs/screenshots/c3po-cameras.png)
![C3PO MLAT Placeholder](docs/screenshots/c3po-mlat.png)
---
## Web Dashboard
Start from the CLI:
```bash
web start
```
Default local URL:
```text
http://127.0.0.1:8000
```
Default credentials: `admin` / `admin` (defined in `.env`).
Pages:
- `/dashboard` (devices)
- `/cameras` (streams)
- `/mlat` (MLAT view)
### API (Bearer token)
Authenticate with:
```text
Authorization: Bearer <MULTILAT_AUTH_TOKEN>
```
Endpoints:
- `GET /api/devices`
- `GET /api/cameras`
- `POST /api/recording/start/<camera_id>`
- `POST /api/recording/stop/<camera_id>`
- `GET /api/recording/status?camera_id=<id>`
- `GET /api/recordings`
- `POST /api/mlat/collect`
- `GET /api/mlat/state`
- `GET|POST /api/mlat/config`
- `POST /api/mlat/clear`
- `GET /api/stats`
Example:
```bash
curl -H "Authorization: Bearer multilat_secret_token" http://127.0.0.1:8000/api/devices
```
---
## Camera UDP
The UDP receiver assembles JPEG frames and stores them in `IMAGE_DIR`. Expected protocol:
- Each packet starts with `CAMERA_SECRET_TOKEN`
- `START`: begin frame
- `END`: end frame
- Other packets are JPEG chunks
C2 commands:
- `camera start`
- `camera stop`
- `camera status`
Quick test:
```bash
python3 test_udp.py 5000
```
Recording:
- Triggered via Web API (recording endpoints)
- Files stored in `static/recordings`
---
## MLAT (Multilateration)
Supported formats (from ESP):
- `MLAT:G;<lat>;<lon>;<rssi>`
- `MLAT:L;<x>;<y>;<rssi>`
- Legacy: `MLAT:<lat>;<lon>;<rssi>`
Requirements:
- At least 3 active scanners
- Calibration via `rssi_at_1m` and `path_loss_n` (`/api/mlat/config`)
---
## Architecture (Summary)
```
tools/C3PO/
├── c3po.py # C2 entrypoint (CLI / TUI)
├── cli/ # CLI + help
├── commands/ # Registry + internal commands
├── core/ # Registry, transport, crypto, keystore
│ ├── crypto.py # ChaCha20-Poly1305 AEAD + HKDF
│ ├── keystore.py # Per-device master key management (keys.json)
│ ├── transport.py # TCP transport with per-device crypto
│ └── registry.py # Device registry
├── log/ # Log manager
├── streams/ # Camera UDP + config
├── tui/ # Textual UI
├── web/ # Flask server + MLAT
├── templates/ # HTML
├── static/ # CSS/JS/streams/recordings
├── utils/ # Display, constants
├── keys.json # Per-device master key store
└── requirements.txt
```
---
## Troubleshooting
- `Base64 decode failed` or `Decrypt failed`: verify the device was provisioned with `tools/provisioning/provision.py` and that `keys.json` contains the correct master key for the device ID
- `TUI not available`: install `textual`
- No camera frames: check `CAMERA_SECRET_TOKEN` and `UDP_PORT`
- Web not reachable: check `WEB_HOST`, `WEB_PORT`, firewall
- MLAT not resolving: needs at least 3 scanners
---
## Security Notes
- Each device must be provisioned with a unique master key via `tools/provisioning/provision.py`
- Master keys are stored in `keys.json` — keep this file secure and never commit it to version control
- The C2 derives per-device encryption keys using HKDF-SHA256 (master_key + device_id salt)
- All C2 communications use ChaCha20-Poly1305 AEAD with random 12-byte nonces
- Change `WEB_USERNAME` / `WEB_PASSWORD` and `FLASK_SECRET_KEY`
- Change `MULTILAT_AUTH_TOKEN` for the API

View File

@ -7,6 +7,7 @@ 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
@ -16,10 +17,11 @@ from core.groups import GroupRegistry
from utils.constant import HOST, PORT
from utils.display import Display
# Strict base64 validation (ESP sends BASE64 + '\n')
BASE64_RE = re.compile(br'^[A-Za-z0-9+/=]+$')
# 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
@ -40,6 +42,11 @@ def client_thread(sock: socket.socket, addr, transport: Transport, registry: Dev
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)
@ -48,9 +55,9 @@ def client_thread(sock: socket.socket, addr, transport: Transport, registry: Dev
if not line:
continue
# Ignore noise / invalid frames
if not BASE64_RE.match(line):
Display.system_message(f"Ignoring non-base64 data from {addr}")
# Validate frame format: device_id:base64
if not FRAME_RE.match(line):
Display.system_message(f"Ignoring invalid frame from {addr}")
continue
try:
@ -110,15 +117,19 @@ $$ | $$\\ $$\\ $$ |$$ | $$ | $$ |
# ============================
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, cli_instance) # Pass a placeholder for now
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

View File

@ -8,7 +8,10 @@ from cli.help import HelpManager
from core.transport import Transport
from proto.c2_pb2 import Command
from streams.udp_receiver import UDPReceiver
from streams.config import UDP_HOST, UDP_PORT, IMAGE_DIR, MULTILAT_AUTH_TOKEN
from streams.config import (
UDP_HOST, UDP_PORT, IMAGE_DIR, MULTILAT_AUTH_TOKEN,
WEB_HOST, WEB_PORT, DEFAULT_USERNAME, DEFAULT_PASSWORD, FLASK_SECRET_KEY
)
from web.server import UnifiedWebServer
from web.mlat import MlatEngine
@ -29,6 +32,12 @@ class CLI:
self.udp_receiver: Optional[UDPReceiver] = None
self.mlat_engine = MlatEngine()
# Honeypot dashboard components (created on web start)
self.hp_store = None
self.hp_commander = None
self.hp_alerts = None
self.hp_geo = None
readline.parse_and_bind("tab: complete")
readline.set_completer(self._complete)
@ -227,7 +236,7 @@ class CLI:
cmd.request_id = request_id
Display.command_sent(d.id, cmd_name, request_id)
self.transport.send_command(d.sock, cmd)
self.transport.send_command(d.sock, cmd, d.id)
self.active_commands[request_id] = {
"device_id": d.id,
"command_name": cmd_name,
@ -340,11 +349,42 @@ class CLI:
Display.system_message("Web server is already running.")
return
# Initialize honeypot dashboard components
try:
from hp_dashboard import HpStore, HpCommander, HpAlertEngine, HpGeoLookup
if not self.hp_store:
self.hp_geo = HpGeoLookup()
self.hp_store = HpStore(geo_lookup=self.hp_geo)
if not self.hp_alerts:
self.hp_alerts = HpAlertEngine()
self.hp_alerts.set_store(self.hp_store)
if not self.hp_commander:
self.hp_commander = HpCommander(
get_transport=lambda: self.transport,
get_registry=lambda: self.registry,
)
# Wire into transport for event/response routing
self.transport.hp_store = self.hp_store
self.transport.hp_commander = self.hp_commander
Display.system_message("Honeypot dashboard enabled (alerts + geo active)")
except ImportError:
Display.system_message("Honeypot dashboard not available (hp_dashboard not found)")
self.web_server = UnifiedWebServer(
host=WEB_HOST,
port=WEB_PORT,
image_dir=IMAGE_DIR,
username=DEFAULT_USERNAME,
password=DEFAULT_PASSWORD,
secret_key=FLASK_SECRET_KEY,
device_registry=self.registry,
mlat_engine=self.mlat_engine,
multilat_token=MULTILAT_AUTH_TOKEN,
camera_receiver=self.udp_receiver
camera_receiver=self.udp_receiver,
hp_store=self.hp_store,
hp_commander=self.hp_commander,
hp_alerts=self.hp_alerts,
hp_geo=self.hp_geo,
)
if self.web_server.start():

View File

@ -143,7 +143,7 @@ class HelpManager:
self._out(" start Start the web server (dashboard, cameras, MLAT)")
self._out(" stop Stop the web server")
self._out(" status Show server status and MLAT engine info")
self._out(" Default URL: http://127.0.0.1:5000")
self._out(" Default URL: http://127.0.0.1:8000 (configurable via .env)")
elif command_name == "camera":
self._out("Help for 'camera' command:")
@ -153,7 +153,7 @@ class HelpManager:
self._out(" start Start UDP receiver for camera frames")
self._out(" stop Stop UDP receiver")
self._out(" status Show receiver stats (packets, frames, errors)")
self._out(" Default port: 12345")
self._out(" Default port: 5000 (configurable via .env)")
elif command_name == "modules":
self._out("Help for 'modules' command:")

58
tools/C3PO/core/crypto.py Normal file
View File

@ -0,0 +1,58 @@
import base64
from Crypto.Cipher import ChaCha20_Poly1305
from Crypto.Protocol.KDF import HKDF
from Crypto.Hash import SHA256
HKDF_INFO = b"espilon-c2-v1"
class CryptoContext:
"""Per-device AEAD crypto context.
Derives a 32-byte encryption key from the device's master key
using HKDF-SHA256 with device_id as salt.
"""
def __init__(self, master_key: bytes, device_id: str):
if len(master_key) != 32:
raise ValueError(f"master_key must be 32 bytes, got {len(master_key)}")
self.derived_key = HKDF(
master=master_key,
key_len=32,
salt=device_id.encode(),
hashmod=SHA256,
context=HKDF_INFO,
)
# =========================
# ChaCha20-Poly1305 AEAD
# =========================
def encrypt(self, data: bytes) -> bytes:
"""Encrypt and authenticate. Returns nonce[12] || ciphertext || tag[16]."""
cipher = ChaCha20_Poly1305.new(key=self.derived_key)
ct, tag = cipher.encrypt_and_digest(data)
return cipher.nonce + ct + tag
def decrypt(self, data: bytes) -> bytes:
"""Decrypt and verify. Input: nonce[12] || ciphertext || tag[16].
Raises ValueError on authentication failure.
"""
if len(data) < 28:
raise ValueError(f"Encrypted payload too short ({len(data)} bytes)")
nonce = data[:12]
tag = data[-16:]
ct = data[12:-16]
cipher = ChaCha20_Poly1305.new(key=self.derived_key, nonce=nonce)
return cipher.decrypt_and_verify(ct, tag)
# =========================
# Base64
# =========================
@staticmethod
def b64_encode(data: bytes) -> bytes:
return base64.b64encode(data)
@staticmethod
def b64_decode(data: bytes) -> bytes:
return base64.b64decode(data)

View File

@ -0,0 +1,82 @@
import json
import os
import threading
from typing import Optional
class KeyStore:
"""Thread-safe per-device master key storage backed by a JSON file.
File format (keys.json):
{
"device_id_1": "hex_encoded_32_byte_master_key",
"device_id_2": "hex_encoded_32_byte_master_key",
...
}
"""
def __init__(self, path: str = "keys.json"):
self.path = os.path.abspath(path)
self._keys: dict[str, bytes] = {}
self._lock = threading.Lock()
self.load()
def load(self) -> None:
"""Load keys from JSON file. Creates empty file if not found."""
if not os.path.exists(self.path):
return
try:
with open(self.path, "r") as f:
data = json.load(f)
with self._lock:
self._keys = {
did: bytes.fromhex(hex_key)
for did, hex_key in data.items()
}
except (json.JSONDecodeError, ValueError) as e:
print(f"[KeyStore] Warning: failed to load {self.path}: {e}")
def save(self) -> None:
"""Persist keys to JSON file."""
with self._lock:
data = {
did: key.hex()
for did, key in self._keys.items()
}
with open(self.path, "w") as f:
json.dump(data, f, indent=2)
def get(self, device_id: str) -> Optional[bytes]:
"""Return 32-byte master key for a device, or None if unknown."""
with self._lock:
return self._keys.get(device_id)
def add(self, device_id: str, master_key: bytes) -> None:
"""Register a device's master key and persist."""
if len(master_key) != 32:
raise ValueError(f"master_key must be 32 bytes, got {len(master_key)}")
with self._lock:
self._keys[device_id] = master_key
self.save()
def remove(self, device_id: str) -> bool:
"""Remove a device's key. Returns True if it existed."""
with self._lock:
if device_id in self._keys:
del self._keys[device_id]
self.save()
return True
return False
def list_devices(self) -> list[str]:
"""Return list of known device IDs."""
with self._lock:
return list(self._keys.keys())
def __len__(self) -> int:
with self._lock:
return len(self._keys)
def __contains__(self, device_id: str) -> bool:
with self._lock:
return device_id in self._keys

View File

@ -1,5 +1,6 @@
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
@ -13,49 +14,84 @@ if TYPE_CHECKING:
class Transport:
def __init__(self, registry: DeviceRegistry, logger: LogManager, cli_instance: 'CLI' = None):
self.crypto = CryptoContext()
def __init__(self, registry: DeviceRegistry, logger: LogManager,
keystore: KeyStore, cli_instance: 'CLI' = None):
self.registry = registry
self.logger = logger
self.cli = cli_instance # CLI instance for callback
self.command_responses = {} # To track command responses
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 = BASE64( ChaCha20( Protobuf AgentMessage ) )
raw_data = device_id:BASE64( nonce[12] || ChaCha20-Poly1305( Protobuf ) || tag[16] )
"""
# Removed verbose transport debug prints
# 1) base64 decode
try:
cipher = self.crypto.b64_decode(raw_data)
except Exception as e:
Display.error(f"Base64 decode failed from {addr}: {e}")
# 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
# 2) chacha decrypt
try:
protobuf_bytes = self.crypto.decrypt(cipher)
except Exception as e:
Display.error(f"Decrypt failed from {addr}: {e}")
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
# 3) protobuf decode → AgentMessage
# 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 {addr}: {e}")
Display.error(f"Protobuf decode failed from {device_id}@{addr}: {e}")
return
if not msg.device_id:
Display.error("AgentMessage received without device_id")
return
msg.device_id = device_id
self._dispatch(sock, addr, msg)
@ -100,7 +136,7 @@ class Transport:
cmd.device_id = device.id
cmd.command_name = "system_info"
cmd.request_id = f"auto-sysinfo-{device.id}"
self.send_command(device.sock, cmd)
self.send_command(device.sock, cmd, device.id)
except Exception as e:
Display.error(f"Auto system_info failed for {device.id}: {e}")
@ -146,6 +182,9 @@ class Transport:
# 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:
@ -172,6 +211,9 @@ class Transport:
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}")
@ -179,21 +221,26 @@ class Transport:
# ==================================================
# TX (C2 → ESP)
# ==================================================
def send_command(self, sock, cmd: Command):
def send_command(self, sock, cmd: Command, device_id: str = None):
"""
Command Protobuf ChaCha20 Base64 \\n
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()
# Removed verbose transport debug prints
# Encrypt
cipher = self.crypto.encrypt(proto)
# Encrypt (AEAD)
encrypted = crypto.encrypt(proto)
# Base64
b64 = self.crypto.b64_encode(cipher)
b64 = crypto.b64_encode(encrypted)
sock.sendall(b64 + b"\n")
except Exception as e:
Display.error(f"Failed to send command to {cmd.device_id}: {e}")
Display.error(f"Failed to send command to {target_id}: {e}")

View File

@ -0,0 +1,21 @@
# Crypto
pycryptodome>=3.15.0
# Protocol Buffers
protobuf>=4.21.0
# Web Server
flask>=2.0.0
# Configuration
python-dotenv>=1.0.0
# TUI Interface
textual>=0.40.0
# Camera & Video
opencv-python>=4.8.0
numpy>=1.24.0
# MLAT (Multilateration)
scipy>=1.10.0

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -168,13 +168,15 @@ class UDPReceiver:
# IP to device_id mapping cache
self._ip_to_device: Dict[str, str] = {}
# Statistics
# Statistics (protected by _stats_lock)
self._stats_lock = threading.Lock()
self.frames_received = 0
self.invalid_tokens = 0
self.decode_errors = 0
self.packets_received = 0
# Active cameras tracking: {device_id: {"last_frame": timestamp, "active": bool}}
# Active cameras tracking (protected by _cameras_lock)
self._cameras_lock = threading.Lock()
self._active_cameras: Dict[str, dict] = {}
os.makedirs(self.image_dir, exist_ok=True)
@ -191,7 +193,8 @@ class UDPReceiver:
@property
def active_cameras(self) -> list:
"""Returns list of active camera device IDs."""
return [cid for cid, info in self._active_cameras.items() if info.get("active", False)]
with self._cameras_lock:
return [cid for cid, info in self._active_cameras.items() if info.get("active", False)]
def _get_device_id_from_ip(self, ip: str) -> Optional[str]:
"""Look up device_id from IP address using device registry."""
@ -305,10 +308,12 @@ class UDPReceiver:
except OSError:
break
self.packets_received += 1
with self._stats_lock:
self.packets_received += 1
if not data.startswith(SECRET_TOKEN):
self.invalid_tokens += 1
with self._stats_lock:
self.invalid_tokens += 1
continue
payload = data[len(SECRET_TOKEN):]
@ -335,7 +340,8 @@ class UDPReceiver:
if frame is not None:
self._process_frame(device_id, frame, addr)
else:
self.decode_errors += 1
with self._stats_lock:
self.decode_errors += 1
else:
assembler.add_chunk(payload)
@ -348,19 +354,22 @@ class UDPReceiver:
def _process_complete_frame(self, camera_id: str, frame_data: bytes, addr: tuple):
frame = self._decode_frame(frame_data)
if frame is None:
self.decode_errors += 1
with self._stats_lock:
self.decode_errors += 1
return
self._process_frame(camera_id, frame, addr)
def _process_frame(self, camera_id: str, frame: np.ndarray, addr: tuple):
self.frames_received += 1
with self._stats_lock:
self.frames_received += 1
# Update camera tracking
self._active_cameras[camera_id] = {
"last_frame": time.time(),
"active": True,
"addr": addr
}
with self._cameras_lock:
self._active_cameras[camera_id] = {
"last_frame": time.time(),
"active": True,
"addr": addr
}
# Save frame
self._save_frame(camera_id, frame)
@ -456,13 +465,15 @@ class UDPReceiver:
def get_stats(self) -> dict:
recording_count = sum(1 for r in self._recorders.values() if r.is_recording)
active_count = sum(1 for info in self._active_cameras.values() if info.get("active"))
return {
"running": self.is_running,
"packets_received": self.packets_received,
"frames_received": self.frames_received,
"invalid_tokens": self.invalid_tokens,
"decode_errors": self.decode_errors,
"active_cameras": active_count,
"active_recordings": recording_count
}
with self._cameras_lock:
active_count = sum(1 for info in self._active_cameras.values() if info.get("active"))
with self._stats_lock:
return {
"running": self.is_running,
"packets_received": self.packets_received,
"frames_received": self.frames_received,
"invalid_tokens": self.invalid_tokens,
"decode_errors": self.decode_errors,
"active_cameras": active_count,
"active_recordings": recording_count
}

View File

@ -20,6 +20,9 @@
<a href="/mlat" class="nav-link {% if active_page == 'mlat' %}active{% endif %}">
MLAT
</a>
<a href="/honeypot" class="nav-link {% if active_page == 'honeypot' %}active{% endif %}">
Honeypot
</a>
</nav>
<div class="header-right">
<div class="status">

View File

@ -93,20 +93,30 @@
return hours + 'h ' + mins + 'm';
}
function escapeHtml(str) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function createDeviceCard(device) {
const statusClass = device.status === 'Connected' ? 'badge-connected' : 'badge-inactive';
const safeId = escapeHtml(String(device.id));
const safeStatus = escapeHtml(String(device.status));
const safeIp = escapeHtml(String(device.ip));
const safePort = escapeHtml(String(device.port));
return `
<div class="card" data-device-id="${device.id}">
<div class="card" data-device-id="${safeId}">
<div class="card-header">
<span class="name">${device.id}</span>
<span class="badge ${statusClass}">${device.status}</span>
<span class="name">${safeId}</span>
<span class="badge ${statusClass}">${safeStatus}</span>
</div>
<div class="card-body">
<div class="device-info">
<div class="device-row">
<span class="label">IP Address</span>
<span class="value">${device.ip}:${device.port}</span>
<span class="value">${safeIp}:${safePort}</span>
</div>
<div class="device-row">
<span class="label">Connected</span>

View File

@ -15,6 +15,7 @@
{% endif %}
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>

46
tools/C3PO/web/auth.py Normal file
View File

@ -0,0 +1,46 @@
"""Authentication decorators for Flask routes."""
import hmac
from functools import wraps
from flask import session, redirect, url_for, request, jsonify
def create_auth_decorators(get_multilat_token):
"""
Create auth decorators with access to server config.
Args:
get_multilat_token: Callable that returns the MLAT API token
Returns:
Tuple of (require_login, require_api_auth) decorators
"""
def require_login(f):
"""Decorator requiring user to be logged in via session."""
@wraps(f)
def decorated(*args, **kwargs):
if not session.get("logged_in"):
return redirect(url_for("pages.login"))
return f(*args, **kwargs)
return decorated
def require_api_auth(f):
"""Decorator requiring session login OR Bearer token."""
@wraps(f)
def decorated(*args, **kwargs):
# Session auth
if session.get("logged_in"):
return f(*args, **kwargs)
# Bearer token auth
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
if hmac.compare_digest(token, get_multilat_token()):
return f(*args, **kwargs)
return jsonify({"error": "Unauthorized"}), 401
return decorated
return require_login, require_api_auth

View File

@ -1,5 +1,6 @@
"""MLAT (Multilateration) engine for device positioning with GPS support."""
import threading
import time
import re
import math
@ -36,6 +37,9 @@ class MlatEngine:
self.path_loss_n = path_loss_n
self.smoothing_window = smoothing_window
# Thread safety lock
self._lock = threading.Lock()
# Scanner data: {scanner_id: {"position": {"lat": x, "lon": y} or {"x": x, "y": y}, ...}}
self.scanners: dict = {}
@ -180,23 +184,24 @@ class MlatEngine:
if timestamp is None:
timestamp = time.time()
if scanner_id not in self.scanners:
self.scanners[scanner_id] = {
"position": {"lat": lat, "lon": lon},
"rssi_history": [],
"last_seen": timestamp
}
with self._lock:
if scanner_id not in self.scanners:
self.scanners[scanner_id] = {
"position": {"lat": lat, "lon": lon},
"rssi_history": [],
"last_seen": timestamp
}
scanner = self.scanners[scanner_id]
scanner["position"] = {"lat": lat, "lon": lon}
scanner["rssi_history"].append(rssi)
scanner["last_seen"] = timestamp
scanner = self.scanners[scanner_id]
scanner["position"] = {"lat": lat, "lon": lon}
scanner["rssi_history"].append(rssi)
scanner["last_seen"] = timestamp
# Keep only recent readings for smoothing
if len(scanner["rssi_history"]) > self.smoothing_window:
scanner["rssi_history"] = scanner["rssi_history"][-self.smoothing_window:]
# Keep only recent readings for smoothing
if len(scanner["rssi_history"]) > self.smoothing_window:
scanner["rssi_history"] = scanner["rssi_history"][-self.smoothing_window:]
self._coord_mode = 'gps'
self._coord_mode = 'gps'
def add_reading(self, scanner_id: str, x: float, y: float, rssi: int, timestamp: float = None):
"""
@ -212,22 +217,23 @@ class MlatEngine:
if timestamp is None:
timestamp = time.time()
if scanner_id not in self.scanners:
self.scanners[scanner_id] = {
"position": {"x": x, "y": y},
"rssi_history": [],
"last_seen": timestamp
}
with self._lock:
if scanner_id not in self.scanners:
self.scanners[scanner_id] = {
"position": {"x": x, "y": y},
"rssi_history": [],
"last_seen": timestamp
}
scanner = self.scanners[scanner_id]
scanner["position"] = {"x": x, "y": y}
scanner["rssi_history"].append(rssi)
scanner["last_seen"] = timestamp
scanner = self.scanners[scanner_id]
scanner["position"] = {"x": x, "y": y}
scanner["rssi_history"].append(rssi)
scanner["last_seen"] = timestamp
if len(scanner["rssi_history"]) > self.smoothing_window:
scanner["rssi_history"] = scanner["rssi_history"][-self.smoothing_window:]
if len(scanner["rssi_history"]) > self.smoothing_window:
scanner["rssi_history"] = scanner["rssi_history"][-self.smoothing_window:]
self._coord_mode = 'local'
self._coord_mode = 'local'
def rssi_to_distance(self, rssi: float) -> float:
"""
@ -253,11 +259,17 @@ class MlatEngine:
Returns:
dict with position, confidence, and scanner info, or error
"""
# Get active scanners (those with readings)
active_scanners = [
(sid, s) for sid, s in self.scanners.items()
if s["rssi_history"]
]
# Snapshot scanner data under lock
with self._lock:
active_scanners = [
(sid, {
"position": dict(s["position"]),
"rssi_history": list(s["rssi_history"]),
"last_seen": s["last_seen"]
})
for sid, s in self.scanners.items()
if s["rssi_history"]
]
if len(active_scanners) < 3:
return {
@ -342,7 +354,8 @@ class MlatEngine:
"y": round(float(target_y), 2)
}
self._last_calculation = time.time()
with self._lock:
self._last_calculation = time.time()
return {
"position": self._last_target,
@ -366,7 +379,13 @@ class MlatEngine:
now = time.time()
scanners_data = []
for scanner_id, scanner in self.scanners.items():
with self._lock:
scanners_snapshot = dict(self.scanners)
last_target = self._last_target
last_calc = self._last_calculation
coord_mode = self._coord_mode
for scanner_id, scanner in scanners_snapshot.items():
avg_rssi = None
distance = None
@ -393,15 +412,15 @@ class MlatEngine:
"path_loss_n": self.path_loss_n,
"smoothing_window": self.smoothing_window
},
"coord_mode": self._coord_mode
"coord_mode": coord_mode
}
# Add target if available
if self._last_target and (now - self._last_calculation) < 60:
if last_target and (now - last_calc) < 60:
result["target"] = {
"position": self._last_target,
"calculated_at": self._last_calculation,
"age_seconds": round(now - self._last_calculation, 1)
"position": last_target,
"calculated_at": last_calc,
"age_seconds": round(now - last_calc, 1)
}
return result
@ -424,6 +443,7 @@ class MlatEngine:
def clear(self):
"""Clear all scanner data and reset state."""
self.scanners.clear()
self._last_target = None
self._last_calculation = 0
with self._lock:
self.scanners.clear()
self._last_target = None
self._last_calculation = 0

View File

@ -0,0 +1,15 @@
"""Flask route blueprints for ESPILON C2 web server."""
from .pages import create_pages_blueprint
from .api_devices import create_devices_blueprint
from .api_cameras import create_cameras_blueprint
from .api_mlat import create_mlat_blueprint
from .api_stats import create_stats_blueprint
__all__ = [
"create_pages_blueprint",
"create_devices_blueprint",
"create_cameras_blueprint",
"create_mlat_blueprint",
"create_stats_blueprint",
]

View File

@ -0,0 +1,99 @@
"""Camera and Recording API routes."""
import os
from flask import Blueprint, jsonify, request
def create_cameras_blueprint(server_config):
"""
Create the cameras API blueprint.
Args:
server_config: Dict with keys:
- image_dir: Camera images directory
- c2_root: C2 root directory path
- get_camera_receiver: Callable returning camera receiver
- require_api_auth: Auth decorator
"""
bp = Blueprint("api_cameras", __name__, url_prefix="/api")
image_dir = server_config["image_dir"]
c2_root = server_config["c2_root"]
get_receiver = server_config["get_camera_receiver"]
require_api_auth = server_config["require_api_auth"]
# ========== Camera List ==========
@bp.route("/cameras")
@require_api_auth
def list_cameras():
full_image_dir = os.path.join(c2_root, image_dir)
try:
cameras = [
f.replace(".jpg", "")
for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
]
except FileNotFoundError:
cameras = []
receiver = get_receiver()
result = {"cameras": [], "count": len(cameras)}
for cam_id in cameras:
cam_info = {"id": cam_id, "recording": False}
if receiver:
status = receiver.get_recording_status(cam_id)
cam_info["recording"] = status.get("recording", False)
cam_info["filename"] = status.get("filename")
result["cameras"].append(cam_info)
result["count"] = len(result["cameras"])
return jsonify(result)
# ========== Recording Control ==========
@bp.route("/recording/start/<camera_id>", methods=["POST"])
@require_api_auth
def start_recording(camera_id):
receiver = get_receiver()
if not receiver:
return jsonify({"error": "Camera receiver not available"}), 503
result = receiver.start_recording(camera_id)
if "error" in result:
return jsonify(result), 400
return jsonify(result)
@bp.route("/recording/stop/<camera_id>", methods=["POST"])
@require_api_auth
def stop_recording(camera_id):
receiver = get_receiver()
if not receiver:
return jsonify({"error": "Camera receiver not available"}), 503
result = receiver.stop_recording(camera_id)
if "error" in result:
return jsonify(result), 400
return jsonify(result)
@bp.route("/recording/status")
@require_api_auth
def recording_status():
receiver = get_receiver()
if not receiver:
return jsonify({"error": "Camera receiver not available"}), 503
camera_id = request.args.get("camera_id")
return jsonify(receiver.get_recording_status(camera_id))
@bp.route("/recordings")
@require_api_auth
def list_recordings():
receiver = get_receiver()
if not receiver:
return jsonify({"recordings": []})
return jsonify({"recordings": receiver.list_recordings()})
return bp

View File

@ -0,0 +1,48 @@
"""Device API routes."""
import time
from flask import Blueprint, jsonify
def create_devices_blueprint(server_config):
"""
Create the devices API blueprint.
Args:
server_config: Dict with keys:
- get_device_registry: Callable returning device registry
- require_api_auth: Auth decorator
"""
bp = Blueprint("api_devices", __name__, url_prefix="/api")
get_registry = server_config["get_device_registry"]
require_api_auth = server_config["require_api_auth"]
@bp.route("/devices")
@require_api_auth
def list_devices():
registry = get_registry()
if registry is None:
return jsonify({"error": "Device registry not available", "devices": []})
now = time.time()
devices = []
for d in registry.all():
devices.append({
"id": d.id,
"ip": d.address[0] if d.address else "unknown",
"port": d.address[1] if d.address else 0,
"status": d.status,
"connected_at": d.connected_at,
"last_seen": d.last_seen,
"connected_for_seconds": round(now - d.connected_at, 1),
"last_seen_ago_seconds": round(now - d.last_seen, 1)
})
return jsonify({
"devices": devices,
"count": len(devices)
})
return bp

View File

@ -0,0 +1,85 @@
"""MLAT (Multilateration) API routes."""
import time
from flask import Blueprint, jsonify, request
def create_mlat_blueprint(server_config):
"""
Create the MLAT API blueprint.
Args:
server_config: Dict with keys:
- get_mlat_engine: Callable returning MLAT engine
- require_api_auth: Auth decorator
"""
bp = Blueprint("api_mlat", __name__, url_prefix="/api/mlat")
get_engine = server_config["get_mlat_engine"]
require_api_auth = server_config["require_api_auth"]
@bp.route("/collect", methods=["POST"])
@require_api_auth
def collect():
"""Receive MLAT readings from scanners."""
engine = get_engine()
raw_data = request.get_data(as_text=True)
count = engine.parse_data(raw_data)
if count > 0:
engine.calculate_position()
return jsonify({
"status": "ok",
"readings_processed": count
})
@bp.route("/state")
@require_api_auth
def state():
"""Get current MLAT state (scanners + target position)."""
engine = get_engine()
state = engine.get_state()
# Auto-calculate if we have enough scanners but no target
if state["target"] is None and state["scanners_count"] >= 3:
result = engine.calculate_position()
if "position" in result:
state["target"] = {
"position": result["position"],
"confidence": result.get("confidence", 0),
"calculated_at": result.get("calculated_at", time.time()),
"age_seconds": 0
}
return jsonify(state)
@bp.route("/config", methods=["GET", "POST"])
@require_api_auth
def config():
"""Get or update MLAT configuration."""
engine = get_engine()
if request.method == "POST":
data = request.get_json() or {}
engine.update_config(
rssi_at_1m=data.get("rssi_at_1m"),
path_loss_n=data.get("path_loss_n"),
smoothing_window=data.get("smoothing_window")
)
return jsonify({
"rssi_at_1m": engine.rssi_at_1m,
"path_loss_n": engine.path_loss_n,
"smoothing_window": engine.smoothing_window
})
@bp.route("/clear", methods=["POST"])
@require_api_auth
def clear():
"""Clear all scanner data."""
engine = get_engine()
engine.clear()
return jsonify({"status": "ok"})
return bp

View File

@ -0,0 +1,59 @@
"""Stats API routes."""
import os
from flask import Blueprint, jsonify
def create_stats_blueprint(server_config):
"""
Create the stats API blueprint.
Args:
server_config: Dict with keys:
- image_dir: Camera images directory
- c2_root: C2 root directory path
- get_device_registry: Callable returning device registry
- get_mlat_engine: Callable returning MLAT engine
- require_api_auth: Auth decorator
"""
bp = Blueprint("api_stats", __name__, url_prefix="/api")
image_dir = server_config["image_dir"]
c2_root = server_config["c2_root"]
get_registry = server_config["get_device_registry"]
get_mlat = server_config["get_mlat_engine"]
require_api_auth = server_config["require_api_auth"]
@bp.route("/stats")
@require_api_auth
def stats():
"""Get server statistics."""
full_image_dir = os.path.join(c2_root, image_dir)
# Camera count
try:
camera_count = len([
f for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
])
except FileNotFoundError:
camera_count = 0
# Device count
device_count = 0
registry = get_registry()
if registry:
device_count = len(list(registry.all()))
# MLAT state
mlat_engine = get_mlat()
mlat_state = mlat_engine.get_state()
return jsonify({
"active_cameras": camera_count,
"connected_devices": device_count,
"multilateration_scanners": mlat_state["scanners_count"],
"server_running": True
})
return bp

View File

@ -0,0 +1,96 @@
"""Page routes (login, dashboard, cameras, mlat)."""
import os
import secrets
from flask import Blueprint, render_template, redirect, url_for, request, session
def create_pages_blueprint(server_config):
"""
Create the pages blueprint.
Args:
server_config: Dict with keys:
- username: Login username
- password: Login password
- image_dir: Camera images directory
- c2_root: C2 root directory path
- require_login: Auth decorator
"""
bp = Blueprint("pages", __name__)
username = server_config["username"]
password = server_config["password"]
image_dir = server_config["image_dir"]
c2_root = server_config["c2_root"]
require_login = server_config["require_login"]
@bp.route("/login", methods=["GET", "POST"])
def login():
error = None
if request.method == "POST":
# CSRF validation
token = request.form.get("csrf_token", "")
if token != session.get("csrf_token", ""):
error = "Invalid request. Please try again."
else:
form_user = request.form.get("username")
form_pass = request.form.get("password")
if form_user == username and form_pass == password:
session["logged_in"] = True
return redirect(url_for("pages.dashboard"))
else:
error = "Invalid credentials."
# Generate CSRF token for the form
session["csrf_token"] = secrets.token_hex(32)
return render_template("login.html", error=error, csrf_token=session["csrf_token"])
@bp.route("/logout")
def logout():
session.pop("logged_in", None)
return redirect(url_for("pages.login"))
@bp.route("/")
@require_login
def index():
return redirect(url_for("pages.dashboard"))
@bp.route("/dashboard")
@require_login
def dashboard():
return render_template("dashboard.html", active_page="dashboard")
@bp.route("/cameras")
@require_login
def cameras():
full_image_dir = os.path.join(c2_root, image_dir)
try:
image_files = sorted([
f for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
])
except FileNotFoundError:
image_files = []
return render_template("cameras.html", active_page="cameras", image_files=image_files)
@bp.route("/mlat")
@require_login
def mlat():
return render_template("mlat.html", active_page="mlat")
@bp.route("/streams/<filename>")
@require_login
def stream_image(filename):
from flask import send_from_directory
full_image_dir = os.path.join(c2_root, image_dir)
return send_from_directory(full_image_dir, filename)
@bp.route("/recordings/<filename>")
@require_login
def download_recording(filename):
from flask import send_from_directory
recordings_dir = os.path.join(c2_root, "static", "recordings")
return send_from_directory(recordings_dir, filename, as_attachment=True)
return bp

197
tools/C3PO/web/server.py Normal file
View File

@ -0,0 +1,197 @@
"""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}"

View File

@ -2,30 +2,33 @@
This directory contains tools for managing and deploying Epsilon ESP32 agents.
## C2 Server (c2/)
## C2 Server (C3PO/)
The C2 (Command & Control) server manages communication with deployed ESP32 agents.
### C3PO - Main C2 Server
**c3po** is the primary C2 server used to control Epsilon bots.
**C3PO** is the primary C2 server used to control Epsilon bots.
Features:
- Asynchronous Python server (asyncio)
- Device registry and management
- Threaded TCP server (sockets + threads)
- Device registry and management with per-device crypto
- Group-based device organization
- Encrypted communications (ChaCha20)
- Encrypted communications (ChaCha20-Poly1305 AEAD + HKDF key derivation)
- Per-device master key keystore (`keys.json`)
- Interactive CLI interface
- Optional TUI (Textual) and Web dashboard
- Camera UDP receiver + MLAT support
- Command dispatching to individual devices, groups, or all
See [c2/README.md](c2/README.md) for complete C2 documentation.
See [C3PO/README.md](C3PO/README.md) for complete C2 documentation.
Quick start:
```bash
cd c2
python3 c3po.py --port 2626
cd C3PO
python3 c3po.py
```
Authors: **@off-path**, **@eun0us**
@ -94,8 +97,8 @@ Each device supports:
| `module_fakeap` | Enable fake AP module |
| `recon_camera` | Enable camera reconnaissance (ESP32-CAM) |
| `recon_ble_trilat` | Enable BLE trilateration |
| `crypto_key` | ChaCha20 encryption key (32 chars) |
| `crypto_nonce` | ChaCha20 nonce (12 chars) |
> **Note**: Crypto keys are no longer configured here. Each device must be provisioned with a unique master key using `tools/provisioning/provision.py`.
### Hostname Randomization
@ -151,6 +154,26 @@ python3 flash.py --config devices.json --flash-only
See [flasher/README.md](flasher/README.md) for complete documentation.
## Device Provisioning (provisioning/)
The **provisioning** tool generates and flashes unique per-device master keys into factory NVS partitions.
### Features
- Generates 32-byte random master keys (cryptographically secure)
- Creates NVS binary for factory partition (`fctry` at offset 0x10000)
- Saves keys to C2 keystore (`keys.json`) for automatic lookup
- Supports flashing directly to connected ESP32
### Quick Start
```bash
cd provisioning
python3 provision.py --device-id my-device --port /dev/ttyUSB0
```
The master key is used by the firmware with HKDF-SHA256 to derive encryption keys for ChaCha20-Poly1305 AEAD.
## NanoPB Tools (nan/)
Tools for Protocol Buffers (nanoPB) code generation for the embedded communication protocol.

View File

@ -1,322 +0,0 @@
# C2 Server Documentation
## TODO
**TODO:**
```md
- Add requierements.txt
- Implementer la coexistence entre multiflasher fichier (valid-id.txt - separator "\n" - C2 will only authorize device who are register in 'valid-id.txt' or add c2 command style 'authorizeId <id>' and he will be add into id list & valid-id.txt file)
```
## Overview
This C2 server is a Python-based control plane designed to manage a fleet of ESP devices (agents).
It provides a clean and extensible command-line interface to:
- Manage connected devices by unique ID
- Send commands to individual devices, groups, or all devices
- Organize devices into logical groups
- Display connection duration to the C2
- Support structured commands and raw developer commands
- Serve as a solid base for future plugins and services
The project intentionally favors simplicity, readability, and extensibility.
---
## Architecture Overview
```md
c2/
├── main.py # Entry point
├── core/
│ ├── device.py # Device model
│ ├── registry.py # Connected device registry
│ ├── crypto.py # Encryption wrapper
│ ├── transport.py # Message handling
│ └── groups.py # Group registry
├── commands/
│ ├── base.py # Command base class
│ ├── registry.py # Command registry
│ └── *.py # ESP command implementations
├── cli/
│ ├── cli.py # Interactive CLI
│ └── help.py # Help system
├── logs/
│ └── manager.py # ESP log handling
├── proto/
│ ├── command.proto # Protocol definition
│ └── command_pb2.py # Generated protobuf code
└── utils/
│ └── constant.py # Default script constant
```
---
## Core Concepts
### Device Identity
- Each ESP device is identified by a unique `esp_id`
- Devices are not identified by IP or port
- Reconnecting devices automatically replace their previous session
### Authentication Model
- Authentication is implicit
- If the server can successfully decrypt and parse a message, the device is considered valid
- No explicit handshake is required
- Fully compatible with existing firmware
---
## Running the Server
```bash
python main.py
```
## CLI Usage
### List Connected Devices
```md
list
*Example d'output:*
ID IP CONNECTED
----------------------------------------
ce4f626b 192.168.1.42 2m 14s
a91dd021 192.168.1.43 18s
```
The `CONNECTED` column shows how long the device has been connected to the c2
### Help Commands
```text
help [commands]
*example:* help
output:
=== C2 HELP ===
CLI Commands:
help [cmd] Show this help
list List connected ESP devices
send <target> <command> Send a command to ESP(s)
group <action> Manage ESP groups
clear Clear the screen
exit Exit the C2
ESP Commands:
reboot Reboot ESP
DEV MODE ENABLED:
You can send arbitrary text commands:
send <id> <any text>
send group <name> <any text>
send all <any text>
```
#### Help System
```md
General Help
help
Help for a Specific ESP Command
help reboot
Help for the send Command
help send
The help output is dynamically generated from the registered commands.
```
### Sending Commands
```md
send <esp_id> <command>
*example:* send ce4f626b reboot # The device named "ce4f626b" will reboot
```
### Send to All Devices
```md
send all [command]
*example:* send all reboot
```
### Send to a Group
```md
send group bots
*example:* send group bots reboot
```
### Developer Mode (RAW Commands)
When `DEV_MODE = True`, arbitrary text commands can be sent:
```md
send <id> [test 12 34]
*example:* send ce4f626b custom start stream
output **ce4f626b:** "start stream"
```
The commands are sent as-is inside the `Command.command` field.
## Groups
### Add Devices to a Group
```md
group add "group_name" <id/s>
*example:* group add bots ce4f626b a91dd021
### List Groups
```md
group list
*Example output:*
bots: ce4f626b, a91dd021
trilat: e2, e3, e1
```
### Show Group Members
```md
group show [group]
*example:* group show bots
output:
bots: ce4f626b, a91dd021
```
### Remove a Device from a Group
```md
group remove bots ce4f626b
```
Command System
Adding a New ESP Command
Create a new file in commands/, for example status.py
Implement the command handler:
from commands.base import CommandHandler
from proto.command_pb2 import Command
class StatusCommand(CommandHandler):
name = "status"
description = "Get device status"
def build(self, args):
cmd = Command()
cmd.command = "status"
return cmd.SerializeToString()
Register the command in main.py:
commands.register(StatusCommand())
The command will automatically appear in:
CLI tab completion
help
send <id> <command>
## Protocol Definition
// explain nano pb
### Command
```h
message Command {
string command = 1;
}
```
### Response
```h
message Response {
string tag = 1;
string id = 2;
string message = 3;
bytes response_data = 4;
}
```
### Log
```h
message Log {
string tag = 1;
string id = 2;
string log_message = 3;
uint32 log_error_code = 4;
}
```
## Communication
The communication between c2 and bots are end-to-end encrypted.</br>
Method:
```md
- ChaCha20 symmetric encryption
- Base64 transport encoding
- Encryption logic centralized in core/crypto.py
- Fully compatible with current ESP firmware
```
## Design Limitations (Intentional)
```md
- No persistent storage (groups reset on restart)
- No request/response correlation (request-id)
- No permissions or role management
- No dynamic plugin loading
```
These are deferred intentionally to keep the core system minimal and clean.
## Suggested Future Extensions
Todo and additional features:
```md
- Plugin system (camera, proxy, trilateration/multilateration / etc.. )
- Persistent user & group storage (JSON) (Multi-Flasher -> devices.json -> id-list.csv [Allow ID on c2])
- Idle time display (last-seen)
- Request/response correlation with request-id
- Protocol versioning
```
---
### Authors
```md
- @off-path
- @Eun0us
```
---

View File

@ -1,31 +0,0 @@
import base64
from Crypto.Cipher import ChaCha20
KEY = b"testde32chars0000000000000000000"
NONCE = b"noncenonceno"
class CryptoContext:
def __init__(self, key: bytes = KEY, nonce: bytes = NONCE):
self.key = key
self.nonce = nonce
# =========================
# ChaCha20
# =========================
def encrypt(self, data: bytes) -> bytes:
cipher = ChaCha20.new(key=self.key, nonce=self.nonce)
return cipher.encrypt(data)
def decrypt(self, data: bytes) -> bytes:
cipher = ChaCha20.new(key=self.key, nonce=self.nonce)
return cipher.decrypt(data)
# =========================
# Base64
# =========================
def b64_encode(self, data: bytes) -> bytes:
return base64.b64encode(data)
def b64_decode(self, data: bytes) -> bytes:
return base64.b64decode(data)

View File

@ -1,2 +0,0 @@
pycryptodome>=3.15.0
protobuf>=4.21.0

View File

@ -1,387 +0,0 @@
"""Unified Flask web server for ESPILON C2 dashboard."""
import os
import logging
import threading
import time
from functools import wraps
from typing import Optional
from flask import Flask, render_template, send_from_directory, request, redirect, url_for, session, jsonify
from werkzeug.serving import make_server
from .mlat import MlatEngine
# 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
- Trilateration: Visualize BLE device 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):
"""
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
"""
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
# Ensure image directory exists
c2_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
full_image_dir = os.path.join(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."""
c2_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
template_dir = os.path.join(c2_root, "templates")
static_dir = os.path.join(c2_root, "static")
app = Flask(__name__,
template_folder=template_dir,
static_folder=static_dir)
app.secret_key = self.secret_key
web_server = self
# ========== Auth Decorators ==========
def require_login(f):
@wraps(f)
def decorated(*args, **kwargs):
if not session.get("logged_in"):
return redirect(url_for("login"))
return f(*args, **kwargs)
return decorated
def require_api_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if session.get("logged_in"):
return f(*args, **kwargs)
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
if token == web_server.multilat_token:
return f(*args, **kwargs)
return jsonify({"error": "Unauthorized"}), 401
return decorated
# ========== Auth Routes ==========
@app.route("/login", methods=["GET", "POST"])
def login():
error = None
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if username == web_server.username and password == web_server.password:
session["logged_in"] = True
return redirect(url_for("dashboard"))
else:
error = "Invalid credentials."
return render_template("login.html", error=error)
@app.route("/logout")
def logout():
session.pop("logged_in", None)
return redirect(url_for("login"))
# ========== Page Routes ==========
@app.route("/")
@require_login
def index():
return redirect(url_for("dashboard"))
@app.route("/dashboard")
@require_login
def dashboard():
return render_template("dashboard.html", active_page="dashboard")
@app.route("/cameras")
@require_login
def cameras():
full_image_dir = os.path.join(c2_root, web_server.image_dir)
try:
image_files = sorted([
f for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
])
except FileNotFoundError:
image_files = []
return render_template("cameras.html", active_page="cameras", image_files=image_files)
@app.route("/mlat")
@require_login
def mlat():
return render_template("mlat.html", active_page="mlat")
# ========== Static Files ==========
@app.route("/streams/<filename>")
@require_login
def stream_image(filename):
full_image_dir = os.path.join(c2_root, web_server.image_dir)
return send_from_directory(full_image_dir, filename)
@app.route("/recordings/<filename>")
@require_login
def download_recording(filename):
recordings_dir = os.path.join(c2_root, "static", "recordings")
return send_from_directory(recordings_dir, filename, as_attachment=True)
# ========== Device API ==========
@app.route("/api/devices")
@require_api_auth
def api_devices():
if web_server.device_registry is None:
return jsonify({"error": "Device registry not available", "devices": []})
now = time.time()
devices = []
for d in web_server.device_registry.all():
devices.append({
"id": d.id,
"ip": d.address[0] if d.address else "unknown",
"port": d.address[1] if d.address else 0,
"status": d.status,
"connected_at": d.connected_at,
"last_seen": d.last_seen,
"connected_for_seconds": round(now - d.connected_at, 1),
"last_seen_ago_seconds": round(now - d.last_seen, 1)
})
return jsonify({
"devices": devices,
"count": len(devices)
})
# ========== Camera API ==========
@app.route("/api/cameras")
@require_api_auth
def api_cameras():
full_image_dir = os.path.join(c2_root, web_server.image_dir)
try:
cameras = [
f.replace(".jpg", "")
for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
]
except FileNotFoundError:
cameras = []
# Add recording status if receiver available
result = {"cameras": [], "count": len(cameras)}
for cam_id in cameras:
cam_info = {"id": cam_id, "recording": False}
if web_server.camera_receiver:
status = web_server.camera_receiver.get_recording_status(cam_id)
cam_info["recording"] = status.get("recording", False)
cam_info["filename"] = status.get("filename")
result["cameras"].append(cam_info)
result["count"] = len(result["cameras"])
return jsonify(result)
# ========== Recording API ==========
@app.route("/api/recording/start/<camera_id>", methods=["POST"])
@require_api_auth
def api_recording_start(camera_id):
if not web_server.camera_receiver:
return jsonify({"error": "Camera receiver not available"}), 503
result = web_server.camera_receiver.start_recording(camera_id)
if "error" in result:
return jsonify(result), 400
return jsonify(result)
@app.route("/api/recording/stop/<camera_id>", methods=["POST"])
@require_api_auth
def api_recording_stop(camera_id):
if not web_server.camera_receiver:
return jsonify({"error": "Camera receiver not available"}), 503
result = web_server.camera_receiver.stop_recording(camera_id)
if "error" in result:
return jsonify(result), 400
return jsonify(result)
@app.route("/api/recording/status")
@require_api_auth
def api_recording_status():
if not web_server.camera_receiver:
return jsonify({"error": "Camera receiver not available"}), 503
camera_id = request.args.get("camera_id")
return jsonify(web_server.camera_receiver.get_recording_status(camera_id))
@app.route("/api/recordings")
@require_api_auth
def api_recordings_list():
if not web_server.camera_receiver:
return jsonify({"recordings": []})
return jsonify({"recordings": web_server.camera_receiver.list_recordings()})
# ========== Trilateration API ==========
@app.route("/api/mlat/collect", methods=["POST"])
@require_api_auth
def api_mlat_collect():
raw_data = request.get_data(as_text=True)
count = web_server.mlat.parse_data(raw_data)
if count > 0:
web_server.mlat.calculate_position()
return jsonify({
"status": "ok",
"readings_processed": count
})
@app.route("/api/mlat/state")
@require_api_auth
def api_mlat_state():
state = web_server.mlat.get_state()
if state["target"] is None and state["scanners_count"] >= 3:
result = web_server.mlat.calculate_position()
if "position" in result:
state["target"] = {
"position": result["position"],
"confidence": result.get("confidence", 0),
"calculated_at": result.get("calculated_at", time.time()),
"age_seconds": 0
}
return jsonify(state)
@app.route("/api/mlat/config", methods=["GET", "POST"])
@require_api_auth
def api_mlat_config():
if request.method == "POST":
data = request.get_json() or {}
web_server.mlat.update_config(
rssi_at_1m=data.get("rssi_at_1m"),
path_loss_n=data.get("path_loss_n"),
smoothing_window=data.get("smoothing_window")
)
return jsonify({
"rssi_at_1m": web_server.mlat.rssi_at_1m,
"path_loss_n": web_server.mlat.path_loss_n,
"smoothing_window": web_server.mlat.smoothing_window
})
@app.route("/api/mlat/clear", methods=["POST"])
@require_api_auth
def api_mlat_clear():
web_server.mlat.clear()
return jsonify({"status": "ok"})
# ========== Stats API ==========
@app.route("/api/stats")
@require_api_auth
def api_stats():
full_image_dir = os.path.join(c2_root, web_server.image_dir)
try:
camera_count = len([
f for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
])
except FileNotFoundError:
camera_count = 0
device_count = 0
if web_server.device_registry:
device_count = len(list(web_server.device_registry.all()))
multilat_state = web_server.mlat.get_state()
return jsonify({
"active_cameras": camera_count,
"connected_devices": device_count,
"multilateration_scanners": multilat_state["scanners_count"],
"server_running": True
})
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}"

View File

@ -1,14 +0,0 @@
# Command & Controll
Code refacto dans .tools/c2 a finir Trilateration -> multilateration
Run c3po.py and ur c2 are up on port 2626 u can edit it on /utils/constant.py
Je dois faire cette doc
( J'ai refacto le code dans [Click Here](../c2/README.md))
## Authors
- @off-path (Main dev c2)
- @Eun0us (Trillateration implementation + proto buff)

View File

@ -1,162 +0,0 @@
import base64
import socket
import threading
import os
from utils.espilon_pb2 import ESPMessage, C2Command
from utils.chacha20 import crypt
from utils.manager import add_to_group, remove_group, remove_esp_from_group
from utils.utils import _print_status, _find_client
from utils.reboot import reboot
from utils.cli import _setup_cli, _color, cli_interface
from tools.c2.utils.constant import HOST, PORT, COMMANDS
from utils.message_process import process_esp_message
class C2Server:
def __init__(self):
self.clients = {}
self.groups = {}
self.clients_ids = {}
# For response synchronization
self.response_events = {}
self.response_data = {}
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((HOST, PORT))
self.server.listen(5)
_setup_cli(self)
self._start_server()
# ---------- Core Server Functions ----------
def _start_server(self):
print(f"\n{_color('GREEN')}[*] Server is listening on {HOST}:{PORT}{_color('RESET')}")
threading.Thread(target=self._accept_clients, daemon=True).start()
cli_interface(self)
def _accept_clients(self):
while True:
client_socket, client_address = self.server.accept()
threading.Thread(target=self._handle_client, args=(client_socket, client_address)).start()
def _handle_client(self, client_socket, client_address):
self._close_existing_client(client_address)
self.clients[client_address] = client_socket
_print_status(f"New client connected : {client_address}", "GREEN")
# Initialize event for this client
self.response_events[client_address] = threading.Event()
self.response_data[client_address] = None
while True:
try:
message = client_socket.recv(4096)
if not message:
break
try:
process_esp_message(self, client_address, message)
except Exception as e:
print(f"Error during decryption: {e}")
continue
except (ConnectionResetError, BrokenPipeError):
break
_print_status(f"Client {client_address} disconnected", "RED")
self.clients.pop(client_address, None)
if client_address in self.response_events:
del self.response_events[client_address]
if client_address in self.response_data:
del self.response_data[client_address]
client_socket.close()
# ---------- Client Management ----------
def _close_existing_client(self, client_address):
if client_address in self.clients:
self.clients[client_address].close()
del self.clients[client_address]
_print_status(f"Client {client_address} disconnected", "RED")
# ---------- CLI Interface ----------
def _complete(self, text, state):
if text.startswith("reboot "):
options = [addr[0] for addr in self.clients.keys() if addr[0].startswith(text[7:])]
options.append("all")
options.extend(self.groups.keys())
elif text.startswith("add_group "):
options = [addr[0] for addr in self.clients.keys() if addr[0].startswith(text[10:])]
elif text.startswith("remove_group "):
options = [group for group in self.groups.keys() if group.startswith(text[13:])]
elif text.startswith("remove_esp_from "):
parts = text.split()
if len(parts) >= 2 and parts[1] in self.groups:
options = self.groups[parts[1]]
else:
options = [cmd for cmd in COMMANDS if cmd.startswith(text)]
return options[state] if state < len(options) else None
def _handle_reboot(self, parts):
if len(parts) != 2:
_print_status("Invalid command. Use 'reboot <id_esp32>', 'reboot all', or 'reboot <group>'", "RED", "")
return
target = parts[1]
if target == "all":
reboot(self, mode="all")
elif target in self.groups:
reboot(self, target, mode="group")
else:
client = _find_client(self, target)
if client:
reboot(self, client, mode="single")
else:
_print_status(f"Client with ID {target} not found", "RED", "")
def _handle_add_group(self, parts):
if len(parts) != 3:
_print_status("Invalid command. Use 'add_group <group> <id_esp32>'", "RED", "")
return
group_name, client_id = parts[1], parts[2]
client = _find_client(self, client_id)
if client:
add_to_group(self, group_name, client)
else:
_print_status(f"Client with ID {client_id} not found", "RED", "")
def _handle_remove_group(self, parts):
if len(parts) != 2:
_print_status(" Invalid command. Use 'remove_group <group>'", "RED", "")
return
remove_group(self, parts[1])
def _handle_remove_esp_from(self, parts):
if len(parts) < 3:
_print_status(" Invalid command. Use 'remove_esp_from <group> <esp>[,<esp>...]'", "RED", "")
return
group_name = parts[1]
esp_list = []
for part in parts[2:]:
esp_list.extend(part.split(','))
remove_esp_from_group(self, group_name, esp_list)
def _shutdown(self):
_print_status(" Closing server ...", "YELLOW", "")
for client in list(self.clients.values()):
client.close()
self.server.close()
os._exit(0)
if __name__ == "__main__":
C2Server()

View File

@ -1,31 +0,0 @@
from dataclasses import dataclass, field
import socket
import time
@dataclass
class Device:
"""
Représente un ESP32 connecté au serveur
"""
id: str
sock: socket.socket
address: tuple[str, int]
connected_at: float = field(default_factory=time.time)
last_seen: float = field(default_factory=time.time)
def touch(self):
"""
Met à jour la date de dernière activité
"""
self.last_seen = time.time()
def close(self):
"""
Ferme proprement la connexion
"""
try:
self.sock.close()
except Exception:
pass

View File

@ -1,67 +0,0 @@
import threading
from typing import Dict, List, Optional
from core.device import Device
class DeviceRegistry:
"""
Registre central des ESP connectés.
Clé primaire : esp_id
"""
def __init__(self):
self._devices: Dict[str, Device] = {}
self._lock = threading.Lock()
# ---------- Gestion des devices ----------
def add(self, device: Device) -> None:
"""
Ajoute ou remplace un device (reconnexion)
"""
with self._lock:
self._devices[device.id] = device
def remove(self, esp_id: str) -> None:
"""
Supprime un device par ID
"""
with self._lock:
device = self._devices.pop(esp_id, None)
if device:
device.close()
def get(self, esp_id: str) -> Optional[Device]:
"""
Récupère un device par ID
"""
with self._lock:
return self._devices.get(esp_id)
def all(self) -> List[Device]:
"""
Retourne la liste de tous les devices
"""
with self._lock:
return list(self._devices.values())
def ids(self) -> List[str]:
"""
Retourne la liste des IDs ESP (pour CLI / tabulation)
"""
with self._lock:
return list(self._devices.keys())
# ---------- Utilitaires ----------
def exists(self, esp_id: str) -> bool:
with self._lock:
return esp_id in self._devices
def touch(self, esp_id: str) -> None:
"""
Met à jour last_seen dun ESP
"""
with self._lock:
device = self._devices.get(esp_id)
if device:
device.touch()

View File

@ -1,5 +0,0 @@
192.168.1.155:5000|GET /search?id=1%20UNION%20SELECT%20NULL,NULL,password%20FROM%20users HTTP/1.1\r\nHost: 192.168.1.155:5000\r\nConnection: close\r\n\r\n
192.168.1.155:5000|GET /login?user=admin&pass=%27%20OR%201%3D1%20--%20 HTTP/1.1\r\nHost: 192.168.1.155:5000\r\nConnection: close\r\n\r\n
192.168.1.155:5000|GET /search?q=%27%20UNION%20SELECT%20NULL--%20 HTTP/1.1\r\nHost: 192.168.1.155:5000\r\nConnection: close\r\n\r\n
192.168.1.155:5000|GET /product?id=1%27%20AND%201%3DCONVERT%28int%2C@@version%29--%20 HTTP/1.1\r\nHost: 192.168.1.155:5000\r\nConnection: close\r\n\r\n
82.67.142.175:22|SSH_CHECK

View File

@ -1,64 +0,0 @@
import socket
import threading
import time
clients = {}
lock = threading.Lock()
def load_all_commands(filename="commands.txt"):
try:
with open(filename, "r") as f:
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"[!] File {filename} not found.")
return []
def handle_client(client_socket, address):
client_id = f"{address[0]}:{address[1]}"
print(f"[+] New client connected : {client_id}")
with lock:
clients[client_id] = client_socket
commands = load_all_commands()
try:
for cmd in commands:
print(f"[→] Send to {client_id} : {cmd}")
client_socket.sendall((cmd + "\n").encode())
# Attente de la réponse du client avant de continuer
data = client_socket.recv(4096)
if not data:
print(f"[!] Client {client_id} has closed connexion.")
break
print(f"[←] Résponse from {client_id} : {data.decode(errors='ignore')}")
except Exception as e:
print(f"[!] Error with {client_id} : {e}")
finally:
with lock:
if client_id in clients:
del clients[client_id]
client_socket.close()
print(f"[-] Client disconnected : {client_id}")
def accept_connections(server_socket):
while True:
client_socket, addr = server_socket.accept()
threading.Thread(target=handle_client, args=(client_socket, addr), daemon=True).start()
def start_server(host="0.0.0.0", port=2021):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((host, port))
server.listen(5)
print(f"[Server] Listening on {host}:{port}")
threading.Thread(target=accept_connections, args=(server,), daemon=True).start()
while True:
time.sleep(1)
if __name__ == "__main__":
start_server()

View File

@ -1,254 +0,0 @@
192.168.1.1:23|PORT_CHECK
192.168.1.2:23|PORT_CHECK
192.168.1.3:23|PORT_CHECK
192.168.1.4:23|PORT_CHECK
192.168.1.5:23|PORT_CHECK
192.168.1.6:23|PORT_CHECK
192.168.1.7:23|PORT_CHECK
192.168.1.8:23|PORT_CHECK
192.168.1.9:23|PORT_CHECK
192.168.1.10:23|PORT_CHECK
192.168.1.11:23|PORT_CHECK
192.168.1.12:23|PORT_CHECK
192.168.1.13:23|PORT_CHECK
192.168.1.14:23|PORT_CHECK
192.168.1.15:23|PORT_CHECK
192.168.1.16:23|PORT_CHECK
192.168.1.17:23|PORT_CHECK
192.168.1.18:23|PORT_CHECK
192.168.1.19:23|PORT_CHECK
192.168.1.20:23|PORT_CHECK
192.168.1.21:23|PORT_CHECK
192.168.1.22:23|PORT_CHECK
192.168.1.23:23|PORT_CHECK
192.168.1.24:23|PORT_CHECK
192.168.1.25:23|PORT_CHECK
192.168.1.26:23|PORT_CHECK
192.168.1.27:23|PORT_CHECK
192.168.1.28:23|PORT_CHECK
192.168.1.29:23|PORT_CHECK
192.168.1.30:23|PORT_CHECK
192.168.1.31:23|PORT_CHECK
192.168.1.32:23|PORT_CHECK
192.168.1.33:23|PORT_CHECK
192.168.1.34:23|PORT_CHECK
192.168.1.35:23|PORT_CHECK
192.168.1.36:23|PORT_CHECK
192.168.1.37:23|PORT_CHECK
192.168.1.38:23|PORT_CHECK
192.168.1.39:23|PORT_CHECK
192.168.1.40:23|PORT_CHECK
192.168.1.41:23|PORT_CHECK
192.168.1.42:23|PORT_CHECK
192.168.1.43:23|PORT_CHECK
192.168.1.44:23|PORT_CHECK
192.168.1.45:23|PORT_CHECK
192.168.1.46:23|PORT_CHECK
192.168.1.47:23|PORT_CHECK
192.168.1.48:23|PORT_CHECK
192.168.1.49:23|PORT_CHECK
192.168.1.50:23|PORT_CHECK
192.168.1.51:23|PORT_CHECK
192.168.1.52:23|PORT_CHECK
192.168.1.53:23|PORT_CHECK
192.168.1.54:23|PORT_CHECK
192.168.1.55:23|PORT_CHECK
192.168.1.56:23|PORT_CHECK
192.168.1.57:23|PORT_CHECK
192.168.1.58:23|PORT_CHECK
192.168.1.59:23|PORT_CHECK
192.168.1.60:23|PORT_CHECK
192.168.1.61:23|PORT_CHECK
192.168.1.62:23|PORT_CHECK
192.168.1.63:23|PORT_CHECK
192.168.1.64:23|PORT_CHECK
192.168.1.65:23|PORT_CHECK
192.168.1.66:23|PORT_CHECK
192.168.1.67:23|PORT_CHECK
192.168.1.68:23|PORT_CHECK
192.168.1.69:23|PORT_CHECK
192.168.1.70:23|PORT_CHECK
192.168.1.71:23|PORT_CHECK
192.168.1.72:23|PORT_CHECK
192.168.1.73:23|PORT_CHECK
192.168.1.74:23|PORT_CHECK
192.168.1.75:23|PORT_CHECK
192.168.1.76:23|PORT_CHECK
192.168.1.77:23|PORT_CHECK
192.168.1.78:23|PORT_CHECK
192.168.1.79:23|PORT_CHECK
192.168.1.80:23|PORT_CHECK
192.168.1.81:23|PORT_CHECK
192.168.1.82:23|PORT_CHECK
192.168.1.83:23|PORT_CHECK
192.168.1.84:23|PORT_CHECK
192.168.1.85:23|PORT_CHECK
192.168.1.86:23|PORT_CHECK
192.168.1.87:23|PORT_CHECK
192.168.1.88:23|PORT_CHECK
192.168.1.89:23|PORT_CHECK
192.168.1.90:23|PORT_CHECK
192.168.1.91:23|PORT_CHECK
192.168.1.92:23|PORT_CHECK
192.168.1.93:23|PORT_CHECK
192.168.1.94:23|PORT_CHECK
192.168.1.95:23|PORT_CHECK
192.168.1.96:23|PORT_CHECK
192.168.1.97:23|PORT_CHECK
192.168.1.98:23|PORT_CHECK
192.168.1.99:23|PORT_CHECK
192.168.1.100:23|PORT_CHECK
192.168.1.101:23|PORT_CHECK
192.168.1.102:23|PORT_CHECK
192.168.1.103:23|PORT_CHECK
192.168.1.104:23|PORT_CHECK
192.168.1.105:23|PORT_CHECK
192.168.1.106:23|PORT_CHECK
192.168.1.107:23|PORT_CHECK
192.168.1.108:23|PORT_CHECK
192.168.1.109:23|PORT_CHECK
192.168.1.110:23|PORT_CHECK
192.168.1.111:23|PORT_CHECK
192.168.1.112:23|PORT_CHECK
192.168.1.113:23|PORT_CHECK
192.168.1.114:23|PORT_CHECK
192.168.1.115:23|PORT_CHECK
192.168.1.116:23|PORT_CHECK
192.168.1.117:23|PORT_CHECK
192.168.1.118:23|PORT_CHECK
192.168.1.119:23|PORT_CHECK
192.168.1.120:23|PORT_CHECK
192.168.1.121:23|PORT_CHECK
192.168.1.122:23|PORT_CHECK
192.168.1.123:23|PORT_CHECK
192.168.1.124:23|PORT_CHECK
192.168.1.125:23|PORT_CHECK
192.168.1.126:23|PORT_CHECK
192.168.1.127:23|PORT_CHECK
192.168.1.128:23|PORT_CHECK
192.168.1.129:23|PORT_CHECK
192.168.1.130:23|PORT_CHECK
192.168.1.131:23|PORT_CHECK
192.168.1.132:23|PORT_CHECK
192.168.1.133:23|PORT_CHECK
192.168.1.134:23|PORT_CHECK
192.168.1.135:23|PORT_CHECK
192.168.1.136:23|PORT_CHECK
192.168.1.137:23|PORT_CHECK
192.168.1.138:23|PORT_CHECK
192.168.1.139:23|PORT_CHECK
192.168.1.140:23|PORT_CHECK
192.168.1.141:23|PORT_CHECK
192.168.1.142:23|PORT_CHECK
192.168.1.143:23|PORT_CHECK
192.168.1.144:23|PORT_CHECK
192.168.1.145:23|PORT_CHECK
192.168.1.146:23|PORT_CHECK
192.168.1.147:23|PORT_CHECK
192.168.1.148:23|PORT_CHECK
192.168.1.149:23|PORT_CHECK
192.168.1.150:23|PORT_CHECK
192.168.1.151:23|PORT_CHECK
192.168.1.152:23|PORT_CHECK
192.168.1.153:23|PORT_CHECK
192.168.1.154:23|PORT_CHECK
192.168.1.155:23|PORT_CHECK
192.168.1.156:23|PORT_CHECK
192.168.1.157:23|PORT_CHECK
192.168.1.158:23|PORT_CHECK
192.168.1.159:23|PORT_CHECK
192.168.1.160:23|PORT_CHECK
192.168.1.161:23|PORT_CHECK
192.168.1.162:23|PORT_CHECK
192.168.1.163:23|PORT_CHECK
192.168.1.164:23|PORT_CHECK
192.168.1.165:23|PORT_CHECK
192.168.1.166:23|PORT_CHECK
192.168.1.167:23|PORT_CHECK
192.168.1.168:23|PORT_CHECK
192.168.1.169:23|PORT_CHECK
192.168.1.170:23|PORT_CHECK
192.168.1.171:23|PORT_CHECK
192.168.1.172:23|PORT_CHECK
192.168.1.173:23|PORT_CHECK
192.168.1.174:23|PORT_CHECK
192.168.1.175:23|PORT_CHECK
192.168.1.176:23|PORT_CHECK
192.168.1.177:23|PORT_CHECK
192.168.1.178:23|PORT_CHECK
192.168.1.179:23|PORT_CHECK
192.168.1.180:23|PORT_CHECK
192.168.1.181:23|PORT_CHECK
192.168.1.182:23|PORT_CHECK
192.168.1.183:23|PORT_CHECK
192.168.1.184:23|PORT_CHECK
192.168.1.185:23|PORT_CHECK
192.168.1.186:23|PORT_CHECK
192.168.1.187:23|PORT_CHECK
192.168.1.188:23|PORT_CHECK
192.168.1.189:23|PORT_CHECK
192.168.1.190:23|PORT_CHECK
192.168.1.191:23|PORT_CHECK
192.168.1.192:23|PORT_CHECK
192.168.1.193:23|PORT_CHECK
192.168.1.194:23|PORT_CHECK
192.168.1.195:23|PORT_CHECK
192.168.1.196:23|PORT_CHECK
192.168.1.197:23|PORT_CHECK
192.168.1.198:23|PORT_CHECK
192.168.1.199:23|PORT_CHECK
192.168.1.200:23|PORT_CHECK
192.168.1.201:23|PORT_CHECK
192.168.1.202:23|PORT_CHECK
192.168.1.203:23|PORT_CHECK
192.168.1.204:23|PORT_CHECK
192.168.1.205:23|PORT_CHECK
192.168.1.206:23|PORT_CHECK
192.168.1.207:23|PORT_CHECK
192.168.1.208:23|PORT_CHECK
192.168.1.209:23|PORT_CHECK
192.168.1.210:23|PORT_CHECK
192.168.1.211:23|PORT_CHECK
192.168.1.212:23|PORT_CHECK
192.168.1.213:23|PORT_CHECK
192.168.1.214:23|PORT_CHECK
192.168.1.215:23|PORT_CHECK
192.168.1.216:23|PORT_CHECK
192.168.1.217:23|PORT_CHECK
192.168.1.218:23|PORT_CHECK
192.168.1.219:23|PORT_CHECK
192.168.1.220:23|PORT_CHECK
192.168.1.221:23|PORT_CHECK
192.168.1.222:23|PORT_CHECK
192.168.1.223:23|PORT_CHECK
192.168.1.224:23|PORT_CHECK
192.168.1.225:23|PORT_CHECK
192.168.1.226:23|PORT_CHECK
192.168.1.227:23|PORT_CHECK
192.168.1.228:23|PORT_CHECK
192.168.1.229:23|PORT_CHECK
192.168.1.230:23|PORT_CHECK
192.168.1.231:23|PORT_CHECK
192.168.1.232:23|PORT_CHECK
192.168.1.233:23|PORT_CHECK
192.168.1.234:23|PORT_CHECK
192.168.1.235:23|PORT_CHECK
192.168.1.236:23|PORT_CHECK
192.168.1.237:23|PORT_CHECK
192.168.1.238:23|PORT_CHECK
192.168.1.239:23|PORT_CHECK
192.168.1.240:23|PORT_CHECK
192.168.1.241:23|PORT_CHECK
192.168.1.242:23|PORT_CHECK
192.168.1.243:23|PORT_CHECK
192.168.1.244:23|PORT_CHECK
192.168.1.245:23|PORT_CHECK
192.168.1.246:23|PORT_CHECK
192.168.1.247:23|PORT_CHECK
192.168.1.248:23|PORT_CHECK
192.168.1.249:23|PORT_CHECK
192.168.1.250:23|PORT_CHECK
192.168.1.251:23|PORT_CHECK
192.168.1.252:23|PORT_CHECK
192.168.1.253:23|PORT_CHECK
192.168.1.254:23|PORT_CHECK

View File

@ -1,65 +0,0 @@
import socket
import threading
import time
clients = {}
lock = threading.Lock()
def load_all_commands(filename="commands.txt"):
try:
with open(filename, "r") as f:
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"[!] Fichier {filename} non trouvé.")
return []
def handle_client(client_socket, address):
client_id = f"{address[0]}:{address[1]}"
print(f"[+] Nouveau client connecté : {client_id}")
with lock:
clients[client_id] = client_socket
commands = load_all_commands()
try:
for cmd in commands:
print(f"[→] Envoi à {client_id} : {cmd}")
client_socket.sendall((cmd + "\n").encode())
# Attente de la réponse du client avant de continuer
data = client_socket.recv(4096)
if not data:
print(f"[!] Le client {client_id} a fermé la connexion.")
break
print(f"[←] Réponse de {client_id} : {data.decode(errors='ignore')}")
except Exception as e:
print(f"[!] Erreur avec {client_id} : {e}")
finally:
with lock:
if client_id in clients:
del clients[client_id]
client_socket.close()
print(f"[-] Client déconnecté : {client_id}")
def accept_connections(server_socket):
while True:
client_socket, addr = server_socket.accept()
threading.Thread(target=handle_client, args=(client_socket, addr), daemon=True).start()
def start_server(host="0.0.0.0", port=1234):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((host, port))
server.listen(5)
print(f"[Serveur] En écoute sur {host}:{port}")
threading.Thread(target=accept_connections, args=(server,), daemon=True).start()
while True:
time.sleep(1)
if __name__ == "__main__":
start_server()

Binary file not shown.

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Multi Flux Caméras UDP</title>
<style>
img {
max-width: 300px;
margin: 10px;
border: 2px solid #333;
}
</style>
</head>
<body>
<h1>Caméras en direct</h1>
<div id="camera-container">
{% for img in image_files %}
<div>
<p>{{ img }}</p>
<img id="img_{{ loop.index }}" src="/streams/{{ img }}?t={{ loop.index }}" alt="Camera {{ loop.index }}">
<p>{{ image.split('_')[0] if '_' in image else image }}</p>
</div>
{% endfor %}
</div>
<script>
function refreshImages() {
const imgs = document.querySelectorAll("img");
imgs.forEach((img, i) => {
const src = img.src.split("?")[0];
img.src = `${src}?t=${Date.now()}`; // Cache-busting
});
}
setInterval(refreshImages, 100); // Rafraîchit toutes les 100 ms
</script>
<a href="/trilateration">→ Voir Trilatération</a>
</body>
</html>

View File

@ -1,58 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
padding: 40px;
}
.card {
max-width: 360px;
margin: auto;
background: #fff;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type='submit'] {
background: #007BFF;
color: #fff;
border: none;
cursor: pointer;
}
input[type='submit']:hover {
background: #0056b3;
}
.error {
color: red;
margin-bottom: 10px;
text-align: center;
}
</style>
</head>
<body>
<div class="card">
<h2>Connexion requise</h2>
<p>Veuillez entrer vos identifiants pour accéder au service.</p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="post">
<input type="text" name="username" placeholder="Nom d'utilisateur" required>
<input type="password" name="password" placeholder="Mot de passe" required>
<input type="submit" value="Se connecter">
</form>
</div>
</body>
</html>

View File

@ -1,153 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Carte de Trilatération</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
}
#canvas {
border: 1px solid #333;
background-color: #f9f9f9;
}
#controls {
margin: 10px;
}
</style>
</head>
<body>
<h1>Carte de Trilatération</h1>
<p><strong>Dernier point :</strong>
{% if point %}
({{ "%.3f"|format(point[0]) }}, {{ "%.3f"|format(point[1]) }})
{% else %}
Aucun point
{% endif %}
</p>
<div id="controls">
<label for="gridSize">Taille du plan (L x L) :</label>
<input type="number" id="gridSize" value="10" min="5" max="50">
<button onclick="redraw()">Appliquer</button>
</div>
<canvas id="canvas" width="600" height="600"></canvas>
<script>
const originalPoint = {{ point|tojson }};
const anchors = {{ anchors|tojson }};
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const scale = 50;
let gridSize = parseInt(document.getElementById("gridSize").value);
const espColors = {
"esp1": "green",
"esp2": "blue",
"esp3": "gold"
};
function toCanvasCoord(x, y) {
return [x * scale, height - y * scale];
}
function drawGrid() {
ctx.strokeStyle = "#ddd";
ctx.lineWidth = 1;
for (let i = 0; i <= gridSize; i++) {
ctx.beginPath();
ctx.moveTo(i * scale, 0);
ctx.lineTo(i * scale, height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * scale);
ctx.lineTo(width, i * scale);
ctx.stroke();
}
}
function drawAxes() {
ctx.strokeStyle = "#888";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, height);
ctx.lineTo(width, height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, height);
ctx.stroke();
ctx.fillStyle = "#000";
ctx.font = "10px sans-serif";
for (let i = 0; i <= gridSize; i++) {
ctx.fillText(i, i * scale + 2, height - 5);
ctx.fillText(i, 2, height - i * scale - 2);
}
}
function drawPoint(x, y, color, label) {
const [px, py] = toCanvasCoord(x, y);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(px, py, 6, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
if (label) {
ctx.font = "12px sans-serif";
ctx.fillStyle = "black"; // ✅ ID toujours en noir
ctx.fillText(label, px + 8, py - 8);
}
}
function drawCircle(x, y, radius, color = 'rgba(0,0,255,0.1)') {
const [px, py] = toCanvasCoord(x, y);
ctx.beginPath();
ctx.arc(px, py, radius * scale, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = color;
ctx.stroke();
}
function redraw() {
gridSize = parseInt(document.getElementById("gridSize").value);
ctx.clearRect(0, 0, width, height);
drawGrid();
drawAxes();
if (anchors) {
for (const [id, coords] of Object.entries(anchors)) {
const [x, y, r] = coords;
const baseColor = espColors[id] || 'gray';
const circleColor = baseColor === 'gold' ? 'rgba(255, 215, 0, 0.1)' :
baseColor === 'green' ? 'rgba(0, 128, 0, 0.1)' :
baseColor === 'blue' ? 'rgba(0, 0, 255, 0.1)' :
'rgba(128,128,128,0.1)';
if (r) drawCircle(x, y, r, circleColor);
drawPoint(x, y, baseColor, id); // ID en noir
}
}
if (originalPoint) {
const [x, y] = originalPoint;
drawCircle(x, y, 0.5, 'rgba(255,0,0,0.2)'); // Cercle d'incertitude
drawPoint(x, y, "red", "TARGET");
}
}
redraw();
</script>
</body>
</html>

View File

@ -1,101 +0,0 @@
from utils.utils import _print_status, send_to_client
from utils.reboot import reboot
from utils.manager import add_to_group, remove_group, remove_esp_from_group
import time
def system_check(c2):
_print_status("=============== TEST GLOBAL DU SYSTÈME ===============", "CYAN")
# 1. Liste des clients connectés
_print_status("[1/10] Liste des clients connectés", "YELLOW")
if not c2.clients:
_print_status("Aucun client connecté", "RED", "")
return
for i, (addr, _) in enumerate(c2.clients.items(), 1):
print(f" {i}. {addr[0]}:{addr[1]}")
_print_status("Liste récupérée", "GREEN", "")
# 2. Envoi d'une commande simple ("ls") à chaque client
_print_status("[2/10] Test d'envoi de commande à chaque client", "YELLOW")
for addr in list(c2.clients.keys()):
try:
response = send_to_client(c2, addr, "ls", wait_response=True)
if response:
_print_status(f"Réponse reçue de {addr[0]}", "GREEN", "")
else:
_print_status(f"Aucune réponse de {addr[0]}", "RED", "")
except:
_print_status(f"Erreur d'envoi vers {addr[0]}", "RED", "")
# 3. Création et remplissage d'un groupe de test
_print_status("[3/10] Création dun groupe test et ajout de clients", "YELLOW")
test_group = "test_all"
for i, addr in enumerate(c2.clients.keys()):
if i < 2: # Limite à 2 clients pour le test
add_to_group(c2, test_group, addr)
if test_group in c2.groups:
_print_status(f"Groupe '{test_group}' créé avec {len(c2.groups[test_group])} clients", "GREEN", "")
else:
_print_status("Échec création groupe", "RED", "")
# 4. Liste des groupes
_print_status("[4/10] Listing des groupes", "YELLOW")
if c2.groups:
for group, members in c2.groups.items():
print(f" {group} : {members}")
_print_status("Groupes listés", "GREEN", "")
else:
_print_status("Aucun groupe trouvé", "RED", "")
# 5. Reboot dun seul client
_print_status("[5/10] Reboot dun seul client", "YELLOW")
first_client = list(c2.clients.keys())[0]
reboot(c2, first_client, mode="single")
time.sleep(5)
# 6. Reboot du groupe
_print_status("[6/10] Reboot du groupe", "YELLOW")
reboot(c2, test_group, mode="group")
time.sleep(5)
# 7. Reboot de tous les clients
_print_status("[7/10] Reboot de tous les clients", "YELLOW")
reboot(c2, mode="all")
time.sleep(5)
# 8. Attente et vérification de reconnexion
_print_status("[8/10] Attente de reconnexion des clients", "YELLOW", "!")
time.sleep(5)
if c2.clients:
for addr in c2.clients.keys():
print(f" - {addr[0]}:{addr[1]}")
_print_status("Clients reconnectés", "GREEN", "")
else:
_print_status("Aucun client reconnecté", "RED", "")
# 9. Retirer un client du groupe
# check si il y a plusieurs clients dans le groupe
# si oui, retirer le premier
# sinon passer le test
_print_status("[9/10] Retirer un client du groupe", "YELLOW")
if len(c2.groups[test_group]) > 1:
first_client = c2.groups[test_group][0]
remove_esp_from_group(c2, test_group, [first_client])
if first_client not in c2.groups[test_group]:
_print_status(f"Client {first_client} retiré du groupe {test_group}", "GREEN", "")
else:
_print_status(f"Échec de retrait du client {first_client} du groupe {test_group}", "RED", "")
else:
_print_status("Groupe ne contient qu'un seul client, pas de retrait effectué", "YELLOW", "!")
# 10. Suppression du groupe
_print_status("[10/10] Suppression du groupe de test", "YELLOW")
if test_group in c2.groups:
remove_group(c2, test_group)
if test_group not in c2.groups:
_print_status("Groupe supprimé", "GREEN", "")
else:
_print_status("Échec de suppression du groupe", "RED", "")
_print_status("=============== TEST TERMINÉ ===============", "CYAN")

View File

@ -1,149 +0,0 @@
from utils.manager import list_groups
from tools.c2.utils.constant import _color
# from utils.genhash import generate_random_endpoint, generate_token
from utils.utils import _print_status, _list_clients, _send_command
#from test.test import system_check
# from udp_server import start_cam_server, stop_cam_server
import os
import readline
from utils.sheldon import call
def _setup_cli(c2):
readline.parse_and_bind("tab: complete")
readline.set_completer_delims(' \t\n;')
readline.set_completer(c2._complete)
readline.set_auto_history(True)
def _show_menu():
menu = f"""
{_color('CYAN')}=============== Menu Server ==============={_color('RESET')}
menu / help -> List of commands
########## Manage Esp32 ##########
add_group <group> <id_esp32> -> Add a client to a group
list_groups -> List all groups
remove_group <group> -> Remove a group
remove_esp_from <group> <esp> -> Remove a client from a group
reboot <id_esp32> -> Reboot a specific client
reboot <group> -> Reboot all clients in a group
reboot all -> Reboot all clients
########## System C2 Commands ##########
list -> List all connected clients
clear -> Clear the terminal screen
exit -> Exit the server
start/stop srv_video -> Register a camera service
########## Firmware Services client ##########
## Send Commands to firmware ##
send <id_esp32> <message> -> Send a message to a client
or
send <id_esp32> <start/stop> <service> <args> -> Start/Stop a service on a specific client
## Start/Stop Services on clients ##
start proxy <IP> <PORT> -> Start a reverse proxy on ur ip port for a specific client
stop proxy -> Stop the revproxy on a specific client
start stream <IP> <PORT> -> Start camera stream on a specific client
stop stream -> Stop camera stream on a specific client
start ap <WIFI_SSID> <PASSWORD> -> Start an access point on a specific client
(WIFI_SSID and PASSWORD are optional, default_SSID="ESP_32_WIFI_SSID")
stop ap -> Stop the access point on a specific client
list_clients -> List all connected clients on the access point
start sniffer -> Start packet sniffing on clients connected to the access point
stop sniffer -> Stop packet sniffing on clients connected to the access point
start captive_portal <WIFI_SSID> -> Start a server on a specific client
(WIFI_SSID is optional, default_SSID="ESP_32_WIFI_SSID")
stop captive_portal -> Stop the server on a specific client
"""
print(menu)
def _show_banner():
banner = rf"""
{_color('CYAN')}Authors : Eunous/grogore, itsoktocryyy, offpath, Wepfen, p2lu
___________
\_ _____/ ____________ |__| | ____ ____
| __)_ / ___/\____ \| | | / _ \ / \
| \\___ \ | |_> > | |_( <_> ) | \
/_______ /____ >| __/|__|____/\____/|___| /
\/ \/ |__| \/
=============== v 0.1 ==============={_color('RESET')}
"""
print(banner)
def cli_interface(self):
_show_banner()
_show_menu()
# def _cmd_start(parts):
# if len(parts) > 1 and parts[1] == "srv_video":
# start_cam_server()
# def _cmd_stop(parts):
# if len(parts) > 1 and parts[1] == "srv_video":
# stop_cam_server()
commands = {
"menu": lambda parts: _show_menu(),
"help": lambda parts: _show_menu(),
"send": lambda parts: _send_command(self, " ".join(parts)),
"list": lambda parts: _list_clients(self),
"clear": lambda parts: os.system('cls' if os.name == 'nt' else 'clear'),
"exit": lambda parts: self._shutdown(),
"reboot": lambda parts: self._handle_reboot(parts),
"add_group": lambda parts: self._handle_add_group(parts),
"list_groups": lambda parts: list_groups(self),
"remove_group": lambda parts: self._handle_remove_group(parts),
"remove_esp_from": lambda parts: self._handle_remove_esp_from(parts),
# "start": _cmd_start,
# "stop": _cmd_stop,
# "system_check": lambda parts: system_check(self),
}
while True:
choix = input(f"\n{_color('BLUE')}striker:> {_color('RESET')}").strip()
if not choix:
continue
parts = choix.split()
cmd = parts[0]
try:
if cmd in commands:
commands[cmd](parts)
else:
call(choix)
except Exception as e:
_print_status(f"Erreur: {str(e)}", "RED", "")

View File

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: espilon.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\respilon.proto\x12\x07\x65spilon\"C\n\nESPMessage\x12\x0b\n\x03tag\x18\x01 \x02(\t\x12\n\n\x02id\x18\x02 \x02(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x0b\n\x03log\x18\x04 \x01(\x0c\"\x1c\n\tC2Command\x12\x0f\n\x07payload\x18\x01 \x02(\t\"U\n\rESPLogMessage\x12\x0b\n\x03tag\x18\x01 \x02(\t\x12\n\n\x02id\x18\x02 \x02(\t\x12\x13\n\x0blog_message\x18\x03 \x01(\t\x12\x16\n\x0elog_error_code\x18\x04 \x01(\r')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'espilon_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_ESPMESSAGE._serialized_start=26
_ESPMESSAGE._serialized_end=93
_C2COMMAND._serialized_start=95
_C2COMMAND._serialized_end=123
_ESPLOGMESSAGE._serialized_start=125
_ESPLOGMESSAGE._serialized_end=210
# @@protoc_insertion_point(module_scope)

View File

@ -1,13 +0,0 @@
import secrets
import hashlib
def generate_random_endpoint():
endpoint = f"/stream_{secrets.token_hex(4)}"
print(f"[+] Endpoint généré : {endpoint}")
return endpoint
def generate_token():
raw = secrets.token_bytes(8)
hashed = hashlib.sha256(raw).hexdigest()
print(f"[+] Bearer Token généré : {hashed}")
return hashed

Some files were not shown because too many files have changed in this diff Show More