AES ipv HMAC voor BLE relay commands #2

Closed
opened 2026-08-01 03:13:43 +02:00 by eddy · 4 comments
Owner

Doel

Vervang de huidige HMAC (32 bytes per packet, 37 bytes totaal) door AES encryptie voor kleinere en snellere packets.

Huidige situatie

  • Packet: 1 byte cmd + 4 bytes seq + 32 bytes HMAC = 37 bytes
  • NVS namespace hmac key key (32 bytes)
  • server.cpp: verifyHmac()
  • Android: HMAC_KEY in BleRelayManager

Aanpak

  • Overstap naar AES-128 of AES-256
  • Nieuwe NVS namespace/key voor AES
  • Key exchange flow aanpassen (SettingsActivity, firmware)
  • Backward compatibility overwegen (tijdelijk HMAC + AES accepteren)

Gerelateerde bestanden

  • src/main.cpp: verifyHmac(), NVS keys
  • src/client.cpp: HMAC signing
  • android/.../BleRelayManager.kt: HMAC key, signing
  • android/.../SettingsActivity.kt: key exchange
## Doel Vervang de huidige HMAC (32 bytes per packet, 37 bytes totaal) door AES encryptie voor kleinere en snellere packets. ## Huidige situatie - Packet: 1 byte cmd + 4 bytes seq + 32 bytes HMAC = 37 bytes - NVS namespace `hmac` key `key` (32 bytes) - server.cpp: `verifyHmac()` - Android: `HMAC_KEY` in BleRelayManager ## Aanpak - Overstap naar AES-128 of AES-256 - Nieuwe NVS namespace/key voor AES - Key exchange flow aanpassen (SettingsActivity, firmware) - Backward compatibility overwegen (tijdelijk HMAC + AES accepteren) ## Gerelateerde bestanden - `src/main.cpp`: `verifyHmac()`, NVS keys - `src/client.cpp`: HMAC signing - `android/.../BleRelayManager.kt`: HMAC key, signing - `android/.../SettingsActivity.kt`: key exchange
eddy added reference main 2026-08-01 03:30:46 +02:00
Author
Owner

De ESP32-S3 is zelfs nog krachtiger dan de C3 (dual-core Tensilica LX7 op 240 MHz i.p.v. single-core RISC-V op 160 MHz) en beschikt eveneens over een dedicated hardware-accelerator voor AES (inclusief AES-128 en AES-256).

Wat betekent dit voor de ESP32-S3?
Rete-snel: De hardware crypto-engine verwerkt AES-256-GCM in luttele microseconden. De CPU merkt er qua belastbaarheid vrijwel niets van.

Dezelfde code: De mbedTLS-bibliotheek die standaard in het Arduino/ESP-IDF framework zit, stuurt op de S3 automatisch de S3-hardware-crypto aan. Je hoeft je code of libraries dus niet aan te passen ten opzichte van het voorgaande voorbeeld.

Extra voordeel op de ESP32-S3: Hardware Security Features
Mocht je de beveiliging nóg een stap verder willen tillen, dan heeft de S3 specifieke hardware-beveiligingsmodules die erg handig zijn bij BLE-communicatie:

Digital Signature Peripheral (DS): De S3 kan cryptographic signatures genereren met een private key die veilig in de hardware opgeslagen zit en nooit door software (of een dump van het geheugen) uitgelezen kan worden.

HMAC module: Voor het veilig afleiden van sessiesleutels uit een hoofdsleutel.

Flash Encryption & Secure Boot v2: Hiermee voorkom je dat iemand de AES-sleutel uit de flash van de ESP32-S3 kan uitlezen als ze fysiek toegang hebben tot de chip.

Kortom: de S3 is uitermate geschikt voor dit doel!

Wil je een voorbeeld hoe je ECDH sleuteluitwisseling (Diffie-Hellman) instelt op de S3?

Ja

De ESP32-S3 is zelfs nog krachtiger dan de C3 (dual-core Tensilica LX7 op 240 MHz i.p.v. single-core RISC-V op 160 MHz) en beschikt eveneens over een dedicated hardware-accelerator voor AES (inclusief AES-128 en AES-256). Wat betekent dit voor de ESP32-S3? Rete-snel: De hardware crypto-engine verwerkt AES-256-GCM in luttele microseconden. De CPU merkt er qua belastbaarheid vrijwel niets van. Dezelfde code: De mbedTLS-bibliotheek die standaard in het Arduino/ESP-IDF framework zit, stuurt op de S3 automatisch de S3-hardware-crypto aan. Je hoeft je code of libraries dus niet aan te passen ten opzichte van het voorgaande voorbeeld. Extra voordeel op de ESP32-S3: Hardware Security Features Mocht je de beveiliging nóg een stap verder willen tillen, dan heeft de S3 specifieke hardware-beveiligingsmodules die erg handig zijn bij BLE-communicatie: Digital Signature Peripheral (DS): De S3 kan cryptographic signatures genereren met een private key die veilig in de hardware opgeslagen zit en nooit door software (of een dump van het geheugen) uitgelezen kan worden. HMAC module: Voor het veilig afleiden van sessiesleutels uit een hoofdsleutel. Flash Encryption & Secure Boot v2: Hiermee voorkom je dat iemand de AES-sleutel uit de flash van de ESP32-S3 kan uitlezen als ze fysiek toegang hebben tot de chip. Kortom: de S3 is uitermate geschikt voor dit doel! Wil je een voorbeeld hoe je ECDH sleuteluitwisseling (Diffie-Hellman) instelt op de S3? Ja
Author
Owner

Om een dynamische AES-256 sleutel op te stellen zonder deze vast in de code te hoeven zetten (hardcoden), gebruik je ECDH (Elliptic Curve Diffie-Hellman) met de Curve25519 of SECP256R1 curve.

Met ECDH genereren beide ESP32-S3's hun eigen Private/Public keypair. Ze wisselen alleen hun Public Keys uit via BLE. Vervolgens berekenen ze beiden onafhankelijk van elkaar exact dezelfde Shared Secret (Master Key).

Hoe het werkt over BLE
ESP32-S3 (BLE Client) ESP32-S3 (BLE Server)


  1. Genereert Keypair A 1. Genereert Keypair B

  2. Verstuurt Public Key A -------- BLE Write ------->
    3. Berekent Shared Secret
    (PrivKey B + PubKey A)

  3. Berekent Shared Secret <------ BLE Notify -------- Verstuurt Public Key B
    (PrivKey A + PubKey B)


BEIDE APPARATEN HEBBEN NU DEZELFDE AES-256 SLEUTEL (GEEN PRIVATE KEYS OVER BLE!)
Voorbeeldcode (Arduino / PlatformIO Framework)
Onderstaande C++ code gebruikt de ingebouwde mbedtls bibliotheek (mbedtls/ecdh.h). Deze maakt op de ESP32-S3 gebruik van de hardwarematige ECC-accelerator.

  1. ECDH Sleuteluitwisseling Logica (ecdh_helper.h)
    Deze klasse maakt het genereren van de sleutels en het berekenen van de shared secret eenvoudig:

C++
#include <Arduino.h>
#include "mbedtls/ecdh.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"

class CryptoECDH {
private:
mbedtls_ecdh_context ecdh;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;

public:
uint8_t public_key[65]; // Uncompressed SECP256R1 Public Key
size_t pub_len = 0;
uint8_t shared_secret[32]; // De uiteindelijke AES-256 Sleutel

bool init() {
    mbedtls_ecdh_init(&ecdh);
    mbedtls_entropy_init(&entropy);
    mbedtls_ctr_drbg_init(&ctr_drbg);

    // Initialiseer de Hardware Random Number Generator (RNG)
    const char *pers = "esp32_s3_ble_ecdh";
    int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
                                    (const unsigned char *)pers, strlen(pers));
    if (ret != 0) return false;

    // Gebruik de SECP256R1 (NIST P-256) curve (Hardware ondersteund op S3)
    ret = mbedtls_ecp_group_load(&ecdh.grp, MBEDTLS_ECP_DP_SECP256R1);
    return (ret == 0);
}

// Stap 1: Genereer eigen Keypair & Export Public Key
bool generate_keys() {
    int ret = mbedtls_ecdh_gen_public(&ecdh.grp, &ecdh.d, &ecdh.Q,
                                      mbedtls_ctr_drbg_random, &ctr_drbg);
    if (ret != 0) return false;

    // Exporteer Public Key naar byte-array om te versturen via BLE
    ret = mbedtls_ecp_point_write_binary(&ecdh.grp, &ecdh.Q,
                                         MBEDTLS_ECP_PF_UNCOMPRESSED,
                                         &pub_len, public_key, sizeof(public_key));
    return (ret == 0);
}

// Stap 2: Ontvang Public Key van tegenpartij & Bereken Shared Secret (AES-256 Sleutel)
bool compute_shared_secret(const uint8_t *peer_pub_key, size_t peer_pub_len) {
    // Importeer de Public Key van de andere ESP32-S3
    int ret = mbedtls_ecp_point_read_binary(&ecdh.grp, &ecdh.Qp,
                                            peer_pub_key, peer_pub_len);
    if (ret != 0) return false;

    // Bereken Shared Secret
    size_t olen;
    ret = mbedtls_ecdh_compute_shared(&ecdh.grp, &ecdh.z, &ecdh.Qp, &ecdh.d,
                                      mbedtls_ctr_drbg_random, &ctr_drbg);
    if (ret != 0) return false;

    // Exporteer de 32-byte (256-bit) shared secret voor AES-256
    ret = mbedtls_mpi_write_binary(&ecdh.z, shared_secret, 32);
    return (ret == 0);
}

void free() {
    mbedtls_ecdh_free(&ecdh);
    mbedtls_ctr_drbg_free(&ctr_drbg);
    mbedtls_entropy_free(&entropy);
}

};
2. Integratie in de BLE Flow (Conceptuele setup)
In je BLE code (met de standaard NimBLE-Arduino of ESP32 BLE Library) gebruik je bovenstaande klasse als volgt tijdens de verbinding opzetten:

C++
#include "ecdh_helper.h"

CryptoECDH ecdh_node;

void setup_security() {
Serial.begin(115200);

// 1. Initialiseer crypto engine
if (!ecdh_node.init()) {
    Serial.println("ECDH Init mislukt!");
    return;
}

// 2. Genereer lokaal keypair
if (ecdh_node.generate_keys()) {
    Serial.println("Eigen Public Key gegenereerd.");
}

}

// Wordt aangeroepen wanneer BLE de Public Key van de ANDERE ESP32-S3 ontvangt
void on_receive_peer_public_key(uint8_t* payload, size_t length) {
Serial.println("Public Key van tegenpartij ontvangen via BLE...");

// 3. Bereken de gedeelde AES-256 sleutel!
if (ecdh_node.compute_shared_secret(payload, length)) {
    Serial.println("SUCCESS: Gedeelde AES-256 sleutel succesvol berekend!");
    
    // ecdh_node.shared_secret bevat nu de 32-byte sleutel voor AES-256-GCM.
    // Je kunt deze direct doorgeven aan mbedtls_gcm_setkey()
} else {
    Serial.println("ERROR: Kon shared secret niet berekenen.");
}

}
Beveiligingstips voor Productie
Man-In-The-Middle (MITM) Aanvallen:
Pure ECDH beschermt tegen afluisteren, maar niet tegen een actieve aanvaller die zich tussen de twee ESP's nestelt (MITM). Om dit op te lossen kun je in BLE Passkey Pairing inschakelen, of eenmalig een vooraf bekende preshared root key gebruiken om de publieke sleutels te ondertekenen.

Ephemeral Keys (Perfect Forward Secrecy):
Genereer bij elke nieuwe BLE-verbinding een nieuw ECDH-sleutelpaar (generate_keys()). Mocht een sessiesleutel ooit uitlekken, dan zijn oude of toekomstige berichten alsnog niet te ontsleutelen.

RAM Opschonen:
Zodra de shared_secret is geladen in de AES-GCM context, roep je ecdh_node.free() op om de tijdelijke ECDH-sleutels uit het RAM-geheugen te wissen.

Om een dynamische AES-256 sleutel op te stellen zonder deze vast in de code te hoeven zetten (hardcoden), gebruik je ECDH (Elliptic Curve Diffie-Hellman) met de Curve25519 of SECP256R1 curve. Met ECDH genereren beide ESP32-S3's hun eigen Private/Public keypair. Ze wisselen alleen hun Public Keys uit via BLE. Vervolgens berekenen ze beiden onafhankelijk van elkaar exact dezelfde Shared Secret (Master Key). Hoe het werkt over BLE ESP32-S3 (BLE Client) ESP32-S3 (BLE Server) ----------------------- ----------------------- 1. Genereert Keypair A 1. Genereert Keypair B 2. Verstuurt Public Key A -------- BLE Write -------> 3. Berekent Shared Secret (PrivKey B + PubKey A) 4. Berekent Shared Secret <------ BLE Notify -------- Verstuurt Public Key B (PrivKey A + PubKey B) ---------------------------------------------------------------------------- BEIDE APPARATEN HEBBEN NU DEZELFDE AES-256 SLEUTEL (GEEN PRIVATE KEYS OVER BLE!) Voorbeeldcode (Arduino / PlatformIO Framework) Onderstaande C++ code gebruikt de ingebouwde mbedtls bibliotheek (mbedtls/ecdh.h). Deze maakt op de ESP32-S3 gebruik van de hardwarematige ECC-accelerator. 1. ECDH Sleuteluitwisseling Logica (ecdh_helper.h) Deze klasse maakt het genereren van de sleutels en het berekenen van de shared secret eenvoudig: C++ #include <Arduino.h> #include "mbedtls/ecdh.h" #include "mbedtls/entropy.h" #include "mbedtls/ctr_drbg.h" class CryptoECDH { private: mbedtls_ecdh_context ecdh; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; public: uint8_t public_key[65]; // Uncompressed SECP256R1 Public Key size_t pub_len = 0; uint8_t shared_secret[32]; // De uiteindelijke AES-256 Sleutel bool init() { mbedtls_ecdh_init(&ecdh); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); // Initialiseer de Hardware Random Number Generator (RNG) const char *pers = "esp32_s3_ble_ecdh"; int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)pers, strlen(pers)); if (ret != 0) return false; // Gebruik de SECP256R1 (NIST P-256) curve (Hardware ondersteund op S3) ret = mbedtls_ecp_group_load(&ecdh.grp, MBEDTLS_ECP_DP_SECP256R1); return (ret == 0); } // Stap 1: Genereer eigen Keypair & Export Public Key bool generate_keys() { int ret = mbedtls_ecdh_gen_public(&ecdh.grp, &ecdh.d, &ecdh.Q, mbedtls_ctr_drbg_random, &ctr_drbg); if (ret != 0) return false; // Exporteer Public Key naar byte-array om te versturen via BLE ret = mbedtls_ecp_point_write_binary(&ecdh.grp, &ecdh.Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &pub_len, public_key, sizeof(public_key)); return (ret == 0); } // Stap 2: Ontvang Public Key van tegenpartij & Bereken Shared Secret (AES-256 Sleutel) bool compute_shared_secret(const uint8_t *peer_pub_key, size_t peer_pub_len) { // Importeer de Public Key van de andere ESP32-S3 int ret = mbedtls_ecp_point_read_binary(&ecdh.grp, &ecdh.Qp, peer_pub_key, peer_pub_len); if (ret != 0) return false; // Bereken Shared Secret size_t olen; ret = mbedtls_ecdh_compute_shared(&ecdh.grp, &ecdh.z, &ecdh.Qp, &ecdh.d, mbedtls_ctr_drbg_random, &ctr_drbg); if (ret != 0) return false; // Exporteer de 32-byte (256-bit) shared secret voor AES-256 ret = mbedtls_mpi_write_binary(&ecdh.z, shared_secret, 32); return (ret == 0); } void free() { mbedtls_ecdh_free(&ecdh); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); } }; 2. Integratie in de BLE Flow (Conceptuele setup) In je BLE code (met de standaard NimBLE-Arduino of ESP32 BLE Library) gebruik je bovenstaande klasse als volgt tijdens de verbinding opzetten: C++ #include "ecdh_helper.h" CryptoECDH ecdh_node; void setup_security() { Serial.begin(115200); // 1. Initialiseer crypto engine if (!ecdh_node.init()) { Serial.println("ECDH Init mislukt!"); return; } // 2. Genereer lokaal keypair if (ecdh_node.generate_keys()) { Serial.println("Eigen Public Key gegenereerd."); } } // Wordt aangeroepen wanneer BLE de Public Key van de ANDERE ESP32-S3 ontvangt void on_receive_peer_public_key(uint8_t* payload, size_t length) { Serial.println("Public Key van tegenpartij ontvangen via BLE..."); // 3. Bereken de gedeelde AES-256 sleutel! if (ecdh_node.compute_shared_secret(payload, length)) { Serial.println("SUCCESS: Gedeelde AES-256 sleutel succesvol berekend!"); // ecdh_node.shared_secret bevat nu de 32-byte sleutel voor AES-256-GCM. // Je kunt deze direct doorgeven aan mbedtls_gcm_setkey() } else { Serial.println("ERROR: Kon shared secret niet berekenen."); } } Beveiligingstips voor Productie Man-In-The-Middle (MITM) Aanvallen: Pure ECDH beschermt tegen afluisteren, maar niet tegen een actieve aanvaller die zich tussen de twee ESP's nestelt (MITM). Om dit op te lossen kun je in BLE Passkey Pairing inschakelen, of eenmalig een vooraf bekende preshared root key gebruiken om de publieke sleutels te ondertekenen. Ephemeral Keys (Perfect Forward Secrecy): Genereer bij elke nieuwe BLE-verbinding een nieuw ECDH-sleutelpaar (generate_keys()). Mocht een sessiesleutel ooit uitlekken, dan zijn oude of toekomstige berichten alsnog niet te ontsleutelen. RAM Opschonen: Zodra de shared_secret is geladen in de AES-GCM context, roep je ecdh_node.free() op om de tijdelijke ECDH-sleutels uit het RAM-geheugen te wissen.
Author
Owner

Implementatieplan

Beveiligingsmodel

ECDH + PSK-signed pubkeys — MITM-bestendig

  1. Provisioning (USB CDC): Android provisioned 32B PSK naar beide ESPs
  2. BLE handshake: pubkey(65B) || HMAC(PSK,pubkey)(32B) uitgewisseld
  3. AES_KEY = SHA-256(ECDH_shared_secret) — per sessie, PFS

Packet (AES-256-GCM relay): 29 bytes

seq(4) || nonce(8) || ciphertext_cmd(1) || tag(16)

v.s. huidige 37 bytes HMAC — 21% kleiner

Nieuwe BLE characteristic

beb5483e-...-f26dc (WRITE_NR | NOTIFY) — 97B ECDH handshake

NVS/Prefs

  • hmac/keysyl/psk (ESP)
  • hmac_keypsk (Android)

Bestanden

  • Nieuw: src/ecdh_helper.h
  • Gewijzigd: main.cpp, client.cpp, BleRelayManager.kt, SettingsActivity.kt, PrefsHelper.kt, version.txt

Geen backward compatibility

Schone break — HMAC packets worden niet meer geaccepteerd.

Volledig plan: docs/plan-issue-2-aes-ecdh.md

## Implementatieplan ### Beveiligingsmodel **ECDH + PSK-signed pubkeys** — MITM-bestendig 1. **Provisioning** (USB CDC): Android provisioned 32B PSK naar beide ESPs 2. **BLE handshake**: pubkey(65B) || HMAC(PSK,pubkey)(32B) uitgewisseld 3. **AES_KEY** = SHA-256(ECDH_shared_secret) — per sessie, PFS ### Packet (AES-256-GCM relay): 29 bytes ``` seq(4) || nonce(8) || ciphertext_cmd(1) || tag(16) ``` v.s. huidige 37 bytes HMAC — 21% kleiner ### Nieuwe BLE characteristic `beb5483e-...-f26dc` (WRITE_NR | NOTIFY) — 97B ECDH handshake ### NVS/Prefs - `hmac/key` → `syl/psk` (ESP) - `hmac_key` → `psk` (Android) ### Bestanden - Nieuw: `src/ecdh_helper.h` - Gewijzigd: `main.cpp`, `client.cpp`, `BleRelayManager.kt`, `SettingsActivity.kt`, `PrefsHelper.kt`, `version.txt` ### Geen backward compatibility Schone break — HMAC packets worden niet meer geaccepteerd. Volledig plan: `docs/plan-issue-2-aes-ecdh.md`
eddy closed this issue 2026-08-04 01:27:17 +02:00
Author
Owner

Code review crypto (afgerond)

Beveiligingsmodel: ECDH-secp256r1 + PSK-HMAC pubkey auth + AES-256-GCM relay commands. Consistent tussen firmware (mbedtls) en Android (JCA).

Bevindingen:

  • Geen kritieke kwetsbaarheden
  • Packet layout, nonce constructie, replay protection, GCM tag correct op alle 3 lagen
  • Alle crypto hardware-accelerated op ESP32-S3 (AES-GCM, SHA-256, MPI/ECC, TRNG)
  • Verbeterpunt in CryptoECDH::free() (public_key niet gewist) — opgelost in e03067c
  • PSK rotatie alleen via USB CDC (acceptabel voor dit threat model)

Conclusie: Implementatie voldoet aan plan. Issue gesloten.

## Code review crypto (afgerond) **Beveiligingsmodel:** ECDH-secp256r1 + PSK-HMAC pubkey auth + AES-256-GCM relay commands. Consistent tussen firmware (mbedtls) en Android (JCA). **Bevindingen:** - Geen kritieke kwetsbaarheden - Packet layout, nonce constructie, replay protection, GCM tag correct op alle 3 lagen - Alle crypto hardware-accelerated op ESP32-S3 (AES-GCM, SHA-256, MPI/ECC, TRNG) - Verbeterpunt in `CryptoECDH::free()` (public_key niet gewist) — opgelost in `e03067c` - PSK rotatie alleen via USB CDC (acceptabel voor dit threat model) **Conclusie:** Implementatie voldoet aan plan. Issue gesloten.
Sign in to join this conversation.
No description provided.