Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
/*
Secure client with static passkey
Secure client with static passkey and IRK retrieval

This example demonstrates how to create a secure BLE client that connects to
a secure BLE server using a static passkey without prompting the user.
The client will automatically use the same passkey (123456) as the server.

After successful bonding, the example demonstrates how to retrieve the
server's Identity Resolving Key (IRK) in multiple formats:
- Comma-separated hex format: 0x1A,0x1B,0x1C,...
- Base64 encoded (for Home Assistant Private BLE Device service)
- Reverse hex order (for Home Assistant ESPresense)

This client is designed to work with the Server_secure_static_passkey example.

Note that ESP32 uses Bluedroid by default and the other SoCs use NimBLE.
Bluedroid initiates security on-connect, while NimBLE initiates security on-demand.
This means that in NimBLE you can read the insecure characteristic without entering
the passkey. This is not possible in Bluedroid.

IMPORTANT: MITM (Man-In-The-Middle protection) must be enabled for password prompts
to work. Without MITM, the BLE stack assumes no user interaction is needed and will use
"Just Works" pairing method (with encryption if secure connection is enabled).
IMPORTANT:
- MITM (Man-In-The-Middle protection) must be enabled for password prompts to work.
- Bonding must be enabled to store and retrieve the IRK.
- The server must distribute its Identity Key during pairing.

Based on examples from Neil Kolban and h2zero.
Created by lucasssvaz.
Expand All @@ -36,10 +43,59 @@ static BLEUUID secureCharUUID("ff1d2614-e2d6-4c87-9154-6625d39ca7f8");
static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLEClient *pClient = nullptr;
static BLERemoteCharacteristic *pRemoteInsecureCharacteristic;
static BLERemoteCharacteristic *pRemoteSecureCharacteristic;
static BLEAdvertisedDevice *myDevice;

// Print an IRK buffer as hex with leading zeros and ':' separator
static void printIrkBinary(uint8_t *irk) {
for (int i = 0; i < 16; i++) {
if (irk[i] < 0x10) {
Serial.print("0");
}
Serial.print(irk[i], HEX);
if (i < 15) {
Serial.print(":");
}
}
}

static void get_peer_irk(BLEAddress peerAddr) {
Serial.println("\n=== Retrieving peer IRK (Server) ===\n");

uint8_t irk[16];

// Get IRK in binary format
if (BLEDevice::getPeerIRK(peerAddr, irk)) {
Serial.println("Successfully retrieved peer IRK in binary format:");
printIrkBinary(irk);
Serial.println("\n");
}

// Get IRK in different string formats
String irkString = BLEDevice::getPeerIRKString(peerAddr);
String irkBase64 = BLEDevice::getPeerIRKBase64(peerAddr);
String irkReverse = BLEDevice::getPeerIRKReverse(peerAddr);

if (irkString.length() > 0) {
Serial.println("Successfully retrieved peer IRK in multiple formats:\n");
Serial.print("IRK (comma-separated hex): ");
Serial.println(irkString);
Serial.print("IRK (Base64 for Home Assistant Private BLE Device): ");
Serial.println(irkBase64);
Serial.print("IRK (reverse hex for Home Assistant ESPresense): ");
Serial.println(irkReverse);
Serial.println();
} else {
Serial.println("!!! Failed to retrieve peer IRK !!!");
Serial.println("This is expected if bonding is disabled or the peer doesn't distribute its Identity Key.");
Serial.println("To enable bonding, change setAuthenticationMode to: pSecurity->setAuthenticationMode(true, true, true);\n");
}

Serial.println("=======================================\n");
}

// Callback function to handle notifications
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
Serial.print("Notify callback for characteristic ");
Expand All @@ -62,11 +118,30 @@ class MyClientCallback : public BLEClientCallbacks {
}
};

// Security callbacks to print IRKs once authentication completes
class MySecurityCallbacks : public BLESecurityCallbacks {
#if defined(CONFIG_BLUEDROID_ENABLED)
void onAuthenticationComplete(esp_ble_auth_cmpl_t desc) override {
// Print the IRK received by the peer
BLEAddress peerAddr(desc.bd_addr);
get_peer_irk(peerAddr);
}
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
void onAuthenticationComplete(ble_gap_conn_desc *desc) override {
// Print the IRK received by the peer
BLEAddress peerAddr(desc->peer_id_addr.val, desc->peer_id_addr.type);
get_peer_irk(peerAddr);
}
#endif
};

bool connectToServer() {
Serial.print("Forming a secure connection to ");
Serial.println(myDevice->getAddress().toString().c_str());

BLEClient *pClient = BLEDevice::createClient();
pClient = BLEDevice::createClient();
Serial.println(" - Created client");

pClient->setClientCallbacks(new MyClientCallback());
Expand Down Expand Up @@ -192,15 +267,19 @@ void setup() {
pSecurity->setPassKey(true, CLIENT_PIN);

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

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

// Set callbacks to handle authentication completion and print IRKs
BLEDevice::setSecurityCallbacks(new MySecurityCallbacks());

// Retrieve a Scanner and set the callback we want to use to be informed when we
// have detected a new device. Specify that we want active scanning and start the
// scan to run for 5 seconds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,73 @@
// This is an example passkey. You should use a different or random passkey.
#define SERVER_PIN 123456

// Print an IRK buffer as hex with leading zeros and ':' separator
static void printIrkBinary(uint8_t *irk) {
for (int i = 0; i < 16; i++) {
if (irk[i] < 0x10) {
Serial.print("0");
}
Serial.print(irk[i], HEX);
if (i < 15) {
Serial.print(":");
}
}
}

static void get_peer_irk(BLEAddress peerAddr) {
Serial.println("\n=== Retrieving peer IRK (Client) ===\n");

uint8_t irk[16];

// Get IRK in binary format
if (BLEDevice::getPeerIRK(peerAddr, irk)) {
Serial.println("Successfully retrieved peer IRK in binary format:");
printIrkBinary(irk);
Serial.println("\n");
}

// Get IRK in different string formats
String irkString = BLEDevice::getPeerIRKString(peerAddr);
String irkBase64 = BLEDevice::getPeerIRKBase64(peerAddr);
String irkReverse = BLEDevice::getPeerIRKReverse(peerAddr);

if (irkString.length() > 0) {
Serial.println("Successfully retrieved peer IRK in multiple formats:\n");
Serial.print("IRK (comma-separated hex): ");
Serial.println(irkString);
Serial.print("IRK (Base64 for Home Assistant Private BLE Device): ");
Serial.println(irkBase64);
Serial.print("IRK (reverse hex for Home Assistant ESPresense): ");
Serial.println(irkReverse);
Serial.println();
} else {
Serial.println("!!! Failed to retrieve peer IRK !!!");
Serial.println("This is expected if bonding is disabled or the peer doesn't distribute its Identity Key.");
Serial.println("To enable bonding, change setAuthenticationMode to: pSecurity->setAuthenticationMode(true, true, true);\n");
}

Serial.println("=======================================\n");
}

// Security callbacks to print IRKs once authentication completes
class MySecurityCallbacks : public BLESecurityCallbacks {
#if defined(CONFIG_BLUEDROID_ENABLED)
void onAuthenticationComplete(esp_ble_auth_cmpl_t desc) override {
// Print the IRK received by the peer
BLEAddress peerAddr(desc.bd_addr);
get_peer_irk(peerAddr);
}
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
void onAuthenticationComplete(ble_gap_conn_desc *desc) override {
// Print the IRK received by the peer
BLEAddress peerAddr(desc->peer_id_addr.val, desc->peer_id_addr.type);
get_peer_irk(peerAddr);
}
#endif
};

void setup() {
Serial.begin(115200);
Serial.println("Starting BLE work!");
Expand Down Expand Up @@ -76,8 +143,11 @@ void setup() {
pSecurity->setCapability(ESP_IO_CAP_OUT);

// Set authentication mode
// Require secure connection and MITM (for password prompts) for this example
pSecurity->setAuthenticationMode(false, true, true);
// Enable bonding, MITM (for password prompts), and secure connection for this example
pSecurity->setAuthenticationMode(true, true, true);

// Set callbacks to handle authentication completion and print IRKs
BLEDevice::setSecurityCallbacks(new MySecurityCallbacks());

BLEServer *pServer = BLEDevice::createServer();
pServer->advertiseOnDisconnect(true);
Expand Down
70 changes: 69 additions & 1 deletion libraries/BLE/src/BLECharacteristic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,52 @@ void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic, esp

#if defined(CONFIG_NIMBLE_ENABLED)

/**
* @brief Process a deferred write callback.
*
* This function is called as a FreeRTOS task to execute the onWrite callback
* after the write response has been sent to the client. This maintains backwards
* compatibility with Bluedroid, where the write response is sent before the
* onWrite callback is invoked.
*
* The delay is based on the connection interval to ensure the write response
* packet has been transmitted over the air before the callback executes.
*
* See: https://github.com/espressif/arduino-esp32/issues/11938
*/
void BLECharacteristic::processDeferredWriteCallback(void *pvParameters) {
DeferredWriteCallback *pCallback = (DeferredWriteCallback *)pvParameters;

// Get connection parameters to calculate appropriate delay
ble_gap_conn_desc desc;
int rc = ble_gap_conn_find(pCallback->conn_handle, &desc);

if (rc == 0) {
// Connection interval is in units of 1.25ms
// Wait for at least one connection interval to ensure the write response
// has been transmitted. Add a small buffer for processing.
uint16_t intervalMs = (desc.conn_itvl * 125) / 100; // Convert to milliseconds
uint16_t delayMs = intervalMs + 5; // Add 5ms buffer

log_v("Deferring write callback by %dms (conn_interval=%d units, %dms)", delayMs, desc.conn_itvl, intervalMs);
vTaskDelay(pdMS_TO_TICKS(delayMs));
} else {
// If we can't get connection parameters, use a conservative default
// Most connections use 7.5-30ms intervals, so 50ms should be safe
log_w("Could not get connection parameters, using default 50ms delay");
vTaskDelay(pdMS_TO_TICKS(50));
}

// Call the onWrite callback now that the response has been transmitted
pCallback->pCharacteristic->m_pCallbacks->onWrite(pCallback->pCharacteristic, &pCallback->desc);

// Free the allocated memory
delete pCallback;

// Delete this one-shot task
vTaskDelete(NULL);
}

int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
const ble_uuid_t *uuid;
int rc;
Expand Down Expand Up @@ -955,7 +1001,29 @@ int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr
rc = ble_gap_conn_find(conn_handle, &desc);
assert(rc == 0);
pCharacteristic->setValue(buf, len);
pCharacteristic->m_pCallbacks->onWrite(pCharacteristic, &desc);

// Defer the onWrite callback to maintain backwards compatibility with Bluedroid.
// In Bluedroid, the write response is sent BEFORE the onWrite callback is invoked.
// In NimBLE, the response is sent implicitly when this function returns.
// By deferring the callback to a separate task with a delay based on the connection
// interval, we ensure the response packet is transmitted before the callback executes.
// See: https://github.com/espressif/arduino-esp32/issues/11938
DeferredWriteCallback *pCallback = new DeferredWriteCallback();
pCallback->pCharacteristic = pCharacteristic;
pCallback->desc = desc;
pCallback->conn_handle = conn_handle;

// Create a one-shot task to execute the callback after the response is transmitted
// Using priority 1 (low priority) and sufficient stack for callback operations
// Note: Stack must be large enough to handle notify() calls from within onWrite()
xTaskCreate(
processDeferredWriteCallback,
"BLEWriteCB",
4096, // Stack size - increased to handle notify() operations
pCallback,
1, // Priority (low)
NULL // Task handle (not needed for one-shot task)
);

return 0;
}
Expand Down
10 changes: 10 additions & 0 deletions libraries/BLE/src/BLECharacteristic.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
#include <host/ble_gatt.h>
#include <host/ble_att.h>
#include "BLEConnInfo.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#define ESP_GATT_MAX_ATTR_LEN BLE_ATT_ATTR_MAX_LEN
#define ESP_GATT_CHAR_PROP_BIT_READ BLE_GATT_CHR_PROP_READ
#define ESP_GATT_CHAR_PROP_BIT_WRITE BLE_GATT_CHR_PROP_WRITE
Expand Down Expand Up @@ -246,6 +248,13 @@ class BLECharacteristic {
portMUX_TYPE m_readMux;
uint8_t m_removed;
std::vector<std::pair<uint16_t, uint16_t>> m_subscribedVec;

// Deferred callback support for maintaining backwards compatibility with Bluedroid timing
struct DeferredWriteCallback {
BLECharacteristic *pCharacteristic;
ble_gap_conn_desc desc;
uint16_t conn_handle;
};
#endif

/***************************************************************************
Expand All @@ -271,6 +280,7 @@ class BLECharacteristic {
#if defined(CONFIG_NIMBLE_ENABLED)
void setSubscribe(struct ble_gap_event *event);
static int handleGATTServerEvent(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
static void processDeferredWriteCallback(void *pvParameters);
#endif
}; // BLECharacteristic

Expand Down
Loading
Loading