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
This commit is contained in:
eddy 2026-07-26 09:30:21 +02:00
commit 9a84b05b8a
5 changed files with 155 additions and 116 deletions

3
.gitignore vendored
View file

@ -1,6 +1,3 @@
# HMAC key (generated, do not commit)
src/hmac_key.h
# PlatformIO # PlatformIO
.pio/ .pio/
.pioenvs/ .pioenvs/

View file

@ -71,22 +71,26 @@ De sequence counter reset naar 0 bij elke (re)connectie op de ESP. De app start
#### Key management #### Key management
- De HMAC key staat in `src/hmac_key.h` (niet in git, `.gitignore`) - De HMAC key wordt **niet meer meegecompileerd** — de ESP start zonder key en wordt geprovisioneerd via USB
- Een CLI-tool genereert een willekeurige key: `python3 tools/generate_hmac_key.py` - De key wordt opgeslagen in **NVS** (Non-Volatile Storage) op de ESP via de Arduino `Preferences` library
- De ESP print de key 5 seconden na opstarten naar serial - **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.
- **USB auto-read:** Sluit de ESP via USB-OTG aan op de telefoon → de app leest de key automatisch via USB serial (CDC-ACM). De ESP reageert op het commando `KEY?\n` met `HMAC_KEY:<hex>`. Geen encryptie nodig — fysieke connectie is de beveiliging. - 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) - Handmatige invoer: via het instellingenscherm in de Android app (64-teken hex string)
- De app slaat de key op in SharedPreferences (overleeft app-herstarts) - 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 - Zo kan dezelfde app gebruikt worden voor meerdere ESP modules — each met een eigen key
#### USB Serial Key Transfer Protocol #### USB Serial Protocol
``` ```
App → ESP: KEY?\n App → ESP: KEY?\n
ESP → App: HMAC_KEY:C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF\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. 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, stuurt `KEY?\n`, en slaat de respons op in SharedPreferences. 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 ### Beveiligingsanalyse
@ -123,33 +127,31 @@ De touch sensor en BLE werken onafzankelijk. Als de sensor losgelaten wordt en d
De characteristic ondersteunt `WRITE` (encrypted) en `NOTIFY`. Writes vereisen een versleutelde link (bonding) plus HMAC-authenticatie. De characteristic ondersteunt `WRITE` (encrypted) en `NOTIFY`. Writes vereisen een versleutelde link (bonding) plus HMAC-authenticatie.
### HMAC Key genereren ### Key provisioning
Voordat de firmware gecompileerd kan worden, moet er een HMAC key gegenereerd worden: De firmware wordt zonder HMAC key gecompileerd. Bij de eerste opstart print de ESP:
```bash ```
python3 tools/generate_hmac_key.py No HMAC key provisioned — connect USB and run SET_KEY
``` ```
Dit maakt `src/hmac_key.h` aan met een willekeurige 32-byte key en print de hex-representatie. Een bestaande key overschrijven: 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:
```bash
echo "y" | python3 tools/generate_hmac_key.py
``` ```
HMAC key loaded from NVS
> **Belangrijk:** `src/hmac_key.h` staat in `.gitignore` en wordt niet gecommit. De key moet op de ESP en in de app identiek zijn. HMAC Key: C3F17B...AF
```
### Serial monitor ### 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: 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 === === Atom VoiceS3R BLE Relay ===
HMAC key loaded from NVS
LED blink done, starting BLE... LED blink done, starting BLE...
BLE: advertising started as 'AtomS3R-Relay' BLE: advertising started as 'AtomS3R-Relay'
Printing HMAC key in 5 seconds...
HMAC Key: C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF HMAC Key: C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF
Enter this key in the Android app > Settings.
Setup done. Setup done.
[heartbeat] relay=OFF touch=no ble=disconnected [heartbeat] relay=OFF touch=no ble=disconnected
BLE: device connected BLE: device connected
@ -163,7 +165,6 @@ BLE: relay OFF (seq=2)
> **Download-mode:** Houd de reset-knop ~2 seconden ingedrukt tot de groene LED aangaat, laat dan los. Het board is nu in download-mode. > **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 ```bash
# Zorg dat src/hmac_key.h bestaat (zo niet: python3 tools/generate_hmac_key.py)
pio run --target upload pio run --target upload
``` ```
@ -179,7 +180,7 @@ Na het uploaden treedt er een harde reset op. Als het board in download-mode bli
- **BLE Pairing:** De app initieert bonding (`createBond()`) na connectie; Just Works pairing verloopt zonder gebruikersinteractie - **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) - **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 - **HMAC authenticatie:** Elk commando wordt ondertekend met HMAC-SHA256 + sequence counter
- **USB auto-read:** Bij aansluiten van de ESP via USB-OTG wordt de HMAC key automatisch gelezen en opgeslagen (BroadcastReceiver) - **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 - **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+) - **Permissies:** Vraagt BLUETOOTH_SCAN, BLUETOOTH_CONNECT en ACCESS_FINE_LOCATION aan (Android 12+)
@ -188,8 +189,8 @@ Na het uploaden treedt er een harde reset op. Als het board in download-mode bli
1. Open de app 1. Open de app
2. Tik op het **tandwiel-icoon** rechtsboven 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."_ 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 (zelfde als de ESP op serial print). Tik op het **oog-icoon** om de key te tonen/verbergen. Tik **Opslaan**. 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:** Sluit de ESP via USB-OTG aan op de telefoon. Android vraagt USB-toestemming (1x). De key wordt automatisch gelezen en opgeslagen. Toast: "HMAC sleutel gelezen van ESP!" 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. 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. 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.
@ -206,7 +207,7 @@ Vereisten:
- Android SDK (API 34+) - Android SDK (API 34+)
- Java 17+ (Java 26 werkt niet met Gradle 8.5 — gebruik Java 21) - Java 17+ (Java 26 werkt niet met Gradle 8.5 — gebruik Java 21)
- Gradle 8.2.2 (via wrapper) - Gradle 8.2.2 (via wrapper)
- USB host support (usb-serial-for-android library via JitPack) - USB host support (raw Android USB API, geen externe library)
```bash ```bash
cd android cd android
@ -230,47 +231,40 @@ adb install -r android/app/build/outputs/apk/debug/app-debug.apk
## Eerste keer setup ## Eerste keer setup
1. **Genereer HMAC key:** 1. **Flash de ESP:**
```bash
python3 tools/generate_hmac_key.py
```
Noteer de geprinte hex key.
2. **Flash de ESP:**
```bash ```bash
pio run -t upload pio run -t upload
``` ```
3. **Installeer de app op de telefoon:** 2. **Installeer de app op de telefoon:**
```bash ```bash
adb install -r android/app/build/outputs/apk/debug/app-debug.apk adb install -r android/app/build/outputs/apk/debug/app-debug.apk
``` ```
4. **Voer de HMAC key in (kies één):** 3. **Provision de HMAC key (kies één):**
**Optie A — USB auto-read (aanbevolen):** **Optie A — USB auto-provisioning (aanbevolen):**
- Sluit de ESP via USB-OTG aan op de telefoon - Sluit de ESP via USB-OTG aan op de telefoon
- Android vraagt USB-toestemming (1x) — tik "Toestaan" - Android vraagt USB-toestemming (1x) — tik "Toestaan"
- De key wordt automatisch gelezen en opgeslagen (Toast bevestigt) - 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 - Ontkoppel de ESP en zet hem op stroom
**Optie B — handmatig:** **Optie B — handmatig:**
- Open de app → tandwiel-icoon → voer de hex key in → Opslaan - 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>`)
5. **Scan & Verbind:** Tik in instellingen op "Scan & Verbind" (of de app scant automatisch bij terugkeren naar het hoofdscherm) 4. **Scan & Verbind:** Tik in instellingen op "Scan & Verbind" (of de app scant automatisch bij terugkeren naar het hoofdscherm)
6. De app scant, paart (Just Works), en verbindt. Houd de HOLD-knop ingedrukt om het relais te schakelen. 5. De app scant, paart (Just Works), en verbindt. Houd de HOLD-knop ingedrukt om het relais te schakelen.
## Projectstructuur ## Projectstructuur
``` ```
sylvia-touch-relay/ 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/ ├── src/
│ ├── main.cpp # Firmware (BLE + HMAC + USB serial + sensor + relais) │ └── main.cpp # Firmware (BLE + HMAC + NVS key storage + USB serial + sensor + relais)
│ └── hmac_key.h # Gegenereerd, niet in git (.gitignore)
├── android/ ├── android/
│ ├── build.gradle # Root Gradle config │ ├── build.gradle # Root Gradle config
│ ├── settings.gradle # JitPack repos voor usb-serial-for-android │ ├── settings.gradle # JitPack repos voor usb-serial-for-android
@ -305,10 +299,9 @@ sylvia-touch-relay/
- G1 en G2 zitten op de HY2.0-4P Grove poort. G5-G8, G38, G39 zitten op de onderste expantie-header. - 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. - 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. - `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 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 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). - 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). - 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` 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 USB serial library (`usb-serial-for-android` v3.7.3 via JitPack) ondersteunt CDC-ACM (ESP32-S3), FTDI, CP210x, CH340, PL2303, en meer. - De `UsbKeyReceiver` doet serial I/O op een achtergrondthread (max 5s timeout per poging, 5 retries) om ANR's te voorkomen.
- De `UsbKeyReceiver` doet serial I/O op een achtergrondthread (max 3s timeout) om ANR's te voorkomen.

View file

@ -15,6 +15,7 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.SecureRandom
class UsbKeyReceiver : BroadcastReceiver() { class UsbKeyReceiver : BroadcastReceiver() {
@ -26,6 +27,7 @@ class UsbKeyReceiver : BroadcastReceiver() {
private const val MAX_RETRIES = 5 private const val MAX_RETRIES = 5
private const val RETRY_DELAY_MS = 1000L private const val RETRY_DELAY_MS = 1000L
private const val CDC_DATA_INTERFACE_CLASS = 0x0A private const val CDC_DATA_INTERFACE_CLASS = 0x0A
private const val SET_KEY_TIMEOUT_MS = 3000
} }
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@ -70,6 +72,13 @@ class UsbKeyReceiver : BroadcastReceiver() {
return usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID } return usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }
} }
private fun generateRandomHexKey(): String {
val random = SecureRandom()
val bytes = ByteArray(32)
random.nextBytes(bytes)
return bytes.joinToString("") { "%02X".format(it) }
}
private fun requestPermission(context: Context, usbManager: UsbManager, device: UsbDevice) { private fun requestPermission(context: Context, usbManager: UsbManager, device: UsbDevice) {
val flags = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { val flags = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
@ -186,7 +195,7 @@ class UsbKeyReceiver : BroadcastReceiver() {
val len = connection.bulkTransfer(epIn, buf, buf.size, 500) val len = connection.bulkTransfer(epIn, buf, buf.size, 500)
if (len > 0) { if (len > 0) {
sb.append(String(buf, 0, len, StandardCharsets.US_ASCII)) sb.append(String(buf, 0, len, StandardCharsets.US_ASCII))
if (sb.contains("HMAC_KEY:") && sb.contains("\n")) { if ((sb.contains("HMAC_KEY:") || sb.contains("NO_KEY")) && sb.contains("\n")) {
break break
} }
} }
@ -196,6 +205,8 @@ class UsbKeyReceiver : BroadcastReceiver() {
Log.d(TAG, "Serial response: ${response.take(200)}") Log.d(TAG, "Serial response: ${response.take(200)}")
val keyLine = response.lines().firstOrNull { it.startsWith("HMAC_KEY:") } val keyLine = response.lines().firstOrNull { it.startsWith("HMAC_KEY:") }
val noKey = response.lines().any { it.trim() == "NO_KEY" }
if (keyLine != null) { if (keyLine != null) {
val hexKey = keyLine.removePrefix("HMAC_KEY:").trim() val hexKey = keyLine.removePrefix("HMAC_KEY:").trim()
if (hexKey.length == 64 && hexKey.all { it in "0123456789ABCDEFabcdef" }) { if (hexKey.length == 64 && hexKey.all { it in "0123456789ABCDEFabcdef" }) {
@ -212,6 +223,45 @@ class UsbKeyReceiver : BroadcastReceiver() {
handler.post { Toast.makeText(context, "Ongeldig sleutel formaat", Toast.LENGTH_SHORT).show() } handler.post { Toast.makeText(context, "Ongeldig sleutel formaat", Toast.LENGTH_SHORT).show() }
return return
} }
} else if (noKey) {
Log.d(TAG, "ESP has no key — generating and provisioning")
val newKey = generateRandomHexKey()
Log.d(TAG, "Generated key: $newKey")
val setKeyCmd = "SET_KEY:$newKey\n".toByteArray(StandardCharsets.US_ASCII)
val written = connection.bulkTransfer(epOut, setKeyCmd, setKeyCmd.size, 1000)
Log.d(TAG, "Sent SET_KEY (${written} bytes)")
Thread.sleep(300)
val okBuf = ByteArray(256)
val okSb = StringBuilder()
val okStart = System.currentTimeMillis()
while (System.currentTimeMillis() - okStart < SET_KEY_TIMEOUT_MS) {
val len = connection.bulkTransfer(epIn, okBuf, okBuf.size, 500)
if (len > 0) {
okSb.append(String(okBuf, 0, len, StandardCharsets.US_ASCII))
if (okSb.contains("OK") && okSb.contains("\n")) break
}
}
Log.d(TAG, "SET_KEY response: ${okSb.toString().take(100)}")
if (okSb.contains("OK")) {
Log.d(TAG, "Key provisioned successfully, saving to prefs")
val prefs = context.getSharedPreferences(SettingsActivity.PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(SettingsActivity.PREF_KEY, newKey).apply()
handler.post {
Toast.makeText(context, "Nieuwe HMAC sleutel gegenereerd en naar ESP geschreven!", Toast.LENGTH_LONG).show()
}
return
} else {
Log.w(TAG, "SET_KEY failed, no OK response")
connection.close()
connection = null
if (attempt < MAX_RETRIES) {
Thread.sleep(RETRY_DELAY_MS)
}
}
} else { } else {
Log.w(TAG, "Attempt $attempt: no HMAC_KEY in response, will retry") Log.w(TAG, "Attempt $attempt: no HMAC_KEY in response, will retry")
connection.close() connection.close()

View file

@ -5,7 +5,7 @@
#include <BLE2902.h> #include <BLE2902.h>
#include <BLESecurity.h> #include <BLESecurity.h>
#include <mbedtls/md.h> #include <mbedtls/md.h>
#include "hmac_key.h" #include <Preferences.h>
#define SENSOR_PIN 38 #define SENSOR_PIN 38
#define RELAY_PIN 1 #define RELAY_PIN 1
@ -15,6 +15,9 @@
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
#define HMAC_PKT_SIZE 37 // 1 cmd + 4 seq + 32 hmac #define HMAC_PKT_SIZE 37 // 1 cmd + 4 seq + 32 hmac
#define HMAC_KEY_LEN 32
#define NVS_NAMESPACE "hmac"
#define NVS_KEY_NAME "key"
BLECharacteristic *pCharacteristic; BLECharacteristic *pCharacteristic;
bool deviceConnected = false; bool deviceConnected = false;
@ -22,6 +25,10 @@ bool bleCommand = false;
bool relayState = false; bool relayState = false;
uint32_t lastSeq = 0; uint32_t lastSeq = 0;
uint8_t hmacKey[HMAC_KEY_LEN];
bool keyProvisioned = false;
Preferences preferences;
class MyServerCallbacks : public BLEServerCallbacks { class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) override { void onConnect(BLEServer *pServer) override {
deviceConnected = true; deviceConnected = true;
@ -58,7 +65,7 @@ static bool verifyHmac(const uint8_t *packet) {
uint8_t computed[32]; uint8_t computed[32];
const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_hmac(mdInfo, HMAC_KEY, 32, hmacData, 5, computed); mbedtls_md_hmac(mdInfo, hmacKey, HMAC_KEY_LEN, hmacData, 5, computed);
if (memcmp(computed, packet + 5, 32) != 0) { if (memcmp(computed, packet + 5, 32) != 0) {
Serial.println("BLE: HMAC mismatch"); Serial.println("BLE: HMAC mismatch");
@ -69,6 +76,20 @@ static bool verifyHmac(const uint8_t *packet) {
return true; return true;
} }
static bool isHexKey(const String &hex) {
for (int i = 0; i < hex.length(); i++) {
char c = hex.charAt(i);
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return false;
}
return true;
}
static void parseHexKey(const String &hex, uint8_t *out) {
for (int i = 0; i < HMAC_KEY_LEN; i++) {
out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16);
}
}
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks { class MyCharacteristicCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pChar) override { void onWrite(BLECharacteristic *pChar) override {
std::string value = pChar->getValue(); std::string value = pChar->getValue();
@ -103,6 +124,18 @@ void setup() {
delay(500); delay(500);
Serial.println("=== Atom VoiceS3R BLE Relay ==="); Serial.println("=== Atom VoiceS3R BLE Relay ===");
preferences.begin(NVS_NAMESPACE, true);
size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
preferences.end();
if (len == HMAC_KEY_LEN) {
keyProvisioned = true;
Serial.println("HMAC key loaded from NVS");
} else {
keyProvisioned = false;
Serial.println("No HMAC key provisioned — connect USB and run SET_KEY");
}
pinMode(SENSOR_PIN, INPUT_PULLUP); pinMode(SENSOR_PIN, INPUT_PULLUP);
pinMode(RELAY_PIN, OUTPUT); pinMode(RELAY_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT);
@ -145,14 +178,15 @@ void setup() {
Serial.println("BLE: advertising started as 'AtomS3R-Relay'"); Serial.println("BLE: advertising started as 'AtomS3R-Relay'");
Serial.println("Printing HMAC key in 5 seconds..."); if (keyProvisioned) {
delay(5000); Serial.print("HMAC Key: ");
Serial.print("HMAC Key: "); for (int i = 0; i < HMAC_KEY_LEN; i++) {
for (int i = 0; i < 32; i++) { Serial.printf("%02X", hmacKey[i]);
Serial.printf("%02X", HMAC_KEY[i]); }
Serial.println();
} else {
Serial.println("No HMAC key — use USB SET_KEY to provision");
} }
Serial.println();
Serial.println("Enter this key in the Android app > Settings.");
Serial.println("Setup done."); Serial.println("Setup done.");
} }
@ -165,11 +199,29 @@ void loop() {
if (c == '\n') { if (c == '\n') {
serialBuf.trim(); serialBuf.trim();
if (serialBuf == "KEY?") { if (serialBuf == "KEY?") {
Serial.print("HMAC_KEY:"); if (keyProvisioned) {
for (int i = 0; i < 32; i++) { Serial.print("HMAC_KEY:");
Serial.printf("%02X", HMAC_KEY[i]); for (int i = 0; i < HMAC_KEY_LEN; i++) {
Serial.printf("%02X", hmacKey[i]);
}
Serial.println();
} else {
Serial.println("NO_KEY");
}
} else if (serialBuf.startsWith("SET_KEY:")) {
String hex = serialBuf.substring(8);
hex.toUpperCase();
if (hex.length() == 64 && isHexKey(hex)) {
parseHexKey(hex, hmacKey);
preferences.begin(NVS_NAMESPACE, false);
preferences.putBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
preferences.end();
keyProvisioned = true;
Serial.println("OK");
Serial.println("HMAC key stored in NVS");
} else {
Serial.println("ERROR: invalid key format");
} }
Serial.println();
} }
serialBuf = ""; serialBuf = "";
} }

View file

@ -1,53 +0,0 @@
#!/usr/bin/env python3
"""Generate src/hmac_key.h with a random 32-byte HMAC-SHA256 key.
Usage:
python3 tools/generate_hmac_key.py
This creates src/hmac_key.h (gitignored) and prints the hex key to stdout.
The hex key should be entered in the Android app's Settings screen.
"""
import os
import sys
import binascii
KEY_LENGTH = 32
OUTPUT_FILE = os.path.join("src", "hmac_key.h")
def generate():
key = os.urandom(KEY_LENGTH)
hex_key = binascii.hexlify(key).decode().upper()
header = f"""#ifndef HMAC_KEY_H
#define HMAC_KEY_H
// AUTO-GENERATED by tools/generate_hmac_key.py
// Do NOT commit this file. Do NOT edit by hand.
// To regenerate: python3 tools/generate_hmac_key.py
const uint8_t HMAC_KEY[{KEY_LENGTH}] = {{
{', '.join(f'0x{b:02x}' for b in key)}
}};
#endif // HMAC_KEY_H
"""
os.makedirs("src", exist_ok=True)
with open(OUTPUT_FILE, "w") as f:
f.write(header)
print(f"HMAC key written to {OUTPUT_FILE}")
print(f"Enter this key in the Android app Settings:")
print(f" {hex_key}")
print()
print(f"ESP serial output after boot will show the same key.")
if __name__ == "__main__":
if os.path.exists(OUTPUT_FILE):
overwrite = input(f"{OUTPUT_FILE} already exists. Overwrite? [y/N] ")
if overwrite.lower() != 'y':
print("Aborted.")
sys.exit(0)
generate()