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:
eddy 2026-07-26 08:18:52 +02:00
commit 7f68e9ad70
13 changed files with 621 additions and 46 deletions

View file

@ -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 (cmd == 0x01) {
bleCommand = true;
Serial.println("BLE: relay ON");
} else {
bleCommand = false;
Serial.println("BLE: relay OFF");
}
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.printf("BLE: relay ON (seq=%lu)\n", seq);
} else {
bleCommand = false;
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.");
}