From 7f68e9ad701e2472f8b4d0657159569b11940273 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 26 Jul 2026 08:18:52 +0200 Subject: [PATCH] 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 --- .gitignore | 3 + README.md | 180 +++++++++++++++--- android/app/src/main/AndroidManifest.xml | 5 + .../java/com/eddy/atomrelay/MainActivity.kt | 123 +++++++++++- .../com/eddy/atomrelay/SettingsActivity.kt | 90 +++++++++ .../src/main/res/drawable/ic_eye_closed.xml | 10 + .../app/src/main/res/drawable/ic_eye_open.xml | 10 + .../app/src/main/res/drawable/ic_settings.xml | 10 + .../app/src/main/res/layout/activity_main.xml | 10 + .../src/main/res/layout/activity_settings.xml | 76 ++++++++ android/app/src/main/res/values/strings.xml | 4 + src/main.cpp | 89 +++++++-- tools/generate_hmac_key.py | 53 ++++++ 13 files changed, 619 insertions(+), 44 deletions(-) create mode 100644 android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt create mode 100644 android/app/src/main/res/drawable/ic_eye_closed.xml create mode 100644 android/app/src/main/res/drawable/ic_eye_open.xml create mode 100644 android/app/src/main/res/drawable/ic_settings.xml create mode 100644 android/app/src/main/res/layout/activity_settings.xml create mode 100644 tools/generate_hmac_key.py diff --git a/.gitignore b/.gitignore index a955ae2..9eb8713 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# HMAC key (generated, do not commit) +src/hmac_key.h + # PlatformIO .pio/ .pioenvs/ diff --git a/README.md b/README.md index 2485979..e45d5f8 100644 --- a/README.md +++ b/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 : 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. \ No newline at end of file +- 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). \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1bc43b6..13b510c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,5 +24,10 @@ + + \ 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 a27934e..222fd61 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt @@ -11,6 +11,10 @@ import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothProfile import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.os.Bundle import android.os.Handler @@ -18,9 +22,14 @@ import android.os.Looper import android.util.Log import android.view.MotionEvent import android.widget.Button +import android.widget.ImageButton import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import java.util.UUID class MainActivity : AppCompatActivity() { @@ -31,6 +40,8 @@ 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_LEN = 32 } private var bluetoothGatt: BluetoothGatt? = null @@ -38,6 +49,10 @@ class MainActivity : AppCompatActivity() { private var isScanning = false private val handler = Handler(Looper.getMainLooper()) + private var seq = 1 + private var hmacKey: ByteArray? = null + private var pendingDevice: BluetoothDevice? = null + private lateinit var statusText: TextView private lateinit var relayButton: Button private lateinit var scanButton: Button @@ -54,11 +69,39 @@ class MainActivity : AppCompatActivity() { } } + private val bondReceiver = object : BroadcastReceiver() { + @SuppressLint("MissingPermission") + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) { + val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) + Log.d(TAG, "Bond state changed: ${device?.address} -> $bondState") + if (bondState == BluetoothDevice.BOND_BONDED && device == pendingDevice) { + Log.d(TAG, "Bonded, discovering services...") + runOnUiThread { statusText.text = "Bonded, connecting..." } + bluetoothGatt?.discoverServices() + } + } + } + } + private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { - Log.d(TAG, "GATT connected, discovering services...") - gatt.discoverServices() + Log.d(TAG, "GATT connected") + bluetoothGatt = gatt + val device = gatt.device + pendingDevice = device + if (device.bondState == BluetoothDevice.BOND_BONDED) { + Log.d(TAG, "Already bonded, discovering services...") + runOnUiThread { statusText.text = "Connected, discovering..." } + gatt.discoverServices() + } else { + Log.d(TAG, "Not bonded, creating bond...") + runOnUiThread { statusText.text = "Pairing..." } + device.createBond() + } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.d(TAG, "GATT disconnected") runOnUiThread { @@ -68,23 +111,29 @@ class MainActivity : AppCompatActivity() { gatt.close() bluetoothGatt = null relayCharacteristic = null - // Auto-reconnect after 2 seconds + seq = 0 handler.postDelayed({ startScan() }, 2000) } } @SuppressLint("MissingPermission") override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + Log.d(TAG, "Services discovered, requesting MTU...") + gatt.requestMtu(256) + } + + @SuppressLint("MissingPermission") + override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + Log.d(TAG, "MTU changed to $mtu") val service = gatt.getService(SERVICE_UUID) if (service != null) { relayCharacteristic = service.getCharacteristic(CHARACTERISTIC_UUID) if (relayCharacteristic != null) { - // Enable notifications gatt.setCharacteristicNotification(relayCharacteristic, true) val descriptor = relayCharacteristic!!.getDescriptor(CCCD_UUID) descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor) - Log.d(TAG, "Services discovered, relay characteristic found") + Log.d(TAG, "Ready, relay characteristic found") runOnUiThread { statusText.text = "Connected" relayButton.isEnabled = true @@ -112,14 +161,59 @@ 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)) + } setupRelayButton() - // Auto-connect on app start + registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) + handler.postDelayed({ startScan() }, 500) } + override fun onResume() { + super.onResume() + loadHmacKey() + if (hmacKey == null) { + statusText.text = "Geen HMAC sleutel - open Instellingen" + } + } + + private fun loadHmacKey() { + val prefs = getSharedPreferences(SettingsActivity.PREFS_NAME, Context.MODE_PRIVATE) + val hexKey = prefs.getString(SettingsActivity.PREF_KEY, null) + hmacKey = hexKey?.let { hexStringToBytes(it) } + Log.d(TAG, "HMAC key loaded: ${if (hmacKey != null) "${hmacKey!!.size} bytes" else "none"}") + } + + private fun hexStringToBytes(hex: String): ByteArray? { + val cleaned = hex.uppercase().filter { it in "0123456789ABCDEF" } + if (cleaned.length != 64) return null + return cleaned.chunked(2).map { it.toInt(16).toByte() }.toByteArray() + } + + private fun hmacSha256(data: ByteArray, key: ByteArray): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(key, "HmacSHA256")) + return mac.doFinal(data) + } + + private fun buildPacket(cmd: Byte, key: ByteArray): ByteArray { + val seqBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(seq).array() + val hmacData = ByteArray(5) + hmacData[0] = cmd + System.arraycopy(seqBytes, 0, hmacData, 1, 4) + val hmac = hmacSha256(hmacData, key) + val packet = ByteArray(HMAC_PKT_SIZE) + packet[0] = cmd + System.arraycopy(seqBytes, 0, packet, 1, 4) + System.arraycopy(hmac, 0, packet, 5, HMAC_LEN) + return packet + } + @Deprecated("Deprecated in Java") override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) @@ -160,6 +254,11 @@ class MainActivity : AppCompatActivity() { return } + if (hmacKey == null) { + statusText.text = "Geen HMAC sleutel - open Instellingen" + return + } + isScanning = true statusText.text = "Scanning..." scanButton.isEnabled = false @@ -167,7 +266,6 @@ class MainActivity : AppCompatActivity() { adapter.bluetoothLeScanner.startScan(scanCallback) - // Stop scan after 10 seconds handler.postDelayed({ if (isScanning) stopScan() }, 10000) @@ -196,10 +294,15 @@ class MainActivity : AppCompatActivity() { private fun sendRelayCommand(on: Boolean) { val char = relayCharacteristic ?: return val gatt = bluetoothGatt ?: return - char.value = byteArrayOf(if (on) 0x01 else 0x00) + val key = hmacKey ?: return + + val cmd: Byte = if (on) 0x01 else 0x00 + val packet = buildPacket(cmd, key) + char.value = packet char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE gatt.writeCharacteristic(char) - Log.d(TAG, "Sent relay ${if (on) "ON" else "OFF"}") + seq++ + Log.d(TAG, "Sent relay ${if (on) "ON" else "OFF"} (seq=${seq - 1})") } private fun getRequiredPermissions(): Array { @@ -224,9 +327,11 @@ class MainActivity : AppCompatActivity() { ActivityCompat.requestPermissions(this, getRequiredPermissions(), 1) } + @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onDestroy() { super.onDestroy() stopScan() bluetoothGatt?.close() + unregisterReceiver(bondReceiver) } } \ No newline at end of file diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt new file mode 100644 index 0000000..871f364 --- /dev/null +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -0,0 +1,90 @@ +package com.eddy.atomrelay + +import android.content.Context +import android.os.Bundle +import android.text.InputType +import android.text.TextWatcher +import android.text.Editable +import android.widget.Button +import android.widget.EditText +import android.widget.ImageButton +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity + +class SettingsActivity : AppCompatActivity() { + + companion object { + const val PREFS_NAME = "relay_prefs" + const val PREF_KEY = "hmac_key" + } + + private var isVisible = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.title = "Instellingen" + + val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val keyInput = findViewById(R.id.keyInput) + val toggleBtn = findViewById(R.id.toggleVisibility) + val saveButton = findViewById