Skip to content

Commit 56a2245

Browse files
fix(ble): Fix broken functions and add IRK retrieval (#11948)
* fix(ble): Fix authentication deadlock * feat(irk): Add peer's IRK retrieval methods * fix(ble): Fix deinit function * fix(ble): Fix notification timing * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent fc8ce8f commit 56a2245

File tree

9 files changed

+531
-16
lines changed

9 files changed

+531
-16
lines changed

libraries/BLE/examples/Client_secure_static_passkey/Client_secure_static_passkey.ino

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
/*
2-
Secure client with static passkey
2+
Secure client with static passkey and IRK retrieval
33
44
This example demonstrates how to create a secure BLE client that connects to
55
a secure BLE server using a static passkey without prompting the user.
66
The client will automatically use the same passkey (123456) as the server.
77
8+
After successful bonding, the example demonstrates how to retrieve the
9+
server's Identity Resolving Key (IRK) in multiple formats:
10+
- Comma-separated hex format: 0x1A,0x1B,0x1C,...
11+
- Base64 encoded (for Home Assistant Private BLE Device service)
12+
- Reverse hex order (for Home Assistant ESPresense)
13+
814
This client is designed to work with the Server_secure_static_passkey example.
915
1016
Note that ESP32 uses Bluedroid by default and the other SoCs use NimBLE.
1117
Bluedroid initiates security on-connect, while NimBLE initiates security on-demand.
1218
This means that in NimBLE you can read the insecure characteristic without entering
1319
the passkey. This is not possible in Bluedroid.
1420
15-
IMPORTANT: MITM (Man-In-The-Middle protection) must be enabled for password prompts
16-
to work. Without MITM, the BLE stack assumes no user interaction is needed and will use
17-
"Just Works" pairing method (with encryption if secure connection is enabled).
21+
IMPORTANT:
22+
- MITM (Man-In-The-Middle protection) must be enabled for password prompts to work.
23+
- Bonding must be enabled to store and retrieve the IRK.
24+
- The server must distribute its Identity Key during pairing.
1825
1926
Based on examples from Neil Kolban and h2zero.
2027
Created by lucasssvaz.
@@ -36,10 +43,59 @@ static BLEUUID secureCharUUID("ff1d2614-e2d6-4c87-9154-6625d39ca7f8");
3643
static boolean doConnect = false;
3744
static boolean connected = false;
3845
static boolean doScan = false;
46+
static BLEClient *pClient = nullptr;
3947
static BLERemoteCharacteristic *pRemoteInsecureCharacteristic;
4048
static BLERemoteCharacteristic *pRemoteSecureCharacteristic;
4149
static BLEAdvertisedDevice *myDevice;
4250

51+
// Print an IRK buffer as hex with leading zeros and ':' separator
52+
static void printIrkBinary(uint8_t *irk) {
53+
for (int i = 0; i < 16; i++) {
54+
if (irk[i] < 0x10) {
55+
Serial.print("0");
56+
}
57+
Serial.print(irk[i], HEX);
58+
if (i < 15) {
59+
Serial.print(":");
60+
}
61+
}
62+
}
63+
64+
static void get_peer_irk(BLEAddress peerAddr) {
65+
Serial.println("\n=== Retrieving peer IRK (Server) ===\n");
66+
67+
uint8_t irk[16];
68+
69+
// Get IRK in binary format
70+
if (BLEDevice::getPeerIRK(peerAddr, irk)) {
71+
Serial.println("Successfully retrieved peer IRK in binary format:");
72+
printIrkBinary(irk);
73+
Serial.println("\n");
74+
}
75+
76+
// Get IRK in different string formats
77+
String irkString = BLEDevice::getPeerIRKString(peerAddr);
78+
String irkBase64 = BLEDevice::getPeerIRKBase64(peerAddr);
79+
String irkReverse = BLEDevice::getPeerIRKReverse(peerAddr);
80+
81+
if (irkString.length() > 0) {
82+
Serial.println("Successfully retrieved peer IRK in multiple formats:\n");
83+
Serial.print("IRK (comma-separated hex): ");
84+
Serial.println(irkString);
85+
Serial.print("IRK (Base64 for Home Assistant Private BLE Device): ");
86+
Serial.println(irkBase64);
87+
Serial.print("IRK (reverse hex for Home Assistant ESPresense): ");
88+
Serial.println(irkReverse);
89+
Serial.println();
90+
} else {
91+
Serial.println("!!! Failed to retrieve peer IRK !!!");
92+
Serial.println("This is expected if bonding is disabled or the peer doesn't distribute its Identity Key.");
93+
Serial.println("To enable bonding, change setAuthenticationMode to: pSecurity->setAuthenticationMode(true, true, true);\n");
94+
}
95+
96+
Serial.println("=======================================\n");
97+
}
98+
4399
// Callback function to handle notifications
44100
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
45101
Serial.print("Notify callback for characteristic ");
@@ -62,11 +118,30 @@ class MyClientCallback : public BLEClientCallbacks {
62118
}
63119
};
64120

121+
// Security callbacks to print IRKs once authentication completes
122+
class MySecurityCallbacks : public BLESecurityCallbacks {
123+
#if defined(CONFIG_BLUEDROID_ENABLED)
124+
void onAuthenticationComplete(esp_ble_auth_cmpl_t desc) override {
125+
// Print the IRK received by the peer
126+
BLEAddress peerAddr(desc.bd_addr);
127+
get_peer_irk(peerAddr);
128+
}
129+
#endif
130+
131+
#if defined(CONFIG_NIMBLE_ENABLED)
132+
void onAuthenticationComplete(ble_gap_conn_desc *desc) override {
133+
// Print the IRK received by the peer
134+
BLEAddress peerAddr(desc->peer_id_addr.val, desc->peer_id_addr.type);
135+
get_peer_irk(peerAddr);
136+
}
137+
#endif
138+
};
139+
65140
bool connectToServer() {
66141
Serial.print("Forming a secure connection to ");
67142
Serial.println(myDevice->getAddress().toString().c_str());
68143

69-
BLEClient *pClient = BLEDevice::createClient();
144+
pClient = BLEDevice::createClient();
70145
Serial.println(" - Created client");
71146

72147
pClient->setClientCallbacks(new MyClientCallback());
@@ -192,15 +267,19 @@ void setup() {
192267
pSecurity->setPassKey(true, CLIENT_PIN);
193268

194269
// Set authentication mode to match server requirements
195-
// Enable secure connection and MITM (for password prompts) for this example
196-
pSecurity->setAuthenticationMode(false, true, true);
270+
// Enable bonding, MITM (for password prompts), and secure connection for this example
271+
// Bonding is required to store and retrieve the IRK
272+
pSecurity->setAuthenticationMode(true, true, true);
197273

198274
// Set IO capability to KeyboardOnly
199275
// We need the proper IO capability for MITM authentication even
200276
// if the passkey is static and won't be entered by the user
201277
// See https://www.bluetooth.com/blog/bluetooth-pairing-part-2-key-generation-methods/
202278
pSecurity->setCapability(ESP_IO_CAP_IN);
203279

280+
// Set callbacks to handle authentication completion and print IRKs
281+
BLEDevice::setSecurityCallbacks(new MySecurityCallbacks());
282+
204283
// Retrieve a Scanner and set the callback we want to use to be informed when we
205284
// have detected a new device. Specify that we want active scanning and start the
206285
// scan to run for 5 seconds.

libraries/BLE/examples/Server_secure_static_passkey/Server_secure_static_passkey.ino

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,73 @@
4040
// This is an example passkey. You should use a different or random passkey.
4141
#define SERVER_PIN 123456
4242

43+
// Print an IRK buffer as hex with leading zeros and ':' separator
44+
static void printIrkBinary(uint8_t *irk) {
45+
for (int i = 0; i < 16; i++) {
46+
if (irk[i] < 0x10) {
47+
Serial.print("0");
48+
}
49+
Serial.print(irk[i], HEX);
50+
if (i < 15) {
51+
Serial.print(":");
52+
}
53+
}
54+
}
55+
56+
static void get_peer_irk(BLEAddress peerAddr) {
57+
Serial.println("\n=== Retrieving peer IRK (Client) ===\n");
58+
59+
uint8_t irk[16];
60+
61+
// Get IRK in binary format
62+
if (BLEDevice::getPeerIRK(peerAddr, irk)) {
63+
Serial.println("Successfully retrieved peer IRK in binary format:");
64+
printIrkBinary(irk);
65+
Serial.println("\n");
66+
}
67+
68+
// Get IRK in different string formats
69+
String irkString = BLEDevice::getPeerIRKString(peerAddr);
70+
String irkBase64 = BLEDevice::getPeerIRKBase64(peerAddr);
71+
String irkReverse = BLEDevice::getPeerIRKReverse(peerAddr);
72+
73+
if (irkString.length() > 0) {
74+
Serial.println("Successfully retrieved peer IRK in multiple formats:\n");
75+
Serial.print("IRK (comma-separated hex): ");
76+
Serial.println(irkString);
77+
Serial.print("IRK (Base64 for Home Assistant Private BLE Device): ");
78+
Serial.println(irkBase64);
79+
Serial.print("IRK (reverse hex for Home Assistant ESPresense): ");
80+
Serial.println(irkReverse);
81+
Serial.println();
82+
} else {
83+
Serial.println("!!! Failed to retrieve peer IRK !!!");
84+
Serial.println("This is expected if bonding is disabled or the peer doesn't distribute its Identity Key.");
85+
Serial.println("To enable bonding, change setAuthenticationMode to: pSecurity->setAuthenticationMode(true, true, true);\n");
86+
}
87+
88+
Serial.println("=======================================\n");
89+
}
90+
91+
// Security callbacks to print IRKs once authentication completes
92+
class MySecurityCallbacks : public BLESecurityCallbacks {
93+
#if defined(CONFIG_BLUEDROID_ENABLED)
94+
void onAuthenticationComplete(esp_ble_auth_cmpl_t desc) override {
95+
// Print the IRK received by the peer
96+
BLEAddress peerAddr(desc.bd_addr);
97+
get_peer_irk(peerAddr);
98+
}
99+
#endif
100+
101+
#if defined(CONFIG_NIMBLE_ENABLED)
102+
void onAuthenticationComplete(ble_gap_conn_desc *desc) override {
103+
// Print the IRK received by the peer
104+
BLEAddress peerAddr(desc->peer_id_addr.val, desc->peer_id_addr.type);
105+
get_peer_irk(peerAddr);
106+
}
107+
#endif
108+
};
109+
43110
void setup() {
44111
Serial.begin(115200);
45112
Serial.println("Starting BLE work!");
@@ -76,8 +143,11 @@ void setup() {
76143
pSecurity->setCapability(ESP_IO_CAP_OUT);
77144

78145
// Set authentication mode
79-
// Require secure connection and MITM (for password prompts) for this example
80-
pSecurity->setAuthenticationMode(false, true, true);
146+
// Enable bonding, MITM (for password prompts), and secure connection for this example
147+
pSecurity->setAuthenticationMode(true, true, true);
148+
149+
// Set callbacks to handle authentication completion and print IRKs
150+
BLEDevice::setSecurityCallbacks(new MySecurityCallbacks());
81151

82152
BLEServer *pServer = BLEDevice::createServer();
83153
pServer->advertiseOnDisconnect(true);

libraries/BLE/src/BLECharacteristic.cpp

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,52 @@ void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic, esp
904904

905905
#if defined(CONFIG_NIMBLE_ENABLED)
906906

907+
/**
908+
* @brief Process a deferred write callback.
909+
*
910+
* This function is called as a FreeRTOS task to execute the onWrite callback
911+
* after the write response has been sent to the client. This maintains backwards
912+
* compatibility with Bluedroid, where the write response is sent before the
913+
* onWrite callback is invoked.
914+
*
915+
* The delay is based on the connection interval to ensure the write response
916+
* packet has been transmitted over the air before the callback executes.
917+
*
918+
* See: https://github.com/espressif/arduino-esp32/issues/11938
919+
*/
920+
void BLECharacteristic::processDeferredWriteCallback(void *pvParameters) {
921+
DeferredWriteCallback *pCallback = (DeferredWriteCallback *)pvParameters;
922+
923+
// Get connection parameters to calculate appropriate delay
924+
ble_gap_conn_desc desc;
925+
int rc = ble_gap_conn_find(pCallback->conn_handle, &desc);
926+
927+
if (rc == 0) {
928+
// Connection interval is in units of 1.25ms
929+
// Wait for at least one connection interval to ensure the write response
930+
// has been transmitted. Add a small buffer for processing.
931+
uint16_t intervalMs = (desc.conn_itvl * 125) / 100; // Convert to milliseconds
932+
uint16_t delayMs = intervalMs + 5; // Add 5ms buffer
933+
934+
log_v("Deferring write callback by %dms (conn_interval=%d units, %dms)", delayMs, desc.conn_itvl, intervalMs);
935+
vTaskDelay(pdMS_TO_TICKS(delayMs));
936+
} else {
937+
// If we can't get connection parameters, use a conservative default
938+
// Most connections use 7.5-30ms intervals, so 50ms should be safe
939+
log_w("Could not get connection parameters, using default 50ms delay");
940+
vTaskDelay(pdMS_TO_TICKS(50));
941+
}
942+
943+
// Call the onWrite callback now that the response has been transmitted
944+
pCallback->pCharacteristic->m_pCallbacks->onWrite(pCallback->pCharacteristic, &pCallback->desc);
945+
946+
// Free the allocated memory
947+
delete pCallback;
948+
949+
// Delete this one-shot task
950+
vTaskDelete(NULL);
951+
}
952+
907953
int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
908954
const ble_uuid_t *uuid;
909955
int rc;
@@ -955,7 +1001,28 @@ int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr
9551001
rc = ble_gap_conn_find(conn_handle, &desc);
9561002
assert(rc == 0);
9571003
pCharacteristic->setValue(buf, len);
958-
pCharacteristic->m_pCallbacks->onWrite(pCharacteristic, &desc);
1004+
1005+
// Defer the onWrite callback to maintain backwards compatibility with Bluedroid.
1006+
// In Bluedroid, the write response is sent BEFORE the onWrite callback is invoked.
1007+
// In NimBLE, the response is sent implicitly when this function returns.
1008+
// By deferring the callback to a separate task with a delay based on the connection
1009+
// interval, we ensure the response packet is transmitted before the callback executes.
1010+
// See: https://github.com/espressif/arduino-esp32/issues/11938
1011+
DeferredWriteCallback *pCallback = new DeferredWriteCallback();
1012+
pCallback->pCharacteristic = pCharacteristic;
1013+
pCallback->desc = desc;
1014+
pCallback->conn_handle = conn_handle;
1015+
1016+
// Create a one-shot task to execute the callback after the response is transmitted
1017+
// Using priority 1 (low priority) and sufficient stack for callback operations
1018+
// Note: Stack must be large enough to handle notify() calls from within onWrite()
1019+
xTaskCreate(
1020+
processDeferredWriteCallback, "BLEWriteCB",
1021+
4096, // Stack size - increased to handle notify() operations
1022+
pCallback,
1023+
1, // Priority (low)
1024+
NULL // Task handle (not needed for one-shot task)
1025+
);
9591026

9601027
return 0;
9611028
}

libraries/BLE/src/BLECharacteristic.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
#include <host/ble_gatt.h>
4848
#include <host/ble_att.h>
4949
#include "BLEConnInfo.h"
50+
#include <freertos/FreeRTOS.h>
51+
#include <freertos/task.h>
5052
#define ESP_GATT_MAX_ATTR_LEN BLE_ATT_ATTR_MAX_LEN
5153
#define ESP_GATT_CHAR_PROP_BIT_READ BLE_GATT_CHR_PROP_READ
5254
#define ESP_GATT_CHAR_PROP_BIT_WRITE BLE_GATT_CHR_PROP_WRITE
@@ -246,6 +248,13 @@ class BLECharacteristic {
246248
portMUX_TYPE m_readMux;
247249
uint8_t m_removed;
248250
std::vector<std::pair<uint16_t, uint16_t>> m_subscribedVec;
251+
252+
// Deferred callback support for maintaining backwards compatibility with Bluedroid timing
253+
struct DeferredWriteCallback {
254+
BLECharacteristic *pCharacteristic;
255+
ble_gap_conn_desc desc;
256+
uint16_t conn_handle;
257+
};
249258
#endif
250259

251260
/***************************************************************************
@@ -271,6 +280,7 @@ class BLECharacteristic {
271280
#if defined(CONFIG_NIMBLE_ENABLED)
272281
void setSubscribe(struct ble_gap_event *event);
273282
static int handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
283+
static void processDeferredWriteCallback(void *pvParameters);
274284
#endif
275285
}; // BLECharacteristic
276286

0 commit comments

Comments
 (0)