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
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
|||
# HMAC key (generated, do not commit)
|
||||
src/hmac_key.h
|
||||
|
||||
# PlatformIO
|
||||
.pio/
|
||||
.pioenvs/
|
||||
|
|
|
|||
170
README.md
170
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,22 +209,55 @@ 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:
|
||||
|
||||
```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")
|
||||
1. **Genereer HMAC key:**
|
||||
```bash
|
||||
python3 tools/generate_hmac_key.py
|
||||
```
|
||||
Noteer de geprinte hex key.
|
||||
|
||||
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)
|
||||
├── 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
|
||||
│ ├── settings.gradle
|
||||
|
|
@ -149,9 +270,16 @@ sylvia-touch-relay/
|
|||
│ └── 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.
|
||||
- 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).
|
||||
|
|
@ -24,5 +24,10 @@
|
|||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="Instellingen"
|
||||
android:parentActivityName=".MainActivity"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -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>(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...")
|
||||
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<ImageButton>(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<out String>, 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<String> {
|
||||
|
|
@ -224,9 +327,11 @@ class MainActivity : AppCompatActivity() {
|
|||
ActivityCompat.requestPermissions(this, getRequiredPermissions(), 1)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopScan()
|
||||
bluetoothGatt?.close()
|
||||
unregisterReceiver(bondReceiver)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EditText>(R.id.keyInput)
|
||||
val toggleBtn = findViewById<ImageButton>(R.id.toggleVisibility)
|
||||
val saveButton = findViewById<Button>(R.id.saveButton)
|
||||
val backButton = findViewById<Button>(R.id.backButton)
|
||||
val keyError = findViewById<TextView>(R.id.keyError)
|
||||
|
||||
keyInput.setText(prefs.getString(PREF_KEY, ""))
|
||||
|
||||
toggleBtn.setOnClickListener {
|
||||
isVisible = !isVisible
|
||||
if (isVisible) {
|
||||
keyInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
toggleBtn.setImageResource(R.drawable.ic_eye_open)
|
||||
} else {
|
||||
keyInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
toggleBtn.setImageResource(R.drawable.ic_eye_closed)
|
||||
}
|
||||
keyInput.setSelection(keyInput.text.length)
|
||||
}
|
||||
|
||||
keyInput.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val cleaned = s?.toString()?.uppercase()?.filter { it in "0123456789ABCDEF" }
|
||||
if (cleaned != null && cleaned != s.toString()) {
|
||||
keyInput.removeTextChangedListener(this)
|
||||
keyInput.setText(cleaned)
|
||||
keyInput.setSelection(cleaned.length)
|
||||
keyInput.addTextChangedListener(this)
|
||||
}
|
||||
val len = cleaned?.length ?: 0
|
||||
if (len == 0 || len == 64) {
|
||||
keyError.visibility = TextView.GONE
|
||||
} else {
|
||||
keyError.text = "Sleutel moet 64 hex tekens zijn (nu $len)"
|
||||
keyError.visibility = TextView.VISIBLE
|
||||
}
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
saveButton.setOnClickListener {
|
||||
val key = keyInput.text.toString().uppercase().filter { it in "0123456789ABCDEF" }
|
||||
if (key.length != 64) {
|
||||
keyError.text = "Sleutel moet 64 hex tekens zijn (nu ${key.length})"
|
||||
keyError.visibility = TextView.VISIBLE
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.edit().putString(PREF_KEY, key).apply()
|
||||
finish()
|
||||
}
|
||||
|
||||
backButton.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
10
android/app/src/main/res/drawable/ic_eye_closed.xml
Normal file
10
android/app/src/main/res/drawable/ic_eye_closed.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/textColorSecondary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.44 1.78,10.07 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2z"/>
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_eye_open.xml
Normal file
10
android/app/src/main/res/drawable/ic_eye_open.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/textColorSecondary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_settings.xml
Normal file
10
android/app/src/main/res/drawable/ic_settings.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?android:attr/textColorSecondary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94 0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94l-0.36,-2.54c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41l-0.36,2.54c-0.59,0.24 -1.13,0.56 -1.62,0.94l-2.39,-0.96c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87c-0.12,0.21 -0.08,0.47 0.12,0.61l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6 3.6,1.62 3.6,3.6 -1.62,3.6 -3.6,3.6z"/>
|
||||
</vector>
|
||||
|
|
@ -6,6 +6,16 @@
|
|||
android:gravity="center"
|
||||
android:padding="32dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="end"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Instellingen"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
76
android/app/src/main/res/layout/activity_settings.xml
Normal file
76
android/app/src/main/res/layout/activity_settings.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="HMAC Sleutel"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Voer de 64-teken hex sleutel in die de ESP op serial print na opstarten."
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/keyInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="C3F17B61..."
|
||||
android:inputType="textPassword"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="16sp"
|
||||
android:maxLength="64"
|
||||
android:letterSpacing="0.05"
|
||||
android:autofillHints=""
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/toggleVisibility"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_eye_closed"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Toon/verberg sleutel" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/keyError"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/holo_red_dark"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Opslaan"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Terug"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Atom Relay</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="hmac_key_hint">C3F17B61...</string>
|
||||
<string name="save">Opslaan</string>
|
||||
<string name="back">Terug</string>
|
||||
</resources>
|
||||
81
src/main.cpp
81
src/main.cpp
|
|
@ -3,6 +3,9 @@
|
|||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <BLE2902.h>
|
||||
#include <BLESecurity.h>
|
||||
#include <mbedtls/md.h>
|
||||
#include "hmac_key.h"
|
||||
|
||||
#define SENSOR_PIN 38
|
||||
#define RELAY_PIN 1
|
||||
|
|
@ -11,36 +14,85 @@
|
|||
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
||||
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
|
||||
|
||||
#define HMAC_PKT_SIZE 37 // 1 cmd + 4 seq + 32 hmac
|
||||
|
||||
BLECharacteristic *pCharacteristic;
|
||||
bool deviceConnected = false;
|
||||
bool bleCommand = false;
|
||||
bool relayState = false;
|
||||
uint32_t lastSeq = 0;
|
||||
|
||||
class MyServerCallbacks : public BLEServerCallbacks {
|
||||
void onConnect(BLEServer *pServer) override {
|
||||
deviceConnected = true;
|
||||
lastSeq = 0;
|
||||
Serial.println("BLE: device connected");
|
||||
}
|
||||
void onDisconnect(BLEServer *pServer) override {
|
||||
deviceConnected = false;
|
||||
bleCommand = false;
|
||||
lastSeq = 0;
|
||||
Serial.println("BLE: device disconnected");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
static bool verifyHmac(const uint8_t *packet) {
|
||||
uint8_t cmd = packet[0];
|
||||
uint32_t seq = ((uint32_t)packet[1] << 24) |
|
||||
((uint32_t)packet[2] << 16) |
|
||||
((uint32_t)packet[3] << 8) |
|
||||
(uint32_t)packet[4];
|
||||
|
||||
if (seq <= lastSeq) {
|
||||
Serial.printf("BLE: replay rejected (seq=%lu, last=%lu)\n", seq, lastSeq);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t hmacData[5];
|
||||
hmacData[0] = cmd;
|
||||
hmacData[1] = (seq >> 24) & 0xFF;
|
||||
hmacData[2] = (seq >> 16) & 0xFF;
|
||||
hmacData[3] = (seq >> 8) & 0xFF;
|
||||
hmacData[4] = seq & 0xFF;
|
||||
|
||||
uint8_t computed[32];
|
||||
const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
mbedtls_md_hmac(mdInfo, HMAC_KEY, 32, hmacData, 5, computed);
|
||||
|
||||
if (memcmp(computed, packet + 5, 32) != 0) {
|
||||
Serial.println("BLE: HMAC mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
lastSeq = seq;
|
||||
return true;
|
||||
}
|
||||
|
||||
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks {
|
||||
void onWrite(BLECharacteristic *pChar) override {
|
||||
std::string value = pChar->getValue();
|
||||
if (value.length() > 0) {
|
||||
uint8_t cmd = (uint8_t)value[0];
|
||||
if (value.length() != HMAC_PKT_SIZE) {
|
||||
Serial.printf("BLE: bad packet length %d (expected %d)\n",
|
||||
(int)value.length(), HMAC_PKT_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *pkt = (const uint8_t *)value.data();
|
||||
|
||||
if (!verifyHmac(pkt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t cmd = pkt[0];
|
||||
uint32_t seq = lastSeq;
|
||||
|
||||
if (cmd == 0x01) {
|
||||
bleCommand = true;
|
||||
Serial.println("BLE: relay ON");
|
||||
Serial.printf("BLE: relay ON (seq=%lu)\n", seq);
|
||||
} else {
|
||||
bleCommand = false;
|
||||
Serial.println("BLE: relay OFF");
|
||||
}
|
||||
Serial.printf("BLE: relay OFF (seq=%lu)\n", seq);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -55,7 +107,6 @@ void setup() {
|
|||
pinMode(RELAY_PIN, OUTPUT);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
|
||||
// Visual confirmation: blink LED 3x before BLE init
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
delay(200);
|
||||
|
|
@ -65,7 +116,12 @@ void setup() {
|
|||
Serial.println("LED blink done, starting BLE...");
|
||||
|
||||
BLEDevice::init("AtomS3R-Relay");
|
||||
BLEDevice::setMTU(23);
|
||||
|
||||
BLESecurity *pSecurity = new BLESecurity();
|
||||
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);
|
||||
pSecurity->setCapability(ESP_IO_CAP_NONE);
|
||||
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
|
||||
BLEServer *pServer = BLEDevice::createServer();
|
||||
pServer->setCallbacks(new MyServerCallbacks());
|
||||
|
||||
|
|
@ -74,6 +130,7 @@ void setup() {
|
|||
CHARACTERISTIC_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
|
||||
);
|
||||
pCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENCRYPTED);
|
||||
pCharacteristic->addDescriptor(new BLE2902());
|
||||
pCharacteristic->setCallbacks(new MyCharacteristicCallbacks());
|
||||
pCharacteristic->setValue("0");
|
||||
|
|
@ -87,6 +144,16 @@ void setup() {
|
|||
BLEDevice::startAdvertising();
|
||||
|
||||
Serial.println("BLE: advertising started as 'AtomS3R-Relay'");
|
||||
|
||||
Serial.println("Printing HMAC key in 5 seconds...");
|
||||
delay(5000);
|
||||
Serial.print("HMAC Key: ");
|
||||
for (int i = 0; i < 32; i++) {
|
||||
Serial.printf("%02X", HMAC_KEY[i]);
|
||||
}
|
||||
Serial.println();
|
||||
Serial.println("Enter this key in the Android app > Settings.");
|
||||
|
||||
Serial.println("Setup done.");
|
||||
}
|
||||
|
||||
|
|
|
|||
53
tools/generate_hmac_key.py
Normal file
53
tools/generate_hmac_key.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue