No description
  • Kotlin 58%
  • C++ 31.3%
  • Python 5.2%
  • Shell 5.2%
  • C 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
eddy 9a84b05b8a Replace compile-time HMAC key with USB provisioning
ESP firmware:
- Store HMAC key in NVS via Preferences library instead of compile-time
  hmac_key.h
- KEY? responds with HMAC_KEY:<hex> (if provisioned) or NO_KEY
- New SET_KEY:<64-hex> command stores key in NVS and responds OK
- Boot messages updated to reflect provisioning state

Android app:
- UsbKeyReceiver detects NO_KEY response and auto-provisions:
  generates random 32-byte key via SecureRandom, sends SET_KEY,
  waits for OK, saves key to SharedPreferences
- Read loop now breaks on both HMAC_KEY: and NO_KEY responses

Cleanup:
- Removed tools/generate_hmac_key.py and src/hmac_key.h
- Removed hmac_key.h from .gitignore
- README updated for new provisioning flow
2026-07-26 09:30:21 +02:00
android Replace compile-time HMAC key with USB provisioning 2026-07-26 09:30:21 +02:00
src Replace compile-time HMAC key with USB provisioning 2026-07-26 09:30:21 +02:00
.gitignore Replace compile-time HMAC key with USB provisioning 2026-07-26 09:30:21 +02:00
platformio.ini Initial commit: BLE relay controller for Atom VoiceS3R 2026-07-26 01:17:31 +02:00
README.md Replace compile-time HMAC key with USB provisioning 2026-07-26 09:30:21 +02:00

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:

  • 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:

  • Zowel de ESP firmware als 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 wordt niet meer meegecompileerd — de ESP start zonder key en wordt geprovisioneerd via USB
  • De key wordt opgeslagen in NVS (Non-Volatile Storage) op de ESP via de Arduino Preferences library
  • USB auto-provisioning: Sluit de ESP via USB-OTG aan op de telefoon → de app stuurt KEY?\n. Als de ESP nog geen key heeft, antwoordt hij met NO_KEY. De app genereert dan een willekeurige 32-byte key (via SecureRandom), stuurt SET_KEY:<hex>\n naar de ESP, en slaat de key both op de ESP (NVS) als lokaal (SharedPreferences) op.
  • Als de ESP al wel een key heeft, antwoordt hij met HMAC_KEY:<hex> en de app slaat die op
  • Handmatige invoer: via het instellingenscherm in de Android app (64-teken hex string)
  • 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

USB Serial Protocol

App → ESP:  KEY?\n
ESP → App:  HMAC_KEY:C3F17B...AF\n     (als key bekend)
ESP → App:  NO_KEY\n                    (als ESP nog geen key heeft)

App → ESP:  SET_KEY:C3F17B...AF\n       (nieuwe key schrijven)
ESP → App:  OK\n                        (bevestiging)

De ESP luistert in loop() voor USB serial data. Bij KEY?\n antwoordt hij met de hex key (HMAC_KEY:<hex>) of NO_KEY. Bij SET_KEY:<64-hex>\n slaat hij de key op in NVS en antwoordt OK. De Android UsbKeyReceiver (manifest-geregistreerde BroadcastReceiver) vangt USB_DEVICE_ATTACHED op, filtert op Espressif (vendor 0x303a), vraagt USB-permission, opent een CDC-ACM serial connectie, en handelt het hele provisioning-proces automatisch af.

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.

Key provisioning

De firmware wordt zonder HMAC key gecompileerd. Bij de eerste opstart print de ESP:

No HMAC key provisioned — connect USB and run SET_KEY

Sluit de ESP via USB aan op de telefoon met de app geinstalleerd. De app genereert automatisch een willekeurige key en stuurt deze naar de ESP. Na provisioning print de ESP bij opstarten:

HMAC key loaded from NVS
HMAC Key: C3F17B...AF

Serial monitor

Baudrate: 115200 (USB CDC). De firmware stuurt heartbeat-statussen elke 5 seconden, logt statuswijzigingen, en print de HMAC key (of de afwezigheid ervan) bij opstarten:

=== Atom VoiceS3R BLE Relay ===
HMAC key loaded from NVS
LED blink done, starting BLE...
BLE: advertising started as 'AtomS3R-Relay'
HMAC Key: C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF
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.

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
  • USB auto-provisioning: Bij aansluiten van de ESP via USB-OTG genereert de app een key als de ESP er nog geen heeft, of leest de bestaande key uit (BroadcastReceiver)
  • Instellingenscherm: Losse Activity voor het invoeren van de HMAC key (met oog-icoon om de key te tonen/verbergen), "Scan & Verbind" knop, en hint-tekst
  • 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. De app toont een hint: "Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon."
  4. Optie A — handmatig: Voer de 64-teken hex key in. Tik op het oog-icoon om de key te tonen/verbergen. Tik Opslaan.
  5. Optie B — automatisch (aanbevolen): Sluit de ESP via USB-OTG aan op de telefoon. Android vraagt USB-toestemming (1x). De app leest de key uit of genereert een nieuwe als de ESP er nog geen heeft. Toast bevestigt de actie.
  6. Tik Scan & Verbind om op te slaan en direct BLE scannen te starten.

De key wordt opgeslagen in SharedPreferences en overleeft app-herstarts. Bij geen key toont de hoofdapp "Geen HMAC sleutel - open Instellingen" en wordt er niet gescant.

Hoofdscherm UI

  • Tandwiel-icoon rechtsboven → opent instellingenscherm
  • HOLD-knop centrum → houd ingedrukt voor relais aan
  • Status tekst linksonder (14sp) → toont verbindingsstatus

Bouwen

Vereisten:

  • Android SDK (API 34+)
  • Java 17+ (Java 26 werkt niet met Gradle 8.5 — gebruik Java 21)
  • Gradle 8.2.2 (via wrapper)
  • USB host support (raw Android USB API, geen externe library)
cd android
JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug

De APK staat na build in android/app/build/outputs/apk/debug/app-debug.apk.

Installeren

adb install -r android/app/build/outputs/apk/debug/app-debug.apk

Of via draadloze foutopsporing:

adb connect <ip>:<poort>
adb install -r android/app/build/outputs/apk/debug/app-debug.apk

Eerste keer setup

  1. Flash de ESP:

    pio run -t upload
    
  2. Installeer de app op de telefoon:

    adb install -r android/app/build/outputs/apk/debug/app-debug.apk
    
  3. Provision de HMAC key (kies één):

    Optie A — USB auto-provisioning (aanbevolen):

    • Sluit de ESP via USB-OTG aan op de telefoon
    • Android vraagt USB-toestemming (1x) — tik "Toestaan"
    • De app detecteert dat de ESP geen key heeft, genereert er één, en stuurt deze naar de ESP
    • Toast: "Nieuwe HMAC sleutel gegenereerd en naar ESP geschreven!"
    • Ontkoppel de ESP en zet hem op stroom

    Optie B — handmatig:

    • Open de app → tandwiel-icoon → voer een 64-teken hex key in → Opslaan
    • Let op: de ESP moet dan dezelfde key hebben (via USB provisioning of handmatig via serial SET_KEY:<hex>)
  4. Scan & Verbind: Tik in instellingen op "Scan & Verbind" (of de app scant automatisch bij terugkeren naar het hoofdscherm)

  5. 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)
├── src/
│   └── main.cpp                # Firmware (BLE + HMAC + NVS key storage + USB serial + sensor + relais)
├── android/
│   ├── build.gradle            # Root Gradle config
│   ├── settings.gradle         # JitPack repos voor usb-serial-for-android
│   ├── gradle.properties
│   ├── gradlew                 # Gradle wrapper
│   ├── gradle/wrapper/
│   │   └── gradle-wrapper.properties
│   └── app/
│       ├── build.gradle        # App module config (+ usb-serial-for-android dep)
│       └── src/main/
│           ├── AndroidManifest.xml
│           ├── java/com/eddy/atomrelay/
│           │   ├── MainActivity.kt       # BLE client + HMAC signing
│           │   ├── SettingsActivity.kt    # HMAC key invoer + scan knop
│           │   └── UsbKeyReceiver.kt     # USB auto-read BroadcastReceiver
│           └── 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 (FrameLayout)
│               │   └── activity_settings.xml    # Key invoer + scan 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 opgeslagen in NVS (Non-Volatile Storage) via de Arduino Preferences library. De key wordt niet meegecompileerd maar via USB provisioning door de Android app op de ESP gezet.
  • 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: sluit elke ESP één voor één aan op de telefoon. De app genereert per ESP een eigen key en slaat deze op. De app ondersteunt momenteel slechtséén key tegelijk (opgeslagen in SharedPreferences).
  • De UsbKeyReceiver is een manifest-geregistreerde BroadcastReceiver voor ACTION_USB_DEVICE_ATTACHED. Hij filtert op Espressif (vendor 0x303a), vraagt USB-permission via PendingIntent, opent een CDC-ACM serial connectie (115200), stuurt KEY?\n, en slaat de respons op. Werkt ook als de app niet actief is.
  • De UsbKeyReceiver doet serial I/O op een achtergrondthread (max 5s timeout per poging, 5 retries) om ANR's te voorkomen.