Merge branch 'feature/aes-ecdh-ble' into main
- AES-256-GCM + ECDH key exchange vervangt HMAC-SHA256 (issue #2) - NVS namespace 'hmac' -> 'syl', key 'key' -> 'psk' - ECDH-UUID characteristic toegevoegd voor key agreement - Android: PSK key exchange via USB CDC, ECDH handshake in BleRelayManager - Conflictoplossingen: - pUuidChar + pEcdhChar beide globaal, INPUT voor touch pins - client.cpp: serverFound + MyScanCallbacks behouden voor ECDH compat - SettingsActivity.kt: PSK detectie + debug log gecombineerd - version.txt: HEAD versie (20/5) behouden - version.h: verwijderd (auto-gegenereerd)
This commit is contained in:
commit
a242bfa58a
9 changed files with 797 additions and 171 deletions
237
src/client.cpp
237
src/client.cpp
|
|
@ -2,12 +2,13 @@
|
|||
#include <NimBLEDevice.h>
|
||||
#include <FastLED.h>
|
||||
|
||||
#include <mbedtls/md.h>
|
||||
#include <mbedtls/gcm.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include "version.h"
|
||||
#include "ecdh_helper.h"
|
||||
|
||||
static const char *TAG = "client";
|
||||
|
||||
|
|
@ -22,21 +23,20 @@ CRGB leds[NUM_LEDS];
|
|||
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
||||
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
|
||||
#define UUID_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d9"
|
||||
#define HMAC_PKT_SIZE 37
|
||||
#define HMAC_KEY_LEN 32
|
||||
#define ECDH_UUID "beb5483e-36e1-4688-b7f5-ea07361f26dc"
|
||||
#define ESP_UUID_LEN 16
|
||||
#define NVS_NAMESPACE "hmac"
|
||||
#define NVS_KEY_NAME "key"
|
||||
#define NVS_NAMESPACE "syl"
|
||||
#define NVS_PSK_NAME "psk"
|
||||
#define NVS_UUID_NAME "uuid"
|
||||
#define NVS_SERVER_NAME "server"
|
||||
#define DEBOUNCE_MS 50
|
||||
#define SCAN_INTERVAL 6000
|
||||
#define SCAN_TIMEOUT 5000
|
||||
|
||||
uint8_t hmacKey[HMAC_KEY_LEN];
|
||||
uint8_t psk[PSK_LEN];
|
||||
uint8_t deviceUuid[ESP_UUID_LEN];
|
||||
uint8_t serverUuid[ESP_UUID_LEN];
|
||||
bool keyProvisioned = false;
|
||||
bool pskProvisioned = false;
|
||||
bool uuidProvisioned = false;
|
||||
bool serverProvisioned = false;
|
||||
bool allProvisioned = false;
|
||||
|
|
@ -50,6 +50,9 @@ static bool relay1SendState = false;
|
|||
static bool relay2SendState = false;
|
||||
static uint32_t seq = 0;
|
||||
|
||||
static uint8_t aesKey[AES_KEY_LEN];
|
||||
static bool ecdhReady = false;
|
||||
|
||||
#define STABLE_LED_TIMEOUT_MS 60000
|
||||
uint32_t connectionStableMs = 0;
|
||||
uint32_t relayFlashUntil = 0;
|
||||
|
|
@ -57,8 +60,23 @@ bool otaInProgress = false;
|
|||
bool otaEnded = false;
|
||||
|
||||
static bool bleReady = false;
|
||||
static bool serverFound = false;
|
||||
static NimBLEAddress serverAddress;
|
||||
|
||||
static bool ecdhNotifyReceived = false;
|
||||
static uint8_t ecdhNotifyBuf[ECDH_PKT_SIZE];
|
||||
static size_t ecdhNotifyLen = 0;
|
||||
|
||||
class MyScanCallbacks : public NimBLEScanCallbacks {
|
||||
void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override {
|
||||
if (advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) {
|
||||
serverAddress = advertisedDevice->getAddress();
|
||||
serverFound = true;
|
||||
NimBLEDevice::getScan()->stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class MyClientCallbacks : public NimBLEClientCallbacks {
|
||||
void onConnect(NimBLEClient *client) override {
|
||||
connected = true;
|
||||
|
|
@ -69,13 +87,15 @@ class MyClientCallbacks : public NimBLEClientCallbacks {
|
|||
relay1SendState = false;
|
||||
relay2SendState = false;
|
||||
pCmdChar = nullptr;
|
||||
ecdhReady = false;
|
||||
memset(aesKey, 0, AES_KEY_LEN);
|
||||
ESP_LOGW(TAG, "BLE: disconnected (reason=%d)", reason);
|
||||
}
|
||||
};
|
||||
|
||||
static MyClientCallbacks clientCallbacks;
|
||||
|
||||
static bool isHexKey(const String &hex) {
|
||||
static bool isHex(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;
|
||||
|
|
@ -83,24 +103,41 @@ static bool isHexKey(const String &hex) {
|
|||
return true;
|
||||
}
|
||||
|
||||
static void parseHexKey(const String &hex, uint8_t *out, int len) {
|
||||
static void parseHex(const String &hex, uint8_t *out, int len) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16);
|
||||
}
|
||||
}
|
||||
|
||||
static void buildHmacPacket(uint8_t cmd, uint8_t *packet) {
|
||||
static void buildAesPacket(uint8_t cmd, uint8_t *packet) {
|
||||
uint32_t s = ++seq;
|
||||
packet[0] = cmd;
|
||||
packet[1] = (s >> 24) & 0xFF;
|
||||
packet[2] = (s >> 16) & 0xFF;
|
||||
packet[3] = (s >> 8) & 0xFF;
|
||||
packet[4] = s & 0xFF;
|
||||
uint8_t hmacData[5];
|
||||
hmacData[0] = cmd;
|
||||
memcpy(hmacData + 1, packet + 1, 4);
|
||||
const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
mbedtls_md_hmac(mdInfo, hmacKey, HMAC_KEY_LEN, hmacData, 5, packet + 5);
|
||||
packet[0] = (s >> 24) & 0xFF;
|
||||
packet[1] = (s >> 16) & 0xFF;
|
||||
packet[2] = (s >> 8) & 0xFF;
|
||||
packet[3] = s & 0xFF;
|
||||
|
||||
uint8_t nonceRandom[8];
|
||||
for (int i = 0; i < 8; i++) nonceRandom[i] = (uint8_t)(esp_random() & 0xFF);
|
||||
memcpy(packet + 4, nonceRandom, 8);
|
||||
|
||||
uint8_t nonce[12];
|
||||
memcpy(nonce, packet, 4);
|
||||
memcpy(nonce + 4, nonceRandom, 8);
|
||||
|
||||
mbedtls_gcm_context gcm;
|
||||
mbedtls_gcm_init(&gcm);
|
||||
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, aesKey, AES_KEY_LEN * 8);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "AES: setkey failed (ret=%d)", ret);
|
||||
mbedtls_gcm_free(&gcm);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t pt[1] = { cmd };
|
||||
mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT,
|
||||
1, nonce, 12, NULL, 0,
|
||||
pt, packet + 12, 16, packet + 13);
|
||||
mbedtls_gcm_free(&gcm);
|
||||
}
|
||||
|
||||
static void disconnectClient() {
|
||||
|
|
@ -114,6 +151,89 @@ static void disconnectClient() {
|
|||
connected = false;
|
||||
relay1SendState = false;
|
||||
relay2SendState = false;
|
||||
ecdhReady = false;
|
||||
memset(aesKey, 0, AES_KEY_LEN);
|
||||
}
|
||||
|
||||
static bool performEcdhHandshake(NimBLERemoteService *pSvc) {
|
||||
if (!pskProvisioned) {
|
||||
ESP_LOGE(TAG, "ECDH: no PSK");
|
||||
return false;
|
||||
}
|
||||
|
||||
CryptoECDH ecdh;
|
||||
if (!ecdh.init()) {
|
||||
ESP_LOGE(TAG, "ECDH: init failed");
|
||||
return false;
|
||||
}
|
||||
if (!ecdh.generateKeys()) {
|
||||
ESP_LOGE(TAG, "ECDH: keygen failed");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
NimBLERemoteCharacteristic *pEcdhChar = pSvc->getCharacteristic(ECDH_UUID);
|
||||
if (pEcdhChar == nullptr) {
|
||||
ESP_LOGW(TAG, "ECDH: characteristic not found");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pEcdhChar->subscribe(true, [](NimBLERemoteCharacteristic *pChar, const uint8_t *data, size_t len, bool isNotify) {
|
||||
if (ecdhReady) return;
|
||||
if (len != ECDH_PKT_SIZE) {
|
||||
ESP_LOGW(TAG, "ECDH: bad notify length %d", len);
|
||||
return;
|
||||
}
|
||||
memcpy(ecdhNotifyBuf, data, len);
|
||||
ecdhNotifyLen = len;
|
||||
ecdhNotifyReceived = true;
|
||||
ESP_LOGI(TAG, "ECDH: notify received (%d bytes)", len);
|
||||
}, true)) {
|
||||
ESP_LOGW(TAG, "ECDH: subscribe failed");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t handshake[ECDH_PKT_SIZE];
|
||||
memcpy(handshake, ecdh.public_key, ECDH_PUBKEY_LEN);
|
||||
CryptoECDH::signPubkey(psk, ecdh.public_key, handshake + ECDH_PUBKEY_LEN);
|
||||
|
||||
ecdhNotifyReceived = false;
|
||||
if (!pEcdhChar->writeValue(handshake, ECDH_PKT_SIZE, false)) {
|
||||
ESP_LOGW(TAG, "ECDH: write pubkey failed");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t start = millis();
|
||||
while (!ecdhNotifyReceived && (millis() - start < 5000)) {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
if (!ecdhNotifyReceived) {
|
||||
ESP_LOGW(TAG, "ECDH: notify timeout");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CryptoECDH::verifyPubkey(psk, ecdhNotifyBuf, ecdhNotifyBuf + ECDH_PUBKEY_LEN)) {
|
||||
ESP_LOGW(TAG, "ECDH: server pubkey HMAC mismatch");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ecdh.computeSharedSecret(ecdhNotifyBuf, ECDH_PUBKEY_LEN)) {
|
||||
ESP_LOGE(TAG, "ECDH: shared secret failed");
|
||||
ecdh.free();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(aesKey, ecdh.aes_key, AES_KEY_LEN);
|
||||
ecdh.free();
|
||||
ecdhReady = true;
|
||||
ESP_LOGI(TAG, "ECDH: handshake complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool connectToServer() {
|
||||
|
|
@ -157,10 +277,19 @@ static bool connectToServer() {
|
|||
return false;
|
||||
}
|
||||
|
||||
ecdhReady = false;
|
||||
memset(aesKey, 0, AES_KEY_LEN);
|
||||
|
||||
if (!performEcdhHandshake(pSvc)) {
|
||||
ESP_LOGE(TAG, "BLE: ECDH handshake failed");
|
||||
disconnectClient();
|
||||
return false;
|
||||
}
|
||||
|
||||
seq = 0;
|
||||
relay1SendState = false;
|
||||
relay2SendState = false;
|
||||
ESP_LOGI(TAG, "BLE: server UUID verified, ready");
|
||||
ESP_LOGI(TAG, "BLE: server UUID verified, ECDH done, ready");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -174,17 +303,17 @@ void setup() {
|
|||
FastLED.show();
|
||||
|
||||
preferences.begin(NVS_NAMESPACE, true);
|
||||
size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
|
||||
size_t pskLen = preferences.getBytes(NVS_PSK_NAME, psk, PSK_LEN);
|
||||
size_t uuidLen = preferences.getBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN);
|
||||
size_t serverLen = preferences.getBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN);
|
||||
preferences.end();
|
||||
|
||||
if (len == HMAC_KEY_LEN) {
|
||||
keyProvisioned = true;
|
||||
ESP_LOGI(TAG, "HMAC key loaded from NVS");
|
||||
if (pskLen == PSK_LEN) {
|
||||
pskProvisioned = true;
|
||||
ESP_LOGI(TAG, "PSK loaded from NVS");
|
||||
} else {
|
||||
keyProvisioned = false;
|
||||
ESP_LOGW(TAG, "No HMAC key — use USB SET_KEY to provision");
|
||||
pskProvisioned = false;
|
||||
ESP_LOGW(TAG, "No PSK — use USB SET_KEY to provision");
|
||||
}
|
||||
|
||||
if (uuidLen == ESP_UUID_LEN) {
|
||||
|
|
@ -208,7 +337,7 @@ void setup() {
|
|||
ESP_LOGW(TAG, "No server UUID — use USB SET_SERVER to provision");
|
||||
}
|
||||
|
||||
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||
allProvisioned = pskProvisioned && uuidProvisioned && serverProvisioned;
|
||||
|
||||
pinMode(TOUCH1_PIN, INPUT_PULLUP);
|
||||
pinMode(TOUCH2_PIN, INPUT_PULLUP);
|
||||
|
|
@ -244,12 +373,12 @@ void loop() {
|
|||
if (c == '\n') {
|
||||
serialBuf.trim();
|
||||
if (serialBuf == "KEY?") {
|
||||
if (keyProvisioned && uuidProvisioned) {
|
||||
if (pskProvisioned && uuidProvisioned) {
|
||||
Serial.print("DEVICE_ID:");
|
||||
for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]);
|
||||
Serial.println();
|
||||
Serial.print("HMAC_KEY:");
|
||||
for (int i = 0; i < HMAC_KEY_LEN; i++) Serial.printf("%02X", hmacKey[i]);
|
||||
Serial.print("PSK:");
|
||||
for (int i = 0; i < PSK_LEN; i++) Serial.printf("%02X", psk[i]);
|
||||
Serial.println();
|
||||
Serial.print("SERVER_UUID:");
|
||||
if (serverProvisioned) {
|
||||
|
|
@ -267,15 +396,15 @@ void loop() {
|
|||
} else if (serialBuf.startsWith("SET_KEY:")) {
|
||||
String hex = serialBuf.substring(8);
|
||||
hex.toUpperCase();
|
||||
if (hex.length() == 64 && isHexKey(hex)) {
|
||||
parseHexKey(hex, hmacKey, HMAC_KEY_LEN);
|
||||
if (hex.length() == 64 && isHex(hex)) {
|
||||
parseHex(hex, psk, PSK_LEN);
|
||||
preferences.begin(NVS_NAMESPACE, false);
|
||||
preferences.putBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
|
||||
preferences.putBytes(NVS_PSK_NAME, psk, PSK_LEN);
|
||||
preferences.end();
|
||||
keyProvisioned = true;
|
||||
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||
pskProvisioned = true;
|
||||
allProvisioned = pskProvisioned && uuidProvisioned && serverProvisioned;
|
||||
Serial.println("OK");
|
||||
Serial.println("HMAC key stored in NVS");
|
||||
Serial.println("PSK stored in NVS");
|
||||
if (allProvisioned && !bleReady) {
|
||||
NimBLEDevice::init("SylDa_Client");
|
||||
bleReady = true;
|
||||
|
|
@ -288,13 +417,13 @@ void loop() {
|
|||
} else if (serialBuf.startsWith("SET_ID:")) {
|
||||
String hex = serialBuf.substring(7);
|
||||
hex.toUpperCase();
|
||||
if (hex.length() == 32 && isHexKey(hex)) {
|
||||
parseHexKey(hex, deviceUuid, ESP_UUID_LEN);
|
||||
if (hex.length() == 32 && isHex(hex)) {
|
||||
parseHex(hex, deviceUuid, ESP_UUID_LEN);
|
||||
preferences.begin(NVS_NAMESPACE, false);
|
||||
preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN);
|
||||
preferences.end();
|
||||
uuidProvisioned = true;
|
||||
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||
allProvisioned = pskProvisioned && uuidProvisioned && serverProvisioned;
|
||||
Serial.println("OK");
|
||||
Serial.println("UUID stored in NVS");
|
||||
} else {
|
||||
|
|
@ -304,13 +433,13 @@ void loop() {
|
|||
} else if (serialBuf.startsWith("SET_SERVER:")) {
|
||||
String hex = serialBuf.substring(11);
|
||||
hex.toUpperCase();
|
||||
if (hex.length() == 32 && isHexKey(hex)) {
|
||||
parseHexKey(hex, serverUuid, ESP_UUID_LEN);
|
||||
if (hex.length() == 32 && isHex(hex)) {
|
||||
parseHex(hex, serverUuid, ESP_UUID_LEN);
|
||||
preferences.begin(NVS_NAMESPACE, false);
|
||||
preferences.putBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN);
|
||||
preferences.end();
|
||||
serverProvisioned = true;
|
||||
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||
allProvisioned = pskProvisioned && uuidProvisioned && serverProvisioned;
|
||||
Serial.println("OK");
|
||||
Serial.println("Server UUID stored in NVS");
|
||||
if (allProvisioned && !bleReady) {
|
||||
|
|
@ -348,9 +477,6 @@ void loop() {
|
|||
bool t2Active = (millis() - t2Change > DEBOUNCE_MS) && t2Stable;
|
||||
bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable;
|
||||
|
||||
static bool prevBtnActive = false;
|
||||
prevBtnActive = btnActive;
|
||||
|
||||
bool relay1State = !t1Active || btnActive;
|
||||
bool relay2State = !t2Active;
|
||||
|
||||
|
|
@ -390,21 +516,21 @@ void loop() {
|
|||
leds[0] = ledColor;
|
||||
FastLED.show();
|
||||
|
||||
if (connected && pCmdChar != nullptr) {
|
||||
if (connected && pCmdChar != nullptr && ecdhReady) {
|
||||
static uint32_t lastCmd = 0;
|
||||
if (millis() - lastCmd > 200) {
|
||||
if (relay1State != relay1SendState) {
|
||||
uint8_t pkt[HMAC_PKT_SIZE];
|
||||
buildHmacPacket(relay1State ? 0x01 : 0x00, pkt);
|
||||
pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false);
|
||||
uint8_t pkt[AES_PKT_SIZE];
|
||||
buildAesPacket(relay1State ? 0x01 : 0x00, pkt);
|
||||
pCmdChar->writeValue(pkt, AES_PKT_SIZE, false);
|
||||
relay1SendState = relay1State;
|
||||
ESP_LOGD(TAG, "TX: relay1 %s (seq=%lu)", relay1State ? "ON" : "OFF", seq);
|
||||
lastCmd = millis();
|
||||
}
|
||||
if (relay2State != relay2SendState) {
|
||||
uint8_t pkt[HMAC_PKT_SIZE];
|
||||
buildHmacPacket(relay2State ? 0x02 : 0x03, pkt);
|
||||
pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false);
|
||||
uint8_t pkt[AES_PKT_SIZE];
|
||||
buildAesPacket(relay2State ? 0x02 : 0x03, pkt);
|
||||
pCmdChar->writeValue(pkt, AES_PKT_SIZE, false);
|
||||
relay2SendState = relay2State;
|
||||
ESP_LOGD(TAG, "TX: relay2 %s (seq=%lu)", relay2State ? "ON" : "OFF", seq);
|
||||
lastCmd = millis();
|
||||
|
|
@ -448,13 +574,14 @@ void loop() {
|
|||
|
||||
static uint32_t heartbeat = 0;
|
||||
if (millis() - heartbeat > 60000) {
|
||||
ESP_LOGD(TAG, "[heartbeat] t1=%s t2=%s btn=%s r1=%s r2=%s connected=%s",
|
||||
ESP_LOGD(TAG, "[heartbeat] t1=%s t2=%s btn=%s r1=%s r2=%s connected=%s ecdh=%s",
|
||||
!t1Active ? "YES" : "no",
|
||||
!t2Active ? "YES" : "no",
|
||||
btnActive ? "YES" : "no",
|
||||
relay1State ? "ON" : "OFF",
|
||||
relay2State ? "ON" : "OFF",
|
||||
connected ? "yes" : "no");
|
||||
connected ? "yes" : "no",
|
||||
ecdhReady ? "yes" : "no");
|
||||
heartbeat = millis();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue