diff --git a/README.md b/README.md index e45d5f8..07eac23 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ De relay is beveiligd tegen ongeautoriseerde toegang via twee onafhankelijke lag 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) +- 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 @@ -74,10 +74,20 @@ De sequence counter reset naar 0 bij elke (re)connectie op de ESP. De app start - 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 +- **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:`. Geen encryptie nodig — fysieke connectie is de beveiliging. +- 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 Key Transfer Protocol + +``` +App → ESP: KEY?\n +ESP → App: HMAC_KEY:C3F17B612B93E82C56B055FA9B04E84664ACE1EC4E889F2763F271B65A5B54AF\n +``` + +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. + ### Beveiligingsanalyse | Aanval | Hoe geweerd | @@ -169,29 +179,38 @@ 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 - **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) +- **USB auto-read:** Bij aansluiten van de ESP via USB-OTG wordt de HMAC key automatisch gelezen en opgeslagen (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. 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** +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**. +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!" +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 app "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. + +### 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+ (getest met Java 17 en 21; Java 26 werkt niet met Kotlin 1.9 daemon — valt terug op non-incremental) +- Java 17+ (Java 26 werkt niet met Gradle 8.5 — gebruik Java 21) - Gradle 8.2.2 (via wrapper) +- USB host support (usb-serial-for-android library via JitPack) ```bash cd android -./gradlew assembleDebug +JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug ``` De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`. @@ -222,29 +241,23 @@ adb install -r android/app/build/outputs/apk/debug/app-debug.apk 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:** +3. **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** +4. **Voer de HMAC key in (kies één):** + + **Optie A — USB auto-read (aanbevolen):** + - Sluit de ESP via USB-OTG aan op de telefoon + - Android vraagt USB-toestemming (1x) — tik "Toestaan" + - De key wordt automatisch gelezen en opgeslagen (Toast bevestigt) + - Ontkoppel de ESP en zet hem op stroom + + **Optie B — handmatig:** + - Open de app → tandwiel-icoon → voer de hex key in → Opslaan + +5. **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. @@ -256,30 +269,31 @@ sylvia-touch-relay/ ├── tools/ │ └── generate_hmac_key.py # CLI tool: genereert src/hmac_key.h ├── src/ -│ ├── main.cpp # Firmware (BLE + HMAC + sensor + relais) +│ ├── main.cpp # Firmware (BLE + HMAC + USB serial + sensor + relais) │ └── hmac_key.h # Gegenereerd, niet in git (.gitignore) ├── android/ │ ├── build.gradle # Root Gradle config -│ ├── settings.gradle +│ ├── 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 +│ ├── 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 + oog toggle +│ │ ├── 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 +│ │ ├── 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 +│ │ ├── activity_main.xml # Hoofd UI (FrameLayout) +│ │ └── activity_settings.xml # Key invoer + scan UI │ └── values/strings.xml ├── .gitignore └── README.md @@ -294,4 +308,7 @@ sylvia-touch-relay/ - 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). \ No newline at end of file +- 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). +- 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 3s timeout) om ANR's te voorkomen. \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 4981ef8..820dac1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -34,4 +34,5 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.github.mik3y:usb-serial-for-android:3.7.3' } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 13b510c..4bb8a97 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt index 222fd61..41670b2 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt @@ -2,6 +2,7 @@ package com.eddy.atomrelay import android.Manifest import android.annotation.SuppressLint +import android.app.Activity import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt @@ -40,8 +41,9 @@ class MainActivity : AppCompatActivity() { private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8") private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") private const val DEVICE_NAME = "AtomS3R-Relay" - private const val HMAC_PKT_SIZE = 37 // 1 cmd + 4 seq + 32 hmac + private const val HMAC_PKT_SIZE = 37 private const val HMAC_LEN = 32 + private const val REQUEST_SETTINGS = 1 } private var bluetoothGatt: BluetoothGatt? = null @@ -55,7 +57,6 @@ class MainActivity : AppCompatActivity() { private lateinit var statusText: TextView private lateinit var relayButton: Button - private lateinit var scanButton: Button private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { @@ -111,7 +112,7 @@ class MainActivity : AppCompatActivity() { gatt.close() bluetoothGatt = null relayCharacteristic = null - seq = 0 + seq = 1 handler.postDelayed({ startScan() }, 2000) } } @@ -160,18 +161,22 @@ class MainActivity : AppCompatActivity() { statusText = findViewById(R.id.statusText) relayButton = findViewById(R.id.relayButton) - scanButton = findViewById(R.id.scanButton) val settingsButton = findViewById(R.id.settingsButton) - scanButton.setOnClickListener { startScan() } settingsButton.setOnClickListener { - startActivity(Intent(this, SettingsActivity::class.java)) + startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS) } setupRelayButton() registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) + } - handler.postDelayed({ startScan() }, 500) + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_SETTINGS && resultCode == Activity.RESULT_OK) { + handler.postDelayed({ startScan() }, 300) + } } override fun onResume() { @@ -179,6 +184,8 @@ class MainActivity : AppCompatActivity() { loadHmacKey() if (hmacKey == null) { statusText.text = "Geen HMAC sleutel - open Instellingen" + } else if (!isScanning && bluetoothGatt == null) { + handler.postDelayed({ startScan() }, 300) } } @@ -261,7 +268,6 @@ class MainActivity : AppCompatActivity() { isScanning = true statusText.text = "Scanning..." - scanButton.isEnabled = false Log.d(TAG, "Starting BLE scan for $DEVICE_NAME") adapter.bluetoothLeScanner.startScan(scanCallback) @@ -275,7 +281,6 @@ class MainActivity : AppCompatActivity() { private fun stopScan() { if (!isScanning) return isScanning = false - scanButton.isEnabled = true val adapter = BluetoothAdapter.getDefaultAdapter() adapter?.bluetoothLeScanner?.stopScan(scanCallback) if (statusText.text == "Scanning...") { diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index 871f364..99cd006 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -16,6 +16,7 @@ class SettingsActivity : AppCompatActivity() { companion object { const val PREFS_NAME = "relay_prefs" const val PREF_KEY = "hmac_key" + const val RESULT_SCAN = 100 } private var isVisible = false @@ -31,7 +32,7 @@ class SettingsActivity : AppCompatActivity() { val keyInput = findViewById(R.id.keyInput) val toggleBtn = findViewById(R.id.toggleVisibility) val saveButton = findViewById