Project_SylDa/README.md
eddy 7f68e9ad70 Add BLE security: Just Works pairing + HMAC-SHA256 authentication
Two-layer security for the BLE relay:

1. BLE Just Works pairing (link-layer encryption)
   - ESP32 requires bonding before accepting writes
   - Characteristic permissions set to ESP_GATT_PERM_WRITE_ENCRYPTED
   - IO capability NO_INPUT_NO_OUTPUT (no passkey needed)
   - LE Secure Connections with bonding (ESP_LE_AUTH_REQ_SC_BOND)

2. HMAC-SHA256 authentication (application-level)
   - 32-byte shared secret in src/hmac_key.h (generated, gitignored)
   - Each command signed: HMAC-SHA256(key, command || sequence)
   - 37-byte packet format: [cmd:1][seq:4][hmac:32]
   - Sequence counter prevents replay (strictly increasing, starts at 1)
   - mbedtls on ESP, javax.crypto.Mac on Android

Key management:
- tools/generate_hmac_key.py generates random key + writes src/hmac_key.h
- ESP prints hex key to serial 5 seconds after boot
- Android app: SettingsActivity with eye-toggle password field
- Key stored in SharedPreferences, supports multiple ESP modules

Other changes:
- Android app: bonding flow (createBond -> BOND_BONDED -> discover)
- Android app: MTU negotiation (request 256)
- Android app: settings button (gear icon) in main screen
- Fix: sequence counter starts at 1 to prevent first-packet rejection
- Updated README with full security documentation
2026-07-26 08:18:52 +02:00

297 lines
No EOL
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Sylvia Touch Relay
BLE-gestuurde relais-switch voor de M5Stack Atom VoiceS3R (ESP32-S3) met een bijbehorende Android-app. Het relais kan worden bediend via een capacitieve touch-sensor (open-drain) óf via Bluetooth Low Energy vanaf een Android-telefoon.
De BLE-verbinding is beveiligd met **twee lagen**: BLE Just Works pairing (link-layer encryptie) en **HMAC-SHA256 authenticatie** (application-level handtekening met replay-protectie).
## Hardware
| Component | Details |
|---|---|
| Board | M5Stack Atom VoiceS3R (SKU: C126-ECHO) |
| SoC | ESP32-S3-PICO-1-N8R8 (8MB Flash, 8MB PSRAM) |
| Touch sensor | Open-drain sensor op **G38** (3.3V, interne pullup) |
| Relais / LED | Op **G1** (3.3V GPIO output) |
| Status LED | Op **G46** (knippert 3x bij opstarten) |
### Pinout (Atom VoiceS3R onderste header)
```
G38 G39 3.3V GND 5V G5 G6 G7 G8
```
- **G38** — Touch sensor input (active LOW, interne pullup ingeschakeld)
- **G1** — Relais output (HIGH = aan, LOW = uit)
- **G46** — Status LED (3x knipperen bij opstarten, daarna uit)
### Aansluiting
```
Sensor GND → GND
Sensor OUT → G38
Sensor VCC → 3.3V
Relais + → G1
Relais - → GND
```
> **Let op:** De ESP32-S3 interne pullup is ~45kΩ. Bij lange draden of elektrische ruis een externe 4.7kΩ–10kΩ pullup naar 3.3V plaatsen.
## Beveiliging
De relay is beveiligd tegen ongeautoriseerde toegang via twee onafhankelijke lagen:
### 1. BLE Just Works Pairing (link-layer encryptie)
- De ESP32 vereist **bonding** voordat de characteristic geschreven kan worden
- IO capability: `ESP_IO_CAP_NONE` (Just Works — geen passcode nodig)
- Authentication mode: `ESP_LE_AUTH_REQ_SC_BOND` (LE Secure Connections met bonding)
- De characteristic heeft `ESP_GATT_PERM_WRITE_ENCRYPTED` — writes worden alleen geaccepteerd over een versleutelde link
- Na de eerste keer paren onthoudt de telefoon de bonding; bij herverbinding wordt de link automatisch versleuteld
### 2. HMAC-SHA256 Authenticatie (application-level)
Omdat BLE Just Works geen MITM-bescherming biedt, is er een tweede laag toegevoegd:
- Both de ESP firmware en de Android app bevatten een **32-byte shared secret** (HMAC key)
- Elk commando wordt ondertekend met `HMAC-SHA256(key, command || sequence)`
- De ESP verifieert de handtekening voordat het relais schakelt
- Een **sequence counter** voorkomt replay-aanvallen: elk packet moet een strikt oplopend volgnummer hebben
#### Packet formaat (37 bytes)
```
Offset Grootte Veld Beschrijving
0 1 byte command 0x01 = relais aan, 0x00 = relais uit
1 4 bytes sequence Big-endian uint32, strikt oplopend (start bij 1)
5 32 bytes hmac HMAC-SHA256(key, command || sequence)
```
De sequence counter reset naar 0 bij elke (re)connectie op de ESP. De app start bij 1 om te zorgen dat het eerste packet altijd geaccepteerd wordt (`1 > 0`). Replay van een eerder packet wordt geweerd omdat `seq <= lastSeq`.
#### Key management
- De HMAC key staat in `src/hmac_key.h` (niet in git, `.gitignore`)
- Een CLI-tool genereert een willekeurige key: `python3 tools/generate_hmac_key.py`
- De ESP print de key 5 seconden na opstarten naar serial
- De gebruiker voert de key eenmalig in via het instellingenscherm in de Android app
- De app slaat de key op in SharedPreferences (overleeft app-herstarts)
- Zo kan dezelfde app gebruikt worden voor meerdere ESP modules — each met een eigen key
### Beveiligingsanalyse
| Aanval | Hoe geweerd |
|---|---|
| **Sniffen** | BLE link encrypted (AES-CCM via Just Works) |
| **Replay** | Sequence counter — zelfde packet werkt niet 2x |
| **Ongeautoriseerd device** | HMAC vereist geheime sleutel |
| **MITM** | Just Works is kwetsbaar voor MITM; HMAC maakt actieve MITM wel moeilijk (aanvaller moet de key kennen) |
| **Reverse engineering van key** | Sleutel staat in APK — extracteerbaar met reverse engineering. Voor hogere veiligheid: gebruiker ProGuard/R8 obfuscation. |
## Firmware
De firmware is geschreven in C++ met behulp van PlatformIO en de Arduino-framework. BLE wordt verzorgd door de ESP32 BLE library, HMAC door mbedtls (ingebouwd in ESP-IDF).
### Logica
Het relais gaat aan als **één van beide** bronnen actief is:
| Bron | Voorwaarde | Resultaat |
|---|---|---|
| Touch sensor | G38 = LOW (aanraken) | Relais aan |
| BLE command | App stuurt HMAC-ondertekend `0x01` | Relais aan |
| Geen van beide | G38 = HIGH + geen BLE command | Relais uit |
De touch sensor en BLE werken onafzankelijk. Als de sensor losgelaten wordt en de BLE-knop niet ingedrukt is, gaat het relais uit.
### BLE GATT
| Eigenschap | UUID |
|---|---|
| Service | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` |
| Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d8` |
De characteristic ondersteunt `WRITE` (encrypted) en `NOTIFY`. Writes vereisen een versleutelde link (bonding) plus HMAC-authenticatie.
### HMAC Key genereren
Voordat de firmware gecompileerd kan worden, moet er een HMAC key gegenereerd worden:
```bash
python3 tools/generate_hmac_key.py
```
Dit maakt `src/hmac_key.h` aan met een willekeurige 32-byte key en print de hex-representatie. Een bestaande key overschrijven:
```bash
echo "y" | python3 tools/generate_hmac_key.py
```
> **Belangrijk:** `src/hmac_key.h` staat in `.gitignore` en wordt niet gecommit. De key moet op de ESP en in de app identiek zijn.
### Serial monitor
Baudrate: **115200** (USB CDC). De firmware stuurt heartbeat-statussen elke 5 seconden, logt statuswijzigingen, en print de HMAC key 5 seconden na opstarten:
```
=== Atom VoiceS3R BLE Relay ===
LED blink done, starting BLE...
BLE: advertising started as 'AtomS3R-Relay'
Printing HMAC key in 5 seconds...
HMAC Key: C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF
Enter this key in the Android app > Settings.
Setup done.
[heartbeat] relay=OFF touch=no ble=disconnected
BLE: device connected
Relay ON (touch=YES, ble=connected)
BLE: relay ON (seq=1)
BLE: relay OFF (seq=2)
```
### Uploaden
> **Download-mode:** Houd de reset-knop ~2 seconden ingedrukt tot de groene LED aangaat, laat dan los. Het board is nu in download-mode.
```bash
# Zorg dat src/hmac_key.h bestaat (zo niet: python3 tools/generate_hmac_key.py)
pio run --target upload
```
Na het uploaden treedt er een harde reset op. Als het board in download-mode blijft, USB eruit/erin of kort op reset tikken om in run-mode te komen.
## Android App
### Features
- **Auto-connect:** De app scant automatisch bij opstarten naar het BLE-apparaat "AtomS3R-Relay"
- **Auto-reconnect:** Bij wegvallen van de verbinding wordt na 2 seconden opnieuw gescant
- **Hold-to-activate:** Houd de knop ingedrukt voor relais aan, loslaten voor relais uit (zelfde gedrag als de sensor)
- **BLE Pairing:** De app initieert bonding (`createBond()`) na connectie; Just Works pairing verloopt zonder gebruikersinteractie
- **MTU negotiatie:** Vraagt MTU 256 aan na service discovery (voor 37-byte HMAC packets)
- **HMAC authenticatie:** Elk commando wordt ondertekend met HMAC-SHA256 + sequence counter
- **Instellingenscherm:** Losse Activity voor het invoeren van de HMAC key (met oog-icoon om de key te tonen/verbergen)
- **Permissies:** Vraagt BLUETOOTH_SCAN, BLUETOOTH_CONNECT en ACCESS_FINE_LOCATION aan (Android 12+)
### Instellingenscherm
1. Open de app
2. Tik op het **tandwiel-icoon** rechtsboven
3. Voer de 64-teken hex key in (zelfde als de ESP op serial print)
4. Tik op het **oog-icoon** om de key te tonen/verbergen
5. Tik **Opslaan**
De key wordt opgeslagen in SharedPreferences en overleeft app-herstarts. Bij geen key toont de app "Geen HMAC sleutel - open Instellingen" en wordt er niet gescant.
### Bouwen
Vereisten:
- Android SDK (API 34+)
- Java 17+ (getest met Java 17 en 21; Java 26 werkt niet met Kotlin 1.9 daemon — valt terug op non-incremental)
- Gradle 8.2.2 (via wrapper)
```bash
cd android
./gradlew assembleDebug
```
De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`.
### Installeren
```bash
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
Of via draadloze foutopsporing:
```bash
adb connect <ip>:<poort>
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
## Eerste keer setup
1. **Genereer HMAC key:**
```bash
python3 tools/generate_hmac_key.py
```
Noteer de geprinte hex key.
2. **Flash de ESP:**
```bash
pio run -t upload
```
3. **Lees de HMAC key uit serial** (5 seconden na opstarten):
```bash
# Via python:
python3 -c "import serial,time; s=serial.Serial('/dev/ttyACM0',115200); [print(l) for l in iter(lambda: s.readline().decode(errors='replace').strip() or None, None) if 'HMAC Key:' in l] if False else exec('''
import serial, time
s = serial.Serial(\"/dev/ttyACM0\", 115200, timeout=2)
start = time.time()
while time.time() - start < 15:
d = s.read(1024)
if d:
for line in d.decode(errors=\"replace\").splitlines():
if line.strip(): print(line.strip())
if b\"HMAC Key:\" in d: break
s.close()
''')"
```
4. **Installeer de app op de telefoon:**
```bash
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
5. **Open de app → tandwiel-icoon → voer key in → Opslaan**
6. De app scant, paart (Just Works), en verbindt. Houd de HOLD-knop ingedrukt om het relais te schakelen.
## Projectstructuur
```
sylvia-touch-relay/
├── platformio.ini # PlatformIO configuratie (ESP32-S3)
├── tools/
│ └── generate_hmac_key.py # CLI tool: genereert src/hmac_key.h
├── src/
│ ├── main.cpp # Firmware (BLE + HMAC + sensor + relais)
│ └── hmac_key.h # Gegenereerd, niet in git (.gitignore)
├── android/
│ ├── build.gradle # Root Gradle config
│ ├── settings.gradle
│ ├── gradle.properties
│ ├── gradlew # Gradle wrapper
│ ├── gradle/wrapper/
│ │ └── gradle-wrapper.properties
│ └── app/
│ ├── build.gradle # App module config
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/eddy/atomrelay/
│ │ ├── MainActivity.kt # BLE client + HMAC signing
│ │ └── SettingsActivity.kt # HMAC key invoer + oog toggle
│ └── res/
│ ├── drawable/
│ │ ├── ic_eye_open.xml # Oog open icoon
│ │ ├── ic_eye_closed.xml # Oog dicht icoon
│ │ └── ic_settings.xml # Tandwiel icoon
│ ├── layout/
│ │ ├── activity_main.xml # Hoofd UI
│ │ └── activity_settings.xml # Key invoer UI
│ └── values/strings.xml
├── .gitignore
└── README.md
```
## Technische notities
- De Atom VoiceS3R gebruikt de ES8311 audio codec op G45/G0/G48/G4/G3/G17/G11, IR TX op G47, en de knop op G41. Deze pinnen zijn niet beschikbaar voor eigen gebruik.
- G1 en G2 zitten op de HY2.0-4P Grove poort. G5-G8, G38, G39 zitten op de onderste expantie-header.
- De firmware gebruikt geen M5Unified library — alleen Arduino core + ESP32 BLE + mbedtls. Dit maakt de serial output betrouwbaarder via USB CDC.
- `ARDUINO_USB_CDC_ON_BOOT=1` is ingeschakeld voor virtuele serial via USB.
- De HMAC key wordt via `#include "hmac_key.h"` in de firmware geincludeerd. Dit bestand is gegenereerd door `tools/generate_hmac_key.py` en staat in `.gitignore`.
- De app gebruikt `WRITE_TYPE_NO_RESPONSE` voor minimale latency — de encryptie en HMAC verificatie gebeuren op de link layer resp. application layer en zijn onafhankelijk van het write type.
- De sequence counter start bij 1 (niet 0) om te voorkomen dat het eerste packet na connectie als replay wordt geweerd (`0 <= 0` is true in de `seq <= lastSeq` check).
- Für meerdere ESP modules met verschillende keys: genereer per module een eigen key, flash de firmware, en voer de juiste key in de app in. De app ondersteunt momenteel slechtséén key (opgeslagen in SharedPreferences).