.. | ||
main | ||
CMakeLists.txt | ||
README.md | ||
sdkconfig.defaults | ||
sdkconfig.defaults.esp32 | ||
sdkconfig.defaults.esp32c3 | ||
sdkconfig.defaults.esp32c6 | ||
sdkconfig.defaults.esp32h2 | ||
sdkconfig.defaults.esp32s3 |
Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S3 |
---|
NimBLE Security Example
Overview
This example is extended from NimBLE GATT Server Example, and further introduces
- How to set random non-resolvable private address for device
- How to ask for connection encryption from peripheral side on characteristic access
- How to bond with peer device using a random generated 6-digit passkey
It uses ESP32's Bluetooth controller and NimBLE host stack.
To test this demo, any BLE scanner application can be used.
Try It Yourself
Set Target
Before project configuration and build, be sure to set the correct chip target using:
idf.py set-target <chip_name>
For example, if you're using ESP32, then input
idf.py set-target esp32
Build and Flash
Run the following command to build, flash and monitor the project.
idf.py -p <PORT> flash monitor
For example, if the corresponding serial port is /dev/ttyACM0
, then it goes
idf.py -p /dev/ttyACM0 flash monitor
(To exit the serial monitor, type Ctrl-]
.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
Code Explained
Overview
The following is additional content compared to the NimBLE GATT Server example.
- Initialization procedure is generally similar to NimBLE GATT Server Example, but we'll initialize random number generator in the beginning, and in
nimble_host_config_init
we will configure security manager to enable related features - On stack sync, a random non-resolvable private address is generated and set as the device address
- Characteristics' permission modified to require connection encryption when accessing
- 3 more GAP event branches added in
gap_event_handler
to handle encryption related events
Entry Point
In nimble_host_config_init
function, we're going to enable some security manager features, including
- Bonding
- Man-in-the-middle protection
- Key distribution
Also, we're going to set the IO capability to BLE_HS_IO_DISPLAY_ONLY
, since it's possible to print out the passkey to serial output.
static void nimble_host_config_init(void) {
...
/* Security manager configuration */
ble_hs_cfg.sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;
ble_hs_cfg.sm_bonding = 1;
ble_hs_cfg.sm_mitm = 1;
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
...
}
GATT Server Updates
For heart rate characteristic and LED characteristic, BLE_GATT_CHR_F_READ_ENC
and BLE_GATT_CHR_F_WRITE_ENC
flag are added respectively to require connection encryption when GATT client tries to access the characteristic. Thanks to NimBLE host stack, the connection encryption will be initiated automatically by adding these flags.
However, heart rate characteristic is also indicatable, and NimBLE host stack does not offer an implementation for indication access to require connection encryption, so we need to do it ourselves. For GATT server, we simply check connection security status by calling an external function is_connection_encrypted
in send_heart_rate_indication
function to determine if the indication should be sent. This external function is defined in GAP layer, and we'll talk about it in GAP Event Handler Updates section.
void send_heart_rate_indication(void) {
/* Check if connection handle is initialized */
if (!heart_rate_chr_conn_handle_inited) {
return;
}
/* Check indication and security status */
if (heart_rate_ind_status &&
is_connection_encrypted(heart_rate_chr_conn_handle)) {
ble_gatts_indicate(heart_rate_chr_conn_handle,
heart_rate_chr_val_handle);
}
}
Random Address
In the following function, we can generate a random non-resolvable private address and set as the device address. We will call it in adv_init
function before ensuring address availability.
static void set_random_addr(void) {
/* Local variables */
int rc = 0;
ble_addr_t addr;
/* Generate new non-resolvable private address */
rc = ble_hs_id_gen_rnd(0, &addr);
assert(rc == 0);
/* Set address */
rc = ble_hs_id_set_rnd(addr.val);
assert(rc == 0);
}
Check Connection Encryption Status
By connection handle, we can fetch connection descriptor from NimBLE host stack, and there's a flag indicating connection encryption status, check the following codes
bool is_connection_encrypted(uint16_t conn_handle) {
/* Local variables */
int rc = 0;
struct ble_gap_conn_desc desc;
/* Print connection descriptor */
rc = ble_gap_conn_find(conn_handle, &desc);
if (rc != 0) {
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
rc);
return false;
}
return desc.sec_state.encrypted;
}
GAP Event Handler Updates
3 more GAP event branches are added in gap_event_handler
, which are
BLE_GAP_EVENT_ENC_CHANGE
- Encryption change eventBLE_GAP_EVENT_REPEAT_PAIRING
- Repeat pairing eventBLE_GAP_EVENT_PASSKEY_ACTION
- Passkey action event
On encryption change event, we're going to print the encryption change status to output.
/* Encryption change event */
case BLE_GAP_EVENT_ENC_CHANGE:
/* Encryption has been enabled or disabled for this connection. */
if (event->enc_change.status == 0) {
ESP_LOGI(TAG, "connection encrypted!");
} else {
ESP_LOGE(TAG, "connection encryption failed, status: %d",
event->enc_change.status);
}
return rc;
On repeat pairing event, to make it simple, we will just delete the old bond and repeat pairing.
/* Repeat pairing event */
case BLE_GAP_EVENT_REPEAT_PAIRING:
/* Delete the old bond */
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
if (rc != 0) {
ESP_LOGE(TAG, "failed to find connection, error code %d", rc);
return rc;
}
ble_store_util_delete_peer(&desc.peer_id_addr);
/* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
* continue with pairing operation */
ESP_LOGI(TAG, "repairing...");
return BLE_GAP_REPEAT_PAIRING_RETRY;
On passkey action event, a random 6-digit passkey is generated, and you are supposed to enter the same passkey on pairing. If the input is consistent with the generated passkey, you should be able to bond with the device.
/* Passkey action event */
case BLE_GAP_EVENT_PASSKEY_ACTION:
/* Display action */
if (event->passkey.params.action == BLE_SM_IOACT_DISP) {
/* Generate passkey */
struct ble_sm_io pkey = {0};
pkey.action = event->passkey.params.action;
pkey.passkey = 100000 + esp_random() % 900000;
ESP_LOGI(TAG, "enter passkey %" PRIu32 " on the peer side",
pkey.passkey);
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
if (rc != 0) {
ESP_LOGE(TAG,
"failed to inject security manager io, error code: %d",
rc);
return rc;
}
}
return rc;
Observation
If everything goes well, pairing will be required when you try to access any of the characteristics, that is, read or indicate heart rate characteristic, or write LED characteristic.
Troubleshooting
For any technical queries, please file an issue on GitHub. We will get back to you soon.