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
This commit is contained in:
parent
7c9dde172a
commit
7f68e9ad70
13 changed files with 621 additions and 46 deletions
180
README.md
180
README.md
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
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 |
|
||||
|
|
@ -35,9 +37,60 @@ 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.
|
||||
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
|
||||
|
||||
|
|
@ -46,10 +99,10 @@ 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 `0x01` | 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 onafhankelijk. Als de sensor losgelaten wordt én de BLE-knop niet ingedrukt is, gaat het 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
|
||||
|
||||
|
|
@ -58,21 +111,41 @@ De touch sensor en BLE werken onafhankelijk. Als de sensor losgelaten wordt én
|
|||
| Service | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` |
|
||||
| Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d8` |
|
||||
|
||||
De characteristic ondersteunt `WRITE` en `NOTIFY`. Schrijf `0x01` om het relais aan te zetten, `0x00` om uit te zetten.
|
||||
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 en logt statuswijzigingen:
|
||||
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)
|
||||
Relay OFF (touch=no, ble=connected)
|
||||
BLE: relay ON (seq=1)
|
||||
BLE: relay OFF (seq=2)
|
||||
```
|
||||
|
||||
### Uploaden
|
||||
|
|
@ -80,6 +153,7 @@ Relay OFF (touch=no, ble=connected)
|
|||
> **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
|
||||
```
|
||||
|
||||
|
|
@ -92,18 +166,32 @@ Na het uploaden treedt er een harde reset op. Als het board in download-mode bli
|
|||
- **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 21
|
||||
- Gradle 8.5
|
||||
- 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
|
||||
JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||
|
|
@ -121,37 +209,77 @@ adb connect <ip>:<poort>
|
|||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
### UUID's
|
||||
## Eerste keer setup
|
||||
|
||||
De app gebruikt dezelfde UUID's als de firmware:
|
||||
1. **Genereer HMAC key:**
|
||||
```bash
|
||||
python3 tools/generate_hmac_key.py
|
||||
```
|
||||
Noteer de geprinte hex key.
|
||||
|
||||
```kotlin
|
||||
private val SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
|
||||
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
|
||||
```
|
||||
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)
|
||||
├── platformio.ini # PlatformIO configuratie (ESP32-S3)
|
||||
├── tools/
|
||||
│ └── generate_hmac_key.py # CLI tool: genereert src/hmac_key.h
|
||||
├── src/
|
||||
│ └── main.cpp # Firmware (BLE + sensor + relais)
|
||||
│ ├── main.cpp # Firmware (BLE + HMAC + sensor + relais)
|
||||
│ └── hmac_key.h # Gegenereerd, niet in git (.gitignore)
|
||||
├── android/
|
||||
│ ├── build.gradle # Root Gradle config
|
||||
│ ├── build.gradle # Root Gradle config
|
||||
│ ├── settings.gradle
|
||||
│ ├── gradle.properties
|
||||
│ ├── gradlew # Gradle wrapper
|
||||
│ ├── gradlew # Gradle wrapper
|
||||
│ ├── gradle/wrapper/
|
||||
│ │ └── gradle-wrapper.properties
|
||||
│ └── app/
|
||||
│ ├── build.gradle # App module config
|
||||
│ ├── build.gradle # App module config
|
||||
│ └── src/main/
|
||||
│ ├── AndroidManifest.xml
|
||||
│ ├── java/com/eddy/atomrelay/
|
||||
│ │ └── MainActivity.kt
|
||||
│ │ ├── MainActivity.kt # BLE client + HMAC signing
|
||||
│ │ └── SettingsActivity.kt # HMAC key invoer + oog toggle
|
||||
│ └── res/
|
||||
│ ├── layout/activity_main.xml
|
||||
│ ├── 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
|
||||
|
|
@ -161,5 +289,9 @@ sylvia-touch-relay/
|
|||
|
||||
- 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. Dit maakt de serial output betrouwbaarder via USB CDC.
|
||||
- `ARDUINO_USB_CDC_ON_BOOT=1` is ingeschakeld voor virtuele serial via USB.
|
||||
- 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).
|
||||
Loading…
Reference in a new issue