From ac80bbe285a31d7fb69c506c10730989c4c60589 Mon Sep 17 00:00:00 2001 From: zhiweijian Date: Thu, 18 Dec 2025 21:13:29 +0800 Subject: [PATCH] feat(ble/bluedroid): Support bluedroid encrypted advertising data --- components/bt/host/bluedroid/Kconfig.in | 10 + .../bt/host/bluedroid/api/esp_gap_ble_api.c | 22 + .../api/include/api/esp_gap_ble_api.h | 19 + .../bt/host/bluedroid/bta/dm/bta_dm_act.c | 16 + .../bt/host/bluedroid/bta/dm/bta_dm_api.c | 35 ++ .../bt/host/bluedroid/bta/dm/bta_dm_main.c | 3 + .../bluedroid/bta/dm/include/bta_dm_int.h | 17 + .../host/bluedroid/bta/include/bta/bta_api.h | 16 + .../btc/profile/std/gap/btc_gap_ble.c | 5 + .../btc/profile/std/include/btc_gap_ble.h | 10 + .../include/common/bluedroid_user_config.h | 6 + .../common/include/common/bt_target.h | 6 + .../bt/host/bluedroid/stack/btm/btm_ble_gap.c | 33 ++ .../bt/host/bluedroid/stack/gap/gap_ble.c | 33 ++ .../bluedroid/stack/gap/include/gap_int.h | 4 + .../stack/include/stack/btm_ble_api.h | 16 + .../bluedroid/stack/include/stack/gap_api.h | 14 + .../bluedroid/stack/include/stack/gattdefs.h | 1 + .../enc_adv_data_cent/CMakeLists.txt | 8 + .../enc_adv_data_cent/README.md | 273 +++++++++ .../enc_adv_data_cent/main/CMakeLists.txt | 8 + .../enc_adv_data_cent/main/Kconfig.projbuild | 22 + .../enc_adv_data_cent/main/ble_ead.c | 426 ++++++++++++++ .../enc_adv_data_cent/main/ble_ead.h | 95 ++++ .../main/enc_adv_data_cent.c | 519 ++++++++++++++++++ .../main/enc_adv_data_cent_no_connect.c | 239 ++++++++ .../enc_adv_data_cent/sdkconfig.defaults | 13 + .../enc_adv_data_prph/CMakeLists.txt | 8 + .../enc_adv_data_prph/README.md | 172 ++++++ .../enc_adv_data_prph/main/CMakeLists.txt | 2 + .../enc_adv_data_prph/main/Kconfig.projbuild | 14 + .../enc_adv_data_prph/main/ble_ead.c | 419 ++++++++++++++ .../enc_adv_data_prph/main/ble_ead.h | 95 ++++ .../main/enc_adv_data_prph.c | 353 ++++++++++++ .../enc_adv_data_prph/sdkconfig.defaults | 13 + 35 files changed, 2945 insertions(+) create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/CMakeLists.txt create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/README.md create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/CMakeLists.txt create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/Kconfig.projbuild create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.h create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent_no_connect.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/sdkconfig.defaults create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/CMakeLists.txt create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/README.md create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/CMakeLists.txt create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/Kconfig.projbuild create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.h create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/enc_adv_data_prph.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/sdkconfig.defaults diff --git a/components/bt/host/bluedroid/Kconfig.in b/components/bt/host/bluedroid/Kconfig.in index f188736b55..022913ca29 100644 --- a/components/bt/host/bluedroid/Kconfig.in +++ b/components/bt/host/bluedroid/Kconfig.in @@ -377,6 +377,16 @@ config BT_GATTS_SECURITY_LEVELS_CHAR help Enable LE GATT Security Levels Characteristic +config BT_GATTS_KEY_MATERIAL_CHAR + bool "Enable Encrypted Data Key Material Characteristic" + depends on BT_GATTS_ENABLE + default n + help + Enable the Encrypted Data Key Material characteristic in GAP service. + This characteristic allows advertising data to be decrypted and authenticated + using the key material (session key + IV) as defined in Bluetooth Core + Specification Version 5.4. The characteristic requires encrypted link to read. + menuconfig BT_GATTC_ENABLE bool "Include GATT client module(GATTC)" depends on BT_BLE_ENABLED diff --git a/components/bt/host/bluedroid/api/esp_gap_ble_api.c b/components/bt/host/bluedroid/api/esp_gap_ble_api.c index 906e1280ba..8df0a9587f 100644 --- a/components/bt/host/bluedroid/api/esp_gap_ble_api.c +++ b/components/bt/host/bluedroid/api/esp_gap_ble_api.c @@ -440,6 +440,28 @@ esp_err_t esp_ble_gap_get_device_name(void) return (btc_transfer_context(&msg, NULL, 0, NULL, NULL) == BT_STATUS_SUCCESS ? ESP_OK : ESP_FAIL); } +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +esp_err_t esp_ble_gap_set_key_material(const uint8_t session_key[16], const uint8_t iv[8]) +{ + btc_msg_t msg = {0}; + btc_ble_gap_args_t arg; + + ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED); + + if (session_key == NULL || iv == NULL) { + return ESP_ERR_INVALID_ARG; + } + + msg.sig = BTC_SIG_API_CALL; + msg.pid = BTC_PID_GAP_BLE; + msg.act = BTC_GAP_BLE_ACT_SET_KEY_MATERIAL; + memcpy(arg.set_key_material.session_key, session_key, 16); + memcpy(arg.set_key_material.iv, iv, 8); + + return (btc_transfer_context(&msg, &arg, sizeof(btc_ble_gap_args_t), NULL, NULL) == BT_STATUS_SUCCESS ? ESP_OK : ESP_FAIL); +} +#endif // (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + esp_err_t esp_ble_gap_get_local_used_addr(esp_bd_addr_t local_used_addr, uint8_t * addr_type) { if(esp_bluedroid_get_status() != (ESP_BLUEDROID_STATUS_ENABLED)) { diff --git a/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h b/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h index ba0f524c68..382ebf5f9c 100644 --- a/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h +++ b/components/bt/host/bluedroid/api/include/api/esp_gap_ble_api.h @@ -3141,6 +3141,25 @@ esp_err_t esp_ble_gap_set_device_name(const char *name); */ esp_err_t esp_ble_gap_get_device_name(void); +#if defined(CONFIG_BT_GATTS_KEY_MATERIAL_CHAR) && CONFIG_BT_GATTS_KEY_MATERIAL_CHAR +/** + * @brief Set the Encrypted Data Key Material in GAP service + * + * This function sets the session key and IV that will be exposed + * through the Key Material characteristic (UUID 0x2B88) in the GAP service. + * The Key Material allows central devices to decrypt encrypted advertising data. + * + * @param[in] session_key - 16-byte (128-bit) session key for AES-CCM encryption + * @param[in] iv - 8-byte (64-bit) initialization vector + * + * @return + * - ESP_OK : success + * - other : failed + * + */ +esp_err_t esp_ble_gap_set_key_material(const uint8_t session_key[16], const uint8_t iv[8]); +#endif // CONFIG_BT_GATTS_KEY_MATERIAL_CHAR + /** * @brief This function is called to get local used address and address type. * uint8_t *esp_bt_dev_get_address(void) get the public address diff --git a/components/bt/host/bluedroid/bta/dm/bta_dm_act.c b/components/bt/host/bluedroid/bta/dm/bta_dm_act.c index 724ea7c67a..5f6e490b5b 100644 --- a/components/bt/host/bluedroid/bta/dm/bta_dm_act.c +++ b/components/bt/host/bluedroid/bta/dm/bta_dm_act.c @@ -5406,6 +5406,22 @@ void bta_dm_ble_config_local_icon (tBTA_DM_MSG *p_data) BTM_BleConfigLocalIcon (p_data->ble_local_icon.icon); } +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +/******************************************************************************* +** +** Function bta_dm_ble_set_key_material +** +** Description This function sets the Encrypted Data Key Material. +** +** +*******************************************************************************/ +void bta_dm_ble_set_key_material (tBTA_DM_MSG *p_data) +{ + BTM_BleSetKeyMaterial (p_data->ble_key_material.session_key, + p_data->ble_key_material.iv); +} +#endif + #if (BLE_HOST_BLE_OBSERVE_EN == TRUE) /******************************************************************************* ** diff --git a/components/bt/host/bluedroid/bta/dm/bta_dm_api.c b/components/bt/host/bluedroid/bta/dm/bta_dm_api.c index cd06eb4fd1..6dcce7beac 100644 --- a/components/bt/host/bluedroid/bta/dm/bta_dm_api.c +++ b/components/bt/host/bluedroid/bta/dm/bta_dm_api.c @@ -2223,6 +2223,41 @@ void BTA_DmBleConfigLocalIcon(uint16_t icon) } } +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +/******************************************************************************* +** +** Function BTA_DmBleSetKeyMaterial +** +** Description Set the Encrypted Data Key Material in GAP service +** +** Parameters: session_key - 16-byte session key (must not be NULL) +** iv - 8-byte initialization vector (must not be NULL) +** +** Returns void +** +*******************************************************************************/ +void BTA_DmBleSetKeyMaterial(const uint8_t *session_key, const uint8_t *iv) +{ + tBTA_DM_API_KEY_MATERIAL *p_msg; + + if (session_key == NULL || iv == NULL) { + APPL_TRACE_ERROR("%s: NULL pointer parameter", __func__); + return; + } + + if ((p_msg = (tBTA_DM_API_KEY_MATERIAL *) osi_malloc(sizeof(tBTA_DM_API_KEY_MATERIAL))) != NULL) { + memset(p_msg, 0, sizeof(tBTA_DM_API_KEY_MATERIAL)); + + p_msg->hdr.event = BTA_DM_API_KEY_MATERIAL_EVT; + memcpy(p_msg->session_key, session_key, 16); + memcpy(p_msg->iv, iv, 8); + bta_sys_sendmsg(p_msg); + } else { + APPL_TRACE_ERROR("%s: failed to allocate memory", __func__); + } +} +#endif + #if (BLE_HOST_BLE_MULTI_ADV_EN == TRUE) /******************************************************************************* ** diff --git a/components/bt/host/bluedroid/bta/dm/bta_dm_main.c b/components/bt/host/bluedroid/bta/dm/bta_dm_main.c index 77bc60a7f2..1d69fb2434 100644 --- a/components/bt/host/bluedroid/bta/dm/bta_dm_main.c +++ b/components/bt/host/bluedroid/bta/dm/bta_dm_main.c @@ -170,6 +170,9 @@ const tBTA_DM_ACTION bta_dm_action[BTA_DM_MAX_EVT] = { bta_dm_ble_config_local_privacy, /* BTA_DM_API_LOCAL_PRIVACY_EVT */ #endif bta_dm_ble_config_local_icon, /* BTA_DM_API_LOCAL_ICON_EVT */ +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + bta_dm_ble_set_key_material, /* BTA_DM_API_KEY_MATERIAL_EVT */ +#endif #if (BLE_42_ADV_EN == TRUE) bta_dm_ble_set_adv_params_all, /* BTA_DM_API_BLE_ADV_PARAM_All_EVT */ bta_dm_ble_set_adv_config, /* BTA_DM_API_BLE_SET_ADV_CONFIG_EVT */ diff --git a/components/bt/host/bluedroid/bta/dm/include/bta_dm_int.h b/components/bt/host/bluedroid/bta/dm/include/bta_dm_int.h index 9ef189a26f..bc625d3df8 100644 --- a/components/bt/host/bluedroid/bta/dm/include/bta_dm_int.h +++ b/components/bt/host/bluedroid/bta/dm/include/bta_dm_int.h @@ -159,6 +159,9 @@ enum { BTA_DM_API_LOCAL_PRIVACY_EVT, #endif BTA_DM_API_LOCAL_ICON_EVT, +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + BTA_DM_API_KEY_MATERIAL_EVT, +#endif /*******This event added by Yulong at 2016/10/20 to support setting the ble advertising param by the APP******/ @@ -853,6 +856,14 @@ typedef struct { uint16_t icon; } tBTA_DM_API_LOCAL_ICON; +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +typedef struct { + BT_HDR hdr; + uint8_t session_key[16]; + uint8_t iv[8]; +} tBTA_DM_API_KEY_MATERIAL; +#endif + /* set scan parameter for BLE connections */ typedef struct { BT_HDR hdr; @@ -1900,6 +1911,9 @@ typedef union { tBTA_DM_API_ENABLE_PRIVACY ble_remote_privacy; tBTA_DM_API_LOCAL_PRIVACY ble_local_privacy; tBTA_DM_API_LOCAL_ICON ble_local_icon; +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + tBTA_DM_API_KEY_MATERIAL ble_key_material; +#endif tBTA_DM_API_BLE_ADV_PARAMS_ALL ble_set_adv_params_all; tBTA_DM_API_SET_ADV_CONFIG ble_set_adv_data; tBTA_DM_API_SET_ADV_CONFIG_RAW ble_set_adv_data_raw; @@ -2504,6 +2518,9 @@ extern void bta_dm_ble_stop_advertising(tBTA_DM_MSG *p_data); #endif // #if (BLE_HOST_STOP_ADV_UNUSED == TRUE) extern void bta_dm_ble_config_local_privacy (tBTA_DM_MSG *p_data); extern void bta_dm_ble_config_local_icon (tBTA_DM_MSG *p_data); +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +extern void bta_dm_ble_set_key_material (tBTA_DM_MSG *p_data); +#endif extern void bta_dm_ble_set_adv_params_all(tBTA_DM_MSG *p_data); extern void bta_dm_ble_set_adv_config (tBTA_DM_MSG *p_data); extern void bta_dm_ble_set_adv_config_raw (tBTA_DM_MSG *p_data); diff --git a/components/bt/host/bluedroid/bta/include/bta/bta_api.h b/components/bt/host/bluedroid/bta/include/bta/bta_api.h index 23bf8915d9..a75e48943b 100644 --- a/components/bt/host/bluedroid/bta/include/bta/bta_api.h +++ b/components/bt/host/bluedroid/bta/include/bta/bta_api.h @@ -2939,6 +2939,22 @@ extern void BTA_DmBleConfigLocalPrivacy(BOOLEAN privacy_enable, tBTA_SET_LOCAL_P *******************************************************************************/ extern void BTA_DmBleConfigLocalIcon(uint16_t icon); +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +/******************************************************************************* +** +** Function BTA_DmBleSetKeyMaterial +** +** Description Set the Encrypted Data Key Material in GAP service +** +** Parameters: session_key - 16-byte session key +** iv - 8-byte initialization vector +** +** Returns void +** +*******************************************************************************/ +extern void BTA_DmBleSetKeyMaterial(const uint8_t *session_key, const uint8_t *iv); +#endif + /******************************************************************************* ** ** Function BTA_DmBleEnableRemotePrivacy diff --git a/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c b/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c index 97ca013390..dd2e51c535 100644 --- a/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c +++ b/components/bt/host/bluedroid/btc/profile/std/gap/btc_gap_ble.c @@ -3185,6 +3185,11 @@ void btc_gap_ble_call_handler(btc_msg_t *msg) BTA_DmBleGapCsProcEnable(arg_5->cs_procedure_enable_params.conn_handle, arg_5->cs_procedure_enable_params.config_id, arg_5->cs_procedure_enable_params.enable); break; #endif // (BT_BLE_FEAT_CHANNEL_SOUNDING == TRUE) +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + case BTC_GAP_BLE_ACT_SET_KEY_MATERIAL: + BTA_DmBleSetKeyMaterial(arg->set_key_material.session_key, arg->set_key_material.iv); + break; +#endif default: break; } diff --git a/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h b/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h index 8f844a50b8..9784dbf126 100644 --- a/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h +++ b/components/bt/host/bluedroid/btc/profile/std/include/btc_gap_ble.h @@ -160,6 +160,9 @@ typedef enum { BTC_GAP_BLE_CS_SET_PROCEDURE_PARAMS, BTC_GAP_BLE_CS_PROCEDURE_ENABLE, #endif // (BT_BLE_FEAT_CHANNEL_SOUNDING == TRUE) +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + BTC_GAP_BLE_ACT_SET_KEY_MATERIAL, +#endif } btc_gap_ble_act_t; /* btc_ble_gap_args_t */ @@ -215,6 +218,13 @@ typedef union { struct cfg_local_icon_args { uint16_t icon; } cfg_local_icon; +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + //BTC_GAP_BLE_ACT_SET_KEY_MATERIAL + struct set_key_material_args { + uint8_t session_key[16]; + uint8_t iv[8]; + } set_key_material; +#endif //BTC_GAP_BLE_ACT_UPDATE_WHITE_LIST struct update_white_list_args { bool add_remove; diff --git a/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h b/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h index 9f6edfe1f1..d82ec6b547 100644 --- a/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h +++ b/components/bt/host/bluedroid/common/include/common/bluedroid_user_config.h @@ -616,6 +616,12 @@ #define UC_BT_GATTS_SECURITY_LEVELS_CHAR FALSE #endif +#ifdef CONFIG_BT_GATTS_KEY_MATERIAL_CHAR +#define UC_BT_GATTS_KEY_MATERIAL_CHAR CONFIG_BT_GATTS_KEY_MATERIAL_CHAR +#else +#define UC_BT_GATTS_KEY_MATERIAL_CHAR FALSE +#endif + #ifdef CONFIG_BT_BLE_ACT_SCAN_REP_ADV_SCAN #define UC_BT_BLE_ACT_SCAN_REP_ADV_SCAN CONFIG_BT_BLE_ACT_SCAN_REP_ADV_SCAN #else diff --git a/components/bt/host/bluedroid/common/include/common/bt_target.h b/components/bt/host/bluedroid/common/include/common/bt_target.h index 4665f9f52b..cca8943447 100644 --- a/components/bt/host/bluedroid/common/include/common/bt_target.h +++ b/components/bt/host/bluedroid/common/include/common/bt_target.h @@ -834,6 +834,12 @@ #define BT_GATTS_SECURITY_LEVELS_CHAR FALSE #endif +#if (UC_BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +#define BT_GATTS_KEY_MATERIAL_CHAR TRUE +#else +#define BT_GATTS_KEY_MATERIAL_CHAR FALSE +#endif + #ifdef UC_BT_BLE_ACT_SCAN_REP_ADV_SCAN #define BTM_BLE_ACTIVE_SCAN_REPORT_ADV_SCAN_RSP_INDIVIDUALLY UC_BT_BLE_ACT_SCAN_REP_ADV_SCAN #endif diff --git a/components/bt/host/bluedroid/stack/btm/btm_ble_gap.c b/components/bt/host/bluedroid/stack/btm/btm_ble_gap.c index 61580de047..ad382d49b4 100644 --- a/components/bt/host/bluedroid/stack/btm/btm_ble_gap.c +++ b/components/bt/host/bluedroid/stack/btm/btm_ble_gap.c @@ -1164,6 +1164,39 @@ void BTM_BleConfigLocalIcon(uint16_t icon) #endif } +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +/******************************************************************************* +** +** Function BTM_BleSetKeyMaterial +** +** Description Set the Encrypted Data Key Material in GAP service +** +** Parameters session_key: 16-byte session key (must not be NULL) +** iv: 8-byte initialization vector (must not be NULL) +** +** Returns void +** +*******************************************************************************/ +void BTM_BleSetKeyMaterial(const uint8_t *session_key, const uint8_t *iv) +{ +#if (defined(GAP_INCLUDED) && GAP_INCLUDED == TRUE && GATTS_INCLUDED == TRUE) + tGAP_BLE_ATTR_VALUE p_value; + + if (session_key == NULL || iv == NULL) { + BTM_TRACE_ERROR("%s: NULL pointer parameter", __func__); + return; + } + + memset(&p_value, 0, sizeof(tGAP_BLE_ATTR_VALUE)); + memcpy(p_value.key_material.session_key, session_key, GAP_KEY_MATERIAL_SESSION_KEY_SIZE); + memcpy(p_value.key_material.iv, iv, GAP_KEY_MATERIAL_IV_SIZE); + GAP_BleAttrDBUpdate(GATT_UUID_GAP_KEY_MATERIAL, &p_value); +#else + BTM_TRACE_ERROR("%s\n", __func__); +#endif +} +#endif + /******************************************************************************* ** ** Function BTM_BleConfigConnParams diff --git a/components/bt/host/bluedroid/stack/gap/gap_ble.c b/components/bt/host/bluedroid/stack/gap/gap_ble.c index 7e4f453278..154fcca0b0 100644 --- a/components/bt/host/bluedroid/stack/gap/gap_ble.c +++ b/components/bt/host/bluedroid/stack/gap/gap_ble.c @@ -262,6 +262,13 @@ tGATT_STATUS gap_read_attr_value (UINT16 handle, tGATT_VALUE *p_value, BOOLEAN i p_value->len = 2; break; #endif // (BT_GATTS_SECURITY_LEVELS_CHAR == TRUE) +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + case GATT_UUID_GAP_KEY_MATERIAL: + ARRAY_TO_STREAM(p, p_db_attr->attr_value.key_material.session_key, GAP_KEY_MATERIAL_SESSION_KEY_SIZE); + ARRAY_TO_STREAM(p, p_db_attr->attr_value.key_material.iv, GAP_KEY_MATERIAL_IV_SIZE); + p_value->len = GAP_KEY_MATERIAL_SIZE; + break; +#endif // (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) } return GATT_SUCCESS; } @@ -481,6 +488,20 @@ void gap_attr_db_init(void) p_db_attr++; #endif // (BT_GATTS_SECURITY_LEVELS_CHAR == TRUE) +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + /* Add Encrypted Data Key Material Characteristic + * Per Bluetooth spec: readable only when authenticated and authorized, + * requires encrypted link to read. + */ + uuid.len = LEN_UUID_16; + uuid.uu.uuid16 = p_db_attr->uuid = GATT_UUID_GAP_KEY_MATERIAL; + p_db_attr->handle = GATTS_AddCharacteristic(service_handle, &uuid, + GATT_PERM_READ_ENCRYPTED, GATT_CHAR_PROP_BIT_READ, + NULL, NULL); + memset(&p_db_attr->attr_value.key_material, 0, sizeof(tGAP_BLE_KEY_MATERIAL)); + p_db_attr++; +#endif // (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + /* start service now */ memset (&app_uuid.uu.uuid128, 0x81, LEN_UUID_128); @@ -512,6 +533,11 @@ void GAP_BleAttrDBUpdate(UINT16 attr_uuid, tGAP_BLE_ATTR_VALUE *p_value) GAP_TRACE_EVENT("GAP_BleAttrDBUpdate attr_uuid=0x%04x\n", attr_uuid); + if (p_value == NULL) { + GAP_TRACE_ERROR("GAP_BleAttrDBUpdate: NULL pointer parameter"); + return; + } + for (i = 0; i < GAP_MAX_CHAR_NUM; i ++, p_db_attr ++) { if (p_db_attr->uuid == attr_uuid) { GAP_TRACE_EVENT("Found attr_uuid=0x%04x\n", attr_uuid); @@ -540,6 +566,13 @@ void GAP_BleAttrDBUpdate(UINT16 attr_uuid, tGAP_BLE_ATTR_VALUE *p_value) break; #endif // (BT_GATTS_SECURITY_LEVELS_CHAR == TRUE) +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + case GATT_UUID_GAP_KEY_MATERIAL: + memcpy(&p_db_attr->attr_value.key_material, &p_value->key_material, + sizeof(tGAP_BLE_KEY_MATERIAL)); + break; +#endif // (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + } break; } diff --git a/components/bt/host/bluedroid/stack/gap/include/gap_int.h b/components/bt/host/bluedroid/stack/gap/include/gap_int.h index 175a27e695..8f5472589c 100644 --- a/components/bt/host/bluedroid/stack/gap/include/gap_int.h +++ b/components/bt/host/bluedroid/stack/gap/include/gap_int.h @@ -93,7 +93,11 @@ typedef struct { #if BLE_INCLUDED == TRUE +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +#define GAP_MAX_CHAR_NUM 6 +#else #define GAP_MAX_CHAR_NUM 5 +#endif typedef struct { UINT16 handle; diff --git a/components/bt/host/bluedroid/stack/include/stack/btm_ble_api.h b/components/bt/host/bluedroid/stack/include/stack/btm_ble_api.h index 8961197865..33522b7aef 100644 --- a/components/bt/host/bluedroid/stack/include/stack/btm_ble_api.h +++ b/components/bt/host/bluedroid/stack/include/stack/btm_ble_api.h @@ -2948,6 +2948,22 @@ BOOLEAN BTM_BleConfigPrivacy(BOOLEAN enable, tBTM_SET_LOCAL_PRIVACY_CBACK *set_l *******************************************************************************/ void BTM_BleConfigLocalIcon(uint16_t icon); +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +/******************************************************************************* +** +** Function BTM_BleSetKeyMaterial +** +** Description Set the Encrypted Data Key Material in GAP service +** +** Parameters session_key: 16-byte session key +** iv: 8-byte initialization vector +** +** Returns void +** +*******************************************************************************/ +void BTM_BleSetKeyMaterial(const uint8_t *session_key, const uint8_t *iv); +#endif + /******************************************************************************* ** ** Function BTM_BleConfigConnParams diff --git a/components/bt/host/bluedroid/stack/include/stack/gap_api.h b/components/bt/host/bluedroid/stack/include/stack/gap_api.h index a79d9748c8..81d353d022 100644 --- a/components/bt/host/bluedroid/stack/include/stack/gap_api.h +++ b/components/bt/host/bluedroid/stack/include/stack/gap_api.h @@ -113,6 +113,17 @@ typedef struct { UINT16 sp_tout; } tGAP_BLE_PREF_PARAM; +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) +#define GAP_KEY_MATERIAL_SESSION_KEY_SIZE 16 /* 128-bit session key */ +#define GAP_KEY_MATERIAL_IV_SIZE 8 /* 64-bit IV */ +#define GAP_KEY_MATERIAL_SIZE (GAP_KEY_MATERIAL_SESSION_KEY_SIZE + GAP_KEY_MATERIAL_IV_SIZE) + +typedef struct { + UINT8 session_key[GAP_KEY_MATERIAL_SESSION_KEY_SIZE]; + UINT8 iv[GAP_KEY_MATERIAL_IV_SIZE]; +} tGAP_BLE_KEY_MATERIAL; +#endif // (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + typedef union { tGAP_BLE_PREF_PARAM conn_param; BD_ADDR reconn_bda; @@ -122,6 +133,9 @@ typedef union { #if (BT_GATTS_SECURITY_LEVELS_CHAR == TRUE) UINT16 security_level; #endif // (BT_GATTS_SECURITY_LEVELS_CHAR == TRUE) +#if (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) + tGAP_BLE_KEY_MATERIAL key_material; +#endif // (BT_GATTS_KEY_MATERIAL_CHAR == TRUE) } tGAP_BLE_ATTR_VALUE; diff --git a/components/bt/host/bluedroid/stack/include/stack/gattdefs.h b/components/bt/host/bluedroid/stack/include/stack/gattdefs.h index 2ae8cef839..8da14f4cb7 100644 --- a/components/bt/host/bluedroid/stack/include/stack/gattdefs.h +++ b/components/bt/host/bluedroid/stack/include/stack/gattdefs.h @@ -53,6 +53,7 @@ #define GATT_UUID_GAP_CENTRAL_ADDR_RESOL 0x2AA6 #define GATT_UUID_GAP_GATT_SECURITY_LEVELS 0x2BF5 +#define GATT_UUID_GAP_KEY_MATERIAL 0x2B88 /* Encrypted Data Key Material */ /* Attribute Profile Attribute UUID */ #define GATT_UUID_GATT_SRV_CHGD 0x2A05 diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/CMakeLists.txt b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/CMakeLists.txt new file mode 100644 index 0000000000..6daf7bc1f0 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/CMakeLists.txt @@ -0,0 +1,8 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(enc_adv_data_cent) diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/README.md b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/README.md new file mode 100644 index 0000000000..7649b1e653 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/README.md @@ -0,0 +1,273 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | + +# BLE Encrypted Advertising Data Central Example (Bluedroid) + +This example demonstrates how to receive and decrypt BLE Encrypted Advertising Data (EAD) with Bluedroid stack. + +## Overview + +This central example works with the `enc_adv_data_prph` peripheral example to demonstrate: + +1. Scanning for devices that advertise encrypted data +2. Connecting to read the Key Material characteristic +3. Decrypting advertising data using the obtained key + +## Two Operation Modes + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Mode Selection │ +├────────────────────────────────┬────────────────────────────────────────────┤ +│ Mode 1: With Connection │ Mode 2: No Connection │ +│ (Default) │ (Pre-shared Key) │ +├────────────────────────────────┼────────────────────────────────────────────┤ +│ │ │ +│ First scan: │ All scans: │ +│ ┌─────────┐ ┌─────────┐ │ ┌─────────┐ ┌─────────┐ │ +│ │ Central │══▶│ Periph │ │ │ Central │──▶│ Periph │ │ +│ └─────────┘ └─────────┘ │ └─────────┘ └─────────┘ │ +│ │ │ │ │ │ +│ │ Connect │ │ │ Scan only │ +│ │ Read Key │ │ ▼ │ +│ │ Disconnect │ │ Use pre-configured │ +│ ▼ │ │ key to decrypt │ +│ Store key │ │ │ │ +│ │ │ │ ▼ │ +│ ▼ │ │ ✅ Decrypt immediately │ +│ Later scans: │ │ │ +│ ┌─────────┐ ┌─────────┐ │ │ +│ │ Central │──▶│ Periph │ │ │ +│ └─────────┘ └─────────┘ │ │ +│ │ │ │ +│ │ No connection needed │ │ +│ ▼ │ │ +│ ✅ Decrypt using stored key │ │ +│ │ │ +├────────────────────────────────┼────────────────────────────────────────────┤ +│ ✓ Secure key exchange │ ✓ No connection latency │ +│ ✓ Dynamic key support │ ✓ Simpler implementation │ +│ ✗ First-time connection needed │ ✗ Key must be pre-provisioned │ +└────────────────────────────────┴────────────────────────────────────────────┘ +``` + +## System Flow Diagram + +### Mode 1: With Connection (Default) + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Central Flow - With Connection Mode │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────┐ ┌─────────────┐ │ +│ │ Central │ │ Peripheral │ │ +│ └────┬────┘ └──────┬──────┘ │ +│ │ │ │ +│ │ ════════════════ First Encounter ════════════════ │ │ +│ │ │ │ +│ │ 1. Scan │ │ +│ │ ──────────────────────────────────────────────────▶ │ │ +│ │ │ │ +│ │ 2. Receive Adv (UUID=0x2C01, Encrypted Data) │ │ +│ │ ◀────────────────────────────────────────────────── │ │ +│ │ │ │ +│ │ [No key yet - cannot decrypt] │ │ +│ │ │ │ +│ │ 3. Connect │ │ +│ │ ═══════════════════════════════════════════════════▶│ │ +│ │ │ │ +│ │ 4. Establish Encrypted Link (Pairing) │ │ +│ │ ◀═══════════════════════════════════════════════════│ │ +│ │ │ │ +│ │ 5. Read Key Material Characteristic (0x2B88) │ │ +│ │ ═══════════════════════════════════════════════════▶│ │ +│ │ │ │ +│ │ 6. Response: [Session Key (16B)] [IV (8B)] │ │ +│ │ ◀═══════════════════════════════════════════════════│ │ +│ │ │ │ +│ │ 7. Store key in memory │ │ +│ │ 8. Disconnect │ │ +│ │ ═══════════════════════════════════════════════════▶│ │ +│ │ │ │ +│ │ ════════════════ Later Scans ════════════════════ │ │ +│ │ │ │ +│ │ 9. Scan │ │ +│ │ ──────────────────────────────────────────────────▶ │ │ +│ │ │ │ +│ │ 10. Receive Encrypted Adv Data │ │ +│ │ ◀────────────────────────────────────────────────── │ │ +│ │ │ │ +│ │ 11. Decrypt using stored key (NO CONNECTION!) │ │ +│ │ ┌────────────────────────────────────────┐ │ │ +│ │ │ ble_ead_decrypt(session_key, iv, ...) │ │ │ +│ │ │ Result: "prph" (decrypted name) │ │ │ +│ │ └────────────────────────────────────────┘ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Mode 2: No Connection (Pre-shared Key) + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Central Flow - No Connection Mode │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Pre-configured Key (same as Peripheral) │ │ +│ │ Session Key: 19 6a 0a d1 2a 61 20 1e │ │ +│ │ 13 6e 2e d1 12 da a9 57 │ │ +│ │ IV: 9E 7a 00 ef b1 7a e7 46 │ │ +│ └─────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────┐ ┌─────────────┐ │ +│ │ Central │ │ Peripheral │ │ +│ └────┬────┘ └──────┬──────┘ │ +│ │ │ │ +│ │ 1. Scan (passive) │ │ +│ │ ──────────────────────────────────────────────────▶ │ │ +│ │ │ │ +│ │ 2. Receive Encrypted Adv Data │ │ +│ │ ◀────────────────────────────────────────────────── │ │ +│ │ │ │ +│ │ 3. Immediately decrypt (NO CONNECTION!) │ │ +│ │ ┌────────────────────────────────────────┐ │ │ +│ │ │ ble_ead_decrypt(pre_shared_key, ...) │ │ │ +│ │ │ Result: "prph" (decrypted name) │ │ │ +│ │ └────────────────────────────────────────┘ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ │ +│ ⚡ No connection overhead - instant decryption! │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## Decryption Process + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Decryption Process │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Received Encrypted Advertising Data: │ +│ ┌───────────────────┬─────────────────────┬─────────────────┐ │ +│ │ Randomizer │ Ciphertext │ MIC │ │ +│ │ (5 bytes) │ (6 bytes) │ (4 bytes) │ │ +│ └─────────┬─────────┴──────────┬──────────┴────────┬────────┘ │ +│ │ │ │ │ +│ ▼ │ │ │ +│ 1. Extract Randomizer │ │ │ +│ ┌─────────────────────┐ │ │ │ +│ │ XX XX XX XX [D|XX] │ │ │ │ +│ └─────────────────────┘ │ │ │ +│ │ │ │ │ +│ ▼ │ │ │ +│ 2. Build Nonce │ │ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Randomizer (5B) + IV from Key Material (8B) = Nonce (13B) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ 3. AES-CCM Decrypt + Verify │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Input: Ciphertext, MIC, Nonce, Session Key, AAD (0xEA) │ │ +│ │ Algorithm: AES-CCM-128 Authenticated Decryption │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 4. Output: Decrypted Plaintext │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ [05] [09] [70 72 70 68] │ │ +│ │ Len Type 'p' 'r' 'p' 'h' │ │ +│ │ │ │ +│ │ → Complete Local Name: "prph" │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## How to Use Example + +### Hardware Required + +* Two development boards with ESP32/ESP32-C2/ESP32-C3/ESP32-C5/ESP32-C6/ESP32-C61/ESP32-H2/ESP32-S3 SoC +* USB cables for power supply and programming + +### Setup + +1. Flash `enc_adv_data_prph` on one board (peripheral) +2. Flash `enc_adv_data_cent` on another board (central) + +### Configure the project + +```bash +idf.py set-target +idf.py menuconfig +``` + +In menuconfig, navigate to: +- `Example Configuration` → `Central Mode` + - `With Connection` - Connect to read key (default) + - `No Connection` - Use pre-shared key + +### Build and Flash + +```bash +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type `Ctrl-]`.) + +### Example Output + +**Mode 1 - With Connection (first scan):** +``` +I (XXX) ENC_ADV_CENT: Encrypted Advertising Data Central started +I (XXX) ENC_ADV_CENT: Scanning started +I (XXX) ENC_ADV_CENT: Found target device: xx:xx:xx:xx:xx:xx +I (XXX) ENC_ADV_CENT: Connecting to get key material... +I (XXX) ENC_ADV_CENT: Connected, conn_id 0 +I (XXX) ENC_ADV_CENT: Authentication success +I (XXX) ENC_ADV_CENT: Key material received: +I (XXX) ENC_ADV_CENT: 19 6a 0a d1 2a 61 20 1e 13 6e 2e d1 12 da a9 57 9e 7a 00 ef b1 7a e7 46 +I (XXX) ENC_ADV_CENT: Disconnected +``` + +**Mode 1 - With Connection (subsequent scans):** +``` +I (XXX) ENC_ADV_CENT: Found target device: xx:xx:xx:xx:xx:xx +I (XXX) ENC_ADV_CENT: Have key material, decrypting... +I (XXX) ENC_ADV_CENT: Decryption successful! +I (XXX) ENC_ADV_CENT: Decrypted data: +I (XXX) ENC_ADV_CENT: 05 09 70 72 70 68 +I (XXX) ENC_ADV_CENT: Decrypted device name: prph +``` + +**Mode 2 - No Connection:** +``` +I (XXX) ENC_ADV_CENT_SIMPLE: ======================================== +I (XXX) ENC_ADV_CENT_SIMPLE: EAD Central - No Connection Mode +I (XXX) ENC_ADV_CENT_SIMPLE: ======================================== +I (XXX) ENC_ADV_CENT_SIMPLE: ⚡ This example decrypts WITHOUT connecting! +I (XXX) ENC_ADV_CENT_SIMPLE: 🔍 Scanning started (no connection mode) +... +I (XXX) ENC_ADV_CENT_SIMPLE: ✅ Decryption successful (no connection needed!) +I (XXX) ENC_ADV_CENT_SIMPLE: 📛 Decrypted device name: "prph" +``` + +## Troubleshooting + +### Authentication Failed +- Ensure both devices have matching security parameters +- Try erasing NVS on both devices: `idf.py erase_flash` + +### Decryption Failed +- Verify the peripheral and central use matching session key and IV +- Check that the encrypted advertising data format is correct + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/CMakeLists.txt b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/CMakeLists.txt new file mode 100644 index 0000000000..6bf438be39 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/CMakeLists.txt @@ -0,0 +1,8 @@ +if(CONFIG_EXAMPLE_MODE_NO_CONNECTION) + set(MAIN_SRC "enc_adv_data_cent_no_connect.c") +else() + set(MAIN_SRC "enc_adv_data_cent.c") +endif() + +idf_component_register(SRCS ${MAIN_SRC} "ble_ead.c" + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/Kconfig.projbuild b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/Kconfig.projbuild new file mode 100644 index 0000000000..a4c00b02ea --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/Kconfig.projbuild @@ -0,0 +1,22 @@ +menu "Example Configuration" + + choice EXAMPLE_MODE + prompt "Central Mode" + default EXAMPLE_MODE_WITH_CONNECTION + help + Select the central operation mode. + + config EXAMPLE_MODE_WITH_CONNECTION + bool "With Connection (read key from peripheral)" + help + Connect to peripheral to read Key Material characteristic. + This is the standard way when key is not pre-shared. + + config EXAMPLE_MODE_NO_CONNECTION + bool "No Connection (use pre-shared key)" + help + Use pre-configured key to decrypt without connecting. + Key must match the peripheral's key. + endchoice + +endmenu diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.c b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.c new file mode 100644 index 0000000000..7303c027b2 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.c @@ -0,0 +1,426 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "ble_ead.h" +#include "esp_random.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#define TAG "BLE_EAD" + +/* Select crypto library based on configuration */ +#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT) +#include "tinycrypt/aes.h" +#include "tinycrypt/ccm_mode.h" +#include "tinycrypt/constants.h" +#elif defined(CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS) +#include "psa/crypto.h" +#else +#error "Please select either CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT or CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS" +#endif + +/* Additional Authenticated Data for EAD - EA (Encrypted Advertising) */ +static const uint8_t ble_ead_aad[BLE_EAD_AAD_SIZE] = { 0xEA }; + +/** + * @brief Generate randomizer with direction bit set + * + * Per Bluetooth Core Spec Supplement v11, Part A 1.23.3: + * The MSB of the Randomizer shall be set to indicate direction + */ +static int ble_ead_generate_randomizer(uint8_t randomizer[BLE_EAD_RANDOMIZER_SIZE]) +{ + /* Generate random bytes */ + esp_fill_random(randomizer, BLE_EAD_RANDOMIZER_SIZE); + + /* Set direction bit (MSB of last byte) - required by spec */ + randomizer[BLE_EAD_RANDOMIZER_SIZE - 1] |= (1 << BLE_EAD_RANDOMIZER_DIRECTION_BIT); + + return 0; +} + +/** + * @brief Generate nonce from IV and randomizer + * + * Nonce = Randomizer (5 bytes) || IV (8 bytes) = 13 bytes + */ +static int ble_ead_generate_nonce(const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t randomizer[BLE_EAD_RANDOMIZER_SIZE], + uint8_t nonce[BLE_EAD_NONCE_SIZE]) +{ + if (iv == NULL || nonce == NULL) { + return -1; + } + + /* Randomizer in first 5 bytes */ + if (randomizer != NULL) { + memcpy(nonce, randomizer, BLE_EAD_RANDOMIZER_SIZE); + } else { + /* Generate new randomizer with direction bit */ + ble_ead_generate_randomizer(nonce); + } + + /* IV in last 8 bytes */ + memcpy(nonce + BLE_EAD_RANDOMIZER_SIZE, iv, BLE_EAD_IV_SIZE); + + return 0; +} + +/** + * @brief AES-CCM encryption using selected crypto library + */ +static int ble_aes_ccm_encrypt(const uint8_t *key, const uint8_t *nonce, + const uint8_t *plaintext, size_t plaintext_len, + const uint8_t *aad, size_t aad_len, + uint8_t *ciphertext, size_t tag_len) +{ +#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT) + struct tc_aes_key_sched_struct sched; + struct tc_ccm_mode_struct ccm_state; + int ret; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Set AES encryption key */ + ret = tc_aes128_set_encrypt_key(&sched, key); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_aes128_set_encrypt_key failed"); + memset(&sched, 0, sizeof(sched)); + return -1; + } + + /* Configure CCM mode */ + ccm_state.sched = &sched; + ccm_state.nonce = (uint8_t *)nonce; + ccm_state.mlen = tag_len; + + ret = tc_ccm_config(&ccm_state, &sched, (uint8_t *)nonce, BLE_EAD_NONCE_SIZE, tag_len); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_config failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Encrypt and generate tag */ + /* TinyCrypt outputs: ciphertext || tag */ + ret = tc_ccm_generation_encryption(ciphertext, plaintext_len + tag_len, + aad, aad_len, + plaintext, plaintext_len, + &ccm_state); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_generation_encryption failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Clear sensitive data from key schedule */ + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return 0; + +#elif defined(CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS) + psa_status_t status; + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t key_id = 0; + psa_algorithm_t alg = PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, tag_len); + size_t output_length = 0; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Set key attributes */ + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT); + psa_set_key_algorithm(&attributes, alg); + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, BLE_EAD_KEY_SIZE * 8); + + /* Import key */ + status = psa_import_key(&attributes, key, BLE_EAD_KEY_SIZE, &key_id); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_import_key failed: %d", status); + psa_reset_key_attributes(&attributes); + return -1; + } + psa_reset_key_attributes(&attributes); + + /* Encrypt and authenticate */ + /* PSA AEAD encrypt outputs: ciphertext || tag */ + status = psa_aead_encrypt(key_id, alg, + nonce, BLE_EAD_NONCE_SIZE, + aad, aad_len, + plaintext, plaintext_len, + ciphertext, plaintext_len + tag_len, + &output_length); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_aead_encrypt failed: %d", status); + psa_destroy_key(key_id); + return -1; + } + + if (output_length != plaintext_len + tag_len) { + ESP_LOGE(TAG, "psa_aead_encrypt output length mismatch: expected %zu, got %zu", + plaintext_len + tag_len, output_length); + psa_destroy_key(key_id); + return -1; + } + + psa_destroy_key(key_id); + return 0; +#else + #error "No crypto library selected" +#endif +} + +/** + * @brief AES-CCM decryption with authentication using selected crypto library + */ +static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce, + const uint8_t *ciphertext, size_t ciphertext_len, + const uint8_t *aad, size_t aad_len, + uint8_t *plaintext, size_t tag_len) +{ +#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT) + struct tc_aes_key_sched_struct sched; + struct tc_ccm_mode_struct ccm_state; + int ret; + /* ciphertext_len here includes both ciphertext and tag */ + size_t plaintext_len; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL || plaintext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Check for integer underflow */ + if (ciphertext_len < tag_len) { + ESP_LOGE(TAG, "ciphertext_len (%zu) < tag_len (%zu)", ciphertext_len, tag_len); + return -1; + } + + plaintext_len = ciphertext_len - tag_len; + + /* Set AES encryption key */ + ret = tc_aes128_set_encrypt_key(&sched, key); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_aes128_set_encrypt_key failed"); + memset(&sched, 0, sizeof(sched)); + return -1; + } + + /* Configure CCM mode */ + ccm_state.sched = &sched; + ccm_state.nonce = (uint8_t *)nonce; + ccm_state.mlen = tag_len; + + ret = tc_ccm_config(&ccm_state, &sched, (uint8_t *)nonce, BLE_EAD_NONCE_SIZE, tag_len); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_config failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Decrypt and verify tag */ + /* TinyCrypt expects: ciphertext || tag */ + ret = tc_ccm_decryption_verification(plaintext, plaintext_len, + aad, aad_len, + (uint8_t *)ciphertext, ciphertext_len, + &ccm_state); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_decryption_verification failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Clear sensitive data from key schedule */ + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return 0; + +#elif defined(CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS) + psa_status_t status; + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t key_id = 0; + psa_algorithm_t alg = PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, tag_len); + size_t output_length = 0; + /* ciphertext_len here includes both ciphertext and tag */ + size_t plaintext_len; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL || plaintext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Check for integer underflow */ + if (ciphertext_len < tag_len) { + ESP_LOGE(TAG, "ciphertext_len (%zu) < tag_len (%zu)", ciphertext_len, tag_len); + return -1; + } + + plaintext_len = ciphertext_len - tag_len; + + /* Set key attributes */ + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, alg); + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, BLE_EAD_KEY_SIZE * 8); + + /* Import key */ + status = psa_import_key(&attributes, key, BLE_EAD_KEY_SIZE, &key_id); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_import_key failed: %d", status); + psa_reset_key_attributes(&attributes); + return -1; + } + psa_reset_key_attributes(&attributes); + + /* Decrypt and verify */ + /* PSA AEAD decrypt expects: ciphertext || tag */ + /* ciphertext_len here already includes tag length */ + status = psa_aead_decrypt(key_id, alg, + nonce, BLE_EAD_NONCE_SIZE, + aad, aad_len, + ciphertext, ciphertext_len, + plaintext, plaintext_len, + &output_length); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_aead_decrypt failed: %d", status); + psa_destroy_key(key_id); + return -1; + } + + if (output_length != plaintext_len) { + ESP_LOGE(TAG, "psa_aead_decrypt output length mismatch: expected %zu, got %zu", + plaintext_len, output_length); + psa_destroy_key(key_id); + return -1; + } + + psa_destroy_key(key_id); + return 0; +#else + #error "No crypto library selected" +#endif +} + +int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *payload, size_t payload_size, + uint8_t *encrypted_payload) +{ + int ret; + uint8_t nonce[BLE_EAD_NONCE_SIZE]; + + if (session_key == NULL) { + ESP_LOGE(TAG, "session_key is NULL"); + return -1; + } + + if (iv == NULL) { + ESP_LOGE(TAG, "iv is NULL"); + return -1; + } + + if (payload == NULL && payload_size > 0) { + ESP_LOGE(TAG, "payload is NULL but payload_size > 0"); + return -1; + } + + if (encrypted_payload == NULL) { + ESP_LOGE(TAG, "encrypted_payload is NULL"); + return -1; + } + + /* Generate nonce with random randomizer */ + ret = ble_ead_generate_nonce(iv, NULL, nonce); + if (ret != 0) { + return ret; + } + + /* Copy randomizer to the start of encrypted payload */ + memcpy(encrypted_payload, nonce, BLE_EAD_RANDOMIZER_SIZE); + + /* Encrypt: output = ciphertext + MIC */ + ret = ble_aes_ccm_encrypt(session_key, nonce, + payload, payload_size, + ble_ead_aad, BLE_EAD_AAD_SIZE, + &encrypted_payload[BLE_EAD_RANDOMIZER_SIZE], + BLE_EAD_MIC_SIZE); + + return ret; +} + +int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *encrypted_payload, size_t encrypted_payload_size, + uint8_t *payload) +{ + int ret; + uint8_t nonce[BLE_EAD_NONCE_SIZE]; + const uint8_t *randomizer; + const uint8_t *ciphertext; + size_t ciphertext_len; + + if (session_key == NULL) { + ESP_LOGE(TAG, "session_key is NULL"); + return -1; + } + + if (iv == NULL) { + ESP_LOGE(TAG, "iv is NULL"); + return -1; + } + + if (encrypted_payload == NULL) { + ESP_LOGE(TAG, "encrypted_payload is NULL"); + return -1; + } + + if (payload == NULL) { + ESP_LOGE(TAG, "payload is NULL"); + return -1; + } + + if (encrypted_payload_size < BLE_EAD_RANDOMIZER_SIZE + BLE_EAD_MIC_SIZE) { + ESP_LOGE(TAG, "encrypted_payload_size too small"); + return -1; + } + + /* Extract randomizer from the start of encrypted payload */ + randomizer = encrypted_payload; + + /* Ciphertext + MIC follows the randomizer */ + ciphertext = &encrypted_payload[BLE_EAD_RANDOMIZER_SIZE]; + /* ciphertext_len includes both ciphertext and MIC (tag) for PSA API */ + ciphertext_len = encrypted_payload_size - BLE_EAD_RANDOMIZER_SIZE; + + /* Generate nonce from randomizer and IV */ + ret = ble_ead_generate_nonce(iv, randomizer, nonce); + if (ret != 0) { + return ret; + } + + /* Decrypt and verify */ + ret = ble_aes_ccm_decrypt(session_key, nonce, + ciphertext, ciphertext_len, + ble_ead_aad, BLE_EAD_AAD_SIZE, + payload, BLE_EAD_MIC_SIZE); + + return ret; +} diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.h b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.h new file mode 100644 index 0000000000..a9bf8954eb --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/ble_ead.h @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_EAD_H +#define BLE_EAD_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief BLE Encrypted Advertising Data (EAD) definitions + * Based on Bluetooth Core Specification Version 5.4 + */ + +#define BLE_EAD_KEY_SIZE 16 /* 128-bit session key */ +#define BLE_EAD_IV_SIZE 8 /* 64-bit Initialization Vector */ +#define BLE_EAD_RANDOMIZER_SIZE 5 /* 40-bit Randomizer */ +#define BLE_EAD_MIC_SIZE 4 /* 32-bit Message Integrity Check */ +#define BLE_EAD_NONCE_SIZE 13 /* 104-bit Nonce (Randomizer + IV) */ +#define BLE_EAD_AAD_SIZE 1 /* Additional Authenticated Data size */ + +/* Direction bit position in Randomizer (MSB of last byte) + * Per Bluetooth Core Spec Supplement v11, Part A 1.23.3 + */ +#define BLE_EAD_RANDOMIZER_DIRECTION_BIT 7 + +/* AD Type for Encrypted Advertising Data (0x31) */ +#define ESP_BLE_AD_TYPE_ENC_ADV_DATA 0x31 + +/** + * @brief Calculate encrypted payload size from plaintext size + */ +#define BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(payload_size) \ + (BLE_EAD_RANDOMIZER_SIZE + (payload_size) + BLE_EAD_MIC_SIZE) + +/** + * @brief Calculate decrypted payload size from encrypted payload size + */ +#define BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_size) \ + ((encrypted_size) - BLE_EAD_RANDOMIZER_SIZE - BLE_EAD_MIC_SIZE) + +/** + * @brief Key material structure for EAD + */ +typedef struct { + uint8_t session_key[BLE_EAD_KEY_SIZE]; /* 128-bit session key */ + uint8_t iv[BLE_EAD_IV_SIZE]; /* 64-bit Initialization Vector */ +} ble_ead_key_material_t; + +/** + * @brief Encrypt advertising data using AES-CCM + * + * @param session_key 16-byte session key + * @param iv 8-byte Initialization Vector + * @param payload Plaintext advertising data to encrypt + * @param payload_size Size of plaintext data + * @param encrypted_payload Output buffer for encrypted data + * Size must be at least BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(payload_size) + * + * @return 0 on success, negative error code on failure + */ +int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *payload, size_t payload_size, + uint8_t *encrypted_payload); + +/** + * @brief Decrypt advertising data using AES-CCM + * + * @param session_key 16-byte session key + * @param iv 8-byte Initialization Vector + * @param encrypted_payload Encrypted advertising data (includes randomizer and MIC) + * @param encrypted_payload_size Size of encrypted data + * @param payload Output buffer for decrypted data + * Size must be at least BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size) + * + * @return 0 on success, negative error code on failure + */ +int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *encrypted_payload, size_t encrypted_payload_size, + uint8_t *payload); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_EAD_H */ diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent.c b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent.c new file mode 100644 index 0000000000..e03c3b86ad --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent.c @@ -0,0 +1,519 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/** + * @brief BLE Encrypted Advertising Data Central Example + * + * This example demonstrates how to: + * 1. Scan for devices broadcasting encrypted advertising data + * 2. Connect to read Key Material characteristic + * 3. Decrypt the advertising data using the obtained key + * + * Based on Bluetooth Core Specification Version 5.4 - Encrypted Advertising Data + */ + +#include +#include +#include +#include +#include "nvs.h" +#include "nvs_flash.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_gattc_api.h" +#include "esp_gatt_defs.h" +#include "esp_bt_main.h" +#include "esp_gatt_common_api.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "ble_ead.h" + +#define TAG "ENC_ADV_CENT" + +/* Service and characteristic UUIDs */ +#define GAP_SERVICE_UUID 0x1800 /* GAP Service UUID */ +#define KEY_MATERIAL_CHAR_UUID 0x2B88 /* Key Material Characteristic UUID */ + +/* Profile configuration */ +#define PROFILE_NUM 1 +#define PROFILE_APP_ID 0 +#define INVALID_HANDLE 0 + +/* Maximum peers to track */ +#define MAX_PEERS 5 + +/* Peer information structure */ +typedef struct { + bool valid; + esp_bd_addr_t addr; + bool key_material_exist; + ble_ead_key_material_t key_material; +} peer_info_t; + +static peer_info_t peers[MAX_PEERS] = {0}; + +/* GATT client state */ +static bool is_connected = false; +static bool get_server = false; +static uint16_t conn_id_stored = 0; +static uint16_t service_start_handle = 0; +static uint16_t service_end_handle = 0; +static uint16_t key_material_char_handle = INVALID_HANDLE; +static esp_bd_addr_t current_peer_addr = {0}; + +/* GATT interface */ +static esp_gatt_if_t gattc_if_stored = ESP_GATT_IF_NONE; + +/* Scan parameters */ +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x50, + .scan_window = 0x30, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, +}; + +/* Forward declarations */ +static void start_scan(void); + +/** + * @brief Find peer by address + */ +static int find_peer(const esp_bd_addr_t addr) +{ + for (int i = 0; i < MAX_PEERS; i++) { + if (peers[i].valid && memcmp(peers[i].addr, addr, sizeof(esp_bd_addr_t)) == 0) { + return i; + } + } + return -1; +} + +/** + * @brief Add or update peer + */ +static int add_peer(const esp_bd_addr_t addr) +{ + int idx = find_peer(addr); + if (idx >= 0) { + return idx; /* Already exists */ + } + + /* Find empty slot */ + for (int i = 0; i < MAX_PEERS; i++) { + if (!peers[i].valid) { + peers[i].valid = true; + memcpy(peers[i].addr, addr, sizeof(esp_bd_addr_t)); + peers[i].key_material_exist = false; + return i; + } + } + return -1; /* No space */ +} + +/** + * @brief Decrypt encrypted advertising data + */ +static void decrypt_enc_adv_data(const uint8_t *adv_data, uint8_t adv_len, const esp_bd_addr_t addr) +{ + int peer_idx = find_peer(addr); + if (peer_idx < 0 || !peers[peer_idx].key_material_exist) { + ESP_LOGW(TAG, "No key material for peer, cannot decrypt"); + return; + } + + uint8_t offset = 0; + while (offset < adv_len) { + uint8_t len = adv_data[offset]; + if (len == 0 || offset + len >= adv_len) { + break; + } + + uint8_t type = adv_data[offset + 1]; + if (type == ESP_BLE_AD_TYPE_ENC_ADV_DATA) { + /* Found encrypted advertising data */ + const uint8_t *enc_data = &adv_data[offset + 2]; + uint8_t enc_data_len = len - 1; /* Exclude type byte */ + + if (enc_data_len < BLE_EAD_RANDOMIZER_SIZE + BLE_EAD_MIC_SIZE) { + ESP_LOGW(TAG, "Encrypted data too short"); + break; + } + + uint8_t dec_data[32]; /* Buffer for decrypted data */ + size_t dec_len = BLE_EAD_DECRYPTED_PAYLOAD_SIZE(enc_data_len); + + int rc = ble_ead_decrypt( + peers[peer_idx].key_material.session_key, + peers[peer_idx].key_material.iv, + enc_data, enc_data_len, + dec_data); + + if (rc == 0) { + ESP_LOGI(TAG, "Decryption successful!"); + ESP_LOGI(TAG, "Decrypted data:"); + ESP_LOG_BUFFER_HEX(TAG, dec_data, dec_len); + + /* Parse decrypted advertising structure */ + if (dec_len >= 2) { + uint8_t dec_type = dec_data[1]; + if (dec_type == ESP_BLE_AD_TYPE_NAME_CMPL || dec_type == ESP_BLE_AD_TYPE_NAME_SHORT) { + char name[32] = {0}; + size_t name_len = dec_data[0] - 1; + if (name_len < sizeof(name)) { + memcpy(name, &dec_data[2], name_len); + ESP_LOGI(TAG, "Decrypted device name: %s", name); + } + } + } + } else { + ESP_LOGE(TAG, "Decryption failed: %d", rc); + } + break; + } + offset += len + 1; + } +} + +/** + * @brief Check if device advertises GAP service UUID + */ +static bool should_connect(const uint8_t *adv_data, uint8_t adv_len) +{ + uint8_t offset = 0; + while (offset < adv_len) { + uint8_t len = adv_data[offset]; + if (len == 0 || offset + len >= adv_len) { + break; + } + + uint8_t type = adv_data[offset + 1]; + if (type == ESP_BLE_AD_TYPE_16SRV_CMPL || type == ESP_BLE_AD_TYPE_16SRV_PART) { + /* Check for GAP service UUID */ + for (int i = 0; i < len - 1; i += 2) { + uint16_t uuid = adv_data[offset + 2 + i] | (adv_data[offset + 3 + i] << 8); + if (uuid == GAP_SERVICE_UUID) { + return true; + } + } + } + offset += len + 1; + } + return false; +} + +/** + * @brief Start scanning + */ +static void start_scan(void) +{ + esp_ble_gap_start_scanning(30); /* Scan for 30 seconds */ +} + +/** + * @brief GAP event handler + */ +static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + ESP_LOGI(TAG, "Scan parameters set"); + start_scan(); + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "Scan start failed: %d", param->scan_start_cmpl.status); + } else { + ESP_LOGI(TAG, "Scanning started"); + } + break; + + case ESP_GAP_BLE_SCAN_RESULT_EVT: { + esp_ble_gap_cb_param_t *scan_result = param; + if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { + uint8_t *adv_data = scan_result->scan_rst.ble_adv; + uint8_t adv_len = scan_result->scan_rst.adv_data_len; + + if (should_connect(adv_data, adv_len)) { + ESP_LOGI(TAG, "Found target device: "ESP_BD_ADDR_STR"", + ESP_BD_ADDR_HEX(scan_result->scan_rst.bda)); + + int peer_idx = find_peer(scan_result->scan_rst.bda); + if (peer_idx >= 0 && peers[peer_idx].key_material_exist) { + /* Already have key, try to decrypt */ + ESP_LOGI(TAG, "Have key material, decrypting..."); + decrypt_enc_adv_data(adv_data, adv_len, scan_result->scan_rst.bda); + } else { + /* Need to connect and get key */ + if (!is_connected) { + ESP_LOGI(TAG, "Connecting to get key material..."); + add_peer(scan_result->scan_rst.bda); + memcpy(current_peer_addr, scan_result->scan_rst.bda, sizeof(esp_bd_addr_t)); + + esp_ble_gap_stop_scanning(); + + esp_ble_gatt_creat_conn_params_t conn_params = {0}; + memcpy(conn_params.remote_bda, scan_result->scan_rst.bda, ESP_BD_ADDR_LEN); + conn_params.remote_addr_type = scan_result->scan_rst.ble_addr_type; + conn_params.own_addr_type = BLE_ADDR_TYPE_PUBLIC; + conn_params.is_direct = true; + conn_params.is_aux = false; + esp_ble_gattc_enh_open(gattc_if_stored, &conn_params); + } + } + } + } else if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { + ESP_LOGI(TAG, "Scan complete"); + if (!is_connected) { + start_scan(); /* Restart scanning */ + } + } + break; + } + + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + ESP_LOGI(TAG, "Scan stopped"); + break; + + case ESP_GAP_BLE_SEC_REQ_EVT: + ESP_LOGI(TAG, "Security request"); + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_AUTH_CMPL_EVT: + if (param->ble_security.auth_cmpl.success) { + ESP_LOGI(TAG, "Authentication success"); + } else { + ESP_LOGW(TAG, "Authentication failed: 0x%x", param->ble_security.auth_cmpl.fail_reason); + } + break; + + default: + break; + } +} + +/** + * @brief GATTC event handler + */ +static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) +{ + switch (event) { + case ESP_GATTC_REG_EVT: + ESP_LOGI(TAG, "GATT client registered, status %d, if %d", param->reg.status, gattc_if); + gattc_if_stored = gattc_if; + esp_ble_gap_set_scan_params(&ble_scan_params); + break; + + case ESP_GATTC_CONNECT_EVT: + ESP_LOGI(TAG, "Connected, conn_id %d", param->connect.conn_id); + conn_id_stored = param->connect.conn_id; + is_connected = true; + + /* Request MTU exchange */ + esp_ble_gattc_send_mtu_req(gattc_if, param->connect.conn_id); + break; + + case ESP_GATTC_OPEN_EVT: + if (param->open.status != ESP_GATT_OK) { + ESP_LOGE(TAG, "Open failed: %d", param->open.status); + is_connected = false; + start_scan(); + } + break; + + case ESP_GATTC_CFG_MTU_EVT: + ESP_LOGI(TAG, "MTU configured: %d", param->cfg_mtu.mtu); + break; + + case ESP_GATTC_DIS_SRVC_CMPL_EVT: + ESP_LOGI(TAG, "Service discovery complete"); + /* Search for GAP service that contains Key Material characteristic */ + ESP_LOGI(TAG, "Searching for GAP service UUID 0x%04X", GAP_SERVICE_UUID); + esp_bt_uuid_t gap_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = GAP_SERVICE_UUID}, + }; + esp_ble_gattc_search_service(gattc_if, param->dis_srvc_cmpl.conn_id, &gap_uuid); + break; + + case ESP_GATTC_SEARCH_RES_EVT: + ESP_LOGI(TAG, "Service found, UUID 0x%04X, start_handle %d, end_handle %d", + param->search_res.srvc_id.uuid.uuid.uuid16, + param->search_res.start_handle, param->search_res.end_handle); + if (param->search_res.srvc_id.uuid.len == ESP_UUID_LEN_16 && + param->search_res.srvc_id.uuid.uuid.uuid16 == GAP_SERVICE_UUID) { + get_server = true; + service_start_handle = param->search_res.start_handle; + service_end_handle = param->search_res.end_handle; + } + break; + + case ESP_GATTC_SEARCH_CMPL_EVT: + ESP_LOGI(TAG, "Service search complete"); + if (get_server) { + /* Get characteristics */ + uint16_t count = 0; + esp_ble_gattc_get_attr_count(gattc_if, conn_id_stored, + ESP_GATT_DB_CHARACTERISTIC, + service_start_handle, + service_end_handle, + INVALID_HANDLE, &count); + + if (count > 0) { + esp_gattc_char_elem_t *char_elem = malloc(sizeof(esp_gattc_char_elem_t) * count); + if (char_elem) { + esp_bt_uuid_t km_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = KEY_MATERIAL_CHAR_UUID}, + }; + esp_ble_gattc_get_char_by_uuid(gattc_if, conn_id_stored, + service_start_handle, + service_end_handle, + km_uuid, char_elem, &count); + + if (count > 0) { + key_material_char_handle = char_elem[0].char_handle; + ESP_LOGI(TAG, "Key Material characteristic found, handle %d", key_material_char_handle); + + /* Read characteristic with encryption requirement + * GATT layer will automatically trigger encryption if needed */ + ESP_LOGI(TAG, "Reading key material (will trigger encryption if needed)..."); + esp_ble_gattc_read_char(gattc_if, conn_id_stored, + key_material_char_handle, ESP_GATT_AUTH_REQ_NO_MITM); + } + free(char_elem); + } + } + } + break; + + case ESP_GATTC_READ_CHAR_EVT: + if (param->read.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "Read characteristic success, handle %d, len %d", + param->read.handle, param->read.value_len); + + if (param->read.handle == key_material_char_handle && + param->read.value_len == sizeof(ble_ead_key_material_t)) { + /* Store key material */ + int peer_idx = find_peer(current_peer_addr); + if (peer_idx >= 0) { + memcpy(&peers[peer_idx].key_material, param->read.value, + sizeof(ble_ead_key_material_t)); + peers[peer_idx].key_material_exist = true; + + ESP_LOGI(TAG, "Key material received:"); + ESP_LOG_BUFFER_HEX(TAG, &peers[peer_idx].key_material, + sizeof(ble_ead_key_material_t)); + } + + /* Disconnect and resume scanning */ + esp_ble_gattc_close(gattc_if, conn_id_stored); + } + } else { + ESP_LOGE(TAG, "Read failed: %d", param->read.status); + } + break; + + case ESP_GATTC_DISCONNECT_EVT: + ESP_LOGI(TAG, "Disconnected, reason 0x%02x", param->disconnect.reason); + is_connected = false; + get_server = false; + key_material_char_handle = INVALID_HANDLE; + start_scan(); + break; + + default: + break; + } +} + + +void app_main(void) +{ + esp_err_t ret; + + /* Initialize NVS */ + ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + /* Release memory for Classic BT */ + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); + + /* Initialize BT controller */ + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret) { + ESP_LOGE(TAG, "initialize controller failed: %s", esp_err_to_name(ret)); + return; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret) { + ESP_LOGE(TAG, "enable controller failed: %s", esp_err_to_name(ret)); + return; + } + + /* Initialize Bluedroid */ + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ret = esp_bluedroid_init_with_cfg(&cfg); + if (ret) { + ESP_LOGE(TAG, "init bluetooth failed: %s", esp_err_to_name(ret)); + return; + } + + ret = esp_bluedroid_enable(); + if (ret) { + ESP_LOGE(TAG, "enable bluetooth failed: %s", esp_err_to_name(ret)); + return; + } + + /* Register callbacks */ + ret = esp_ble_gap_register_callback(gap_event_handler); + if (ret) { + ESP_LOGE(TAG, "gap register error: %x", ret); + return; + } + + ret = esp_ble_gattc_register_callback(gattc_event_handler); + if (ret) { + ESP_LOGE(TAG, "gattc register error: %x", ret); + return; + } + + ret = esp_ble_gattc_app_register(PROFILE_APP_ID); + if (ret) { + ESP_LOGE(TAG, "gattc app register error: %x", ret); + return; + } + + /* Set MTU */ + esp_ble_gatt_set_local_mtu(500); + + /* Configure security parameters + * Using SC (Secure Connections) with bonding, no MITM (since IO_CAP is NONE) + */ + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_BOND; /* SC + Bond, no MITM */ + esp_ble_io_cap_t io_cap = ESP_IO_CAP_NONE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(auth_req)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &io_cap, sizeof(io_cap)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(key_size)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(init_key)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(rsp_key)); + + ESP_LOGI(TAG, "Encrypted Advertising Data Central started"); +} diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent_no_connect.c b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent_no_connect.c new file mode 100644 index 0000000000..62416b1ecc --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/main/enc_adv_data_cent_no_connect.c @@ -0,0 +1,239 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/** + * @brief BLE Encrypted Advertising Data Central Example - No Connection Version + * + * This simplified example demonstrates decrypting advertising data WITHOUT connecting. + * The key material is pre-configured (same as peripheral). + * + * Use case: When the key is pre-shared or provisioned out-of-band. + */ + +#include +#include +#include +#include +#include "nvs.h" +#include "nvs_flash.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "ble_ead.h" + +#define TAG "ENC_ADV_CENT_SIMPLE" + +/* Custom service UUID to identify target device */ +#define CUSTOM_SERVICE_UUID 0x2C01 + +/* + * Pre-shared Key Material - MUST match the Peripheral! + * In real applications, this would be provisioned securely. + */ +static const ble_ead_key_material_t pre_shared_key = { + .session_key = { + 0x19, 0x6a, 0x0a, 0xd1, 0x2a, 0x61, 0x20, 0x1e, + 0x13, 0x6e, 0x2e, 0xd1, 0x12, 0xda, 0xa9, 0x57 + }, + .iv = {0x9E, 0x7a, 0x00, 0xef, 0xb1, 0x7a, 0xe7, 0x46}, +}; + +/* Scan parameters */ +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, /* Passive scan is enough */ + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x50, + .scan_window = 0x30, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, +}; + +/** + * @brief Check if device advertises our target service UUID + */ +static bool is_target_device(const uint8_t *adv_data, uint8_t adv_len) +{ + uint8_t offset = 0; + while (offset < adv_len) { + uint8_t len = adv_data[offset]; + if (len == 0 || offset + len >= adv_len) { + break; + } + + uint8_t type = adv_data[offset + 1]; + if (type == ESP_BLE_AD_TYPE_16SRV_CMPL || type == ESP_BLE_AD_TYPE_16SRV_PART) { + for (int i = 0; i < len - 1; i += 2) { + uint16_t uuid = adv_data[offset + 2 + i] | (adv_data[offset + 3 + i] << 8); + if (uuid == CUSTOM_SERVICE_UUID) { + return true; + } + } + } + offset += len + 1; + } + return false; +} + +/** + * @brief Decrypt encrypted advertising data using pre-shared key + * + * No connection required! + */ +static void decrypt_adv_data_no_connect(const uint8_t *adv_data, uint8_t adv_len, + const esp_bd_addr_t addr) +{ + uint8_t offset = 0; + + ESP_LOGI(TAG, "Processing advertising data from "ESP_BD_ADDR_STR"", + ESP_BD_ADDR_HEX(addr)); + + while (offset < adv_len) { + uint8_t len = adv_data[offset]; + if (len == 0 || offset + len >= adv_len) { + break; + } + + uint8_t type = adv_data[offset + 1]; + + /* Look for Encrypted Advertising Data (AD Type 0x31) */ + if (type == ESP_BLE_AD_TYPE_ENC_ADV_DATA) { + const uint8_t *enc_data = &adv_data[offset + 2]; + uint8_t enc_data_len = len - 1; /* Exclude type byte */ + + ESP_LOGI(TAG, "Found encrypted advertising data (%d bytes)", enc_data_len); + ESP_LOG_BUFFER_HEX(TAG, enc_data, enc_data_len); + + if (enc_data_len < BLE_EAD_RANDOMIZER_SIZE + BLE_EAD_MIC_SIZE) { + ESP_LOGW(TAG, "Encrypted data too short"); + break; + } + + /* Decrypt using pre-shared key */ + uint8_t dec_data[32]; + size_t dec_len = BLE_EAD_DECRYPTED_PAYLOAD_SIZE(enc_data_len); + + int rc = ble_ead_decrypt( + pre_shared_key.session_key, + pre_shared_key.iv, + enc_data, enc_data_len, + dec_data); + + if (rc == 0) { + ESP_LOGI(TAG, "✅ Decryption successful (no connection needed!)"); + ESP_LOGI(TAG, "Decrypted data (%d bytes):", dec_len); + ESP_LOG_BUFFER_HEX(TAG, dec_data, dec_len); + + /* Parse the decrypted advertising structure */ + if (dec_len >= 2) { + uint8_t inner_len = dec_data[0]; + uint8_t inner_type = dec_data[1]; + + if (inner_type == ESP_BLE_AD_TYPE_NAME_CMPL || + inner_type == ESP_BLE_AD_TYPE_NAME_SHORT) { + char name[32] = {0}; + size_t name_len = inner_len - 1; + if (name_len < sizeof(name) && name_len <= dec_len - 2) { + memcpy(name, &dec_data[2], name_len); + ESP_LOGI(TAG, "📛 Decrypted device name: \"%s\"", name); + } + } + } + } else { + ESP_LOGE(TAG, "❌ Decryption failed (rc=%d) - wrong key?", rc); + } + return; /* Found and processed encrypted data */ + } + offset += len + 1; + } + + ESP_LOGW(TAG, "No encrypted advertising data found in this packet"); +} + +/** + * @brief GAP event handler + */ +static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + ESP_LOGI(TAG, "Scan parameters set, starting scan..."); + esp_ble_gap_start_scanning(0); /* Scan indefinitely */ + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status == ESP_BT_STATUS_SUCCESS) { + ESP_LOGI(TAG, "🔍 Scanning started (no connection mode)"); + ESP_LOGI(TAG, "Looking for devices with UUID 0x%04X...", CUSTOM_SERVICE_UUID); + } else { + ESP_LOGE(TAG, "Scan start failed: %d", param->scan_start_cmpl.status); + } + break; + + case ESP_GAP_BLE_SCAN_RESULT_EVT: { + esp_ble_gap_cb_param_t *scan_result = param; + + if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { + uint8_t *adv_data = scan_result->scan_rst.ble_adv; + uint8_t adv_len = scan_result->scan_rst.adv_data_len; + + /* Check if this is our target device */ + if (is_target_device(adv_data, adv_len)) { + /* Decrypt without connecting! */ + decrypt_adv_data_no_connect(adv_data, adv_len, scan_result->scan_rst.bda); + } + } + break; + } + + default: + break; + } +} + +void app_main(void) +{ + esp_err_t ret; + + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " EAD Central - No Connection Mode"); + ESP_LOGI(TAG, "========================================"); + + /* Initialize NVS */ + ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); + + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); + ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); + + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_bluedroid_init_with_cfg(&cfg)); + ESP_ERROR_CHECK(esp_bluedroid_enable()); + + ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); + + /* Display pre-shared key */ + ESP_LOGI(TAG, "Using pre-shared key material:"); + ESP_LOGI(TAG, " Session Key:"); + ESP_LOG_BUFFER_HEX(TAG, pre_shared_key.session_key, BLE_EAD_KEY_SIZE); + ESP_LOGI(TAG, " IV:"); + ESP_LOG_BUFFER_HEX(TAG, pre_shared_key.iv, BLE_EAD_IV_SIZE); + + /* Start scanning */ + ESP_ERROR_CHECK(esp_ble_gap_set_scan_params(&ble_scan_params)); + + ESP_LOGI(TAG, ""); + ESP_LOGI(TAG, "⚡ This example decrypts WITHOUT connecting!"); + ESP_LOGI(TAG, " Key must be pre-shared with peripheral."); +} diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/sdkconfig.defaults b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/sdkconfig.defaults new file mode 100644 index 0000000000..ae0ee30898 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_cent/sdkconfig.defaults @@ -0,0 +1,13 @@ +# Enable BLE +CONFIG_BT_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set + +# Enable SMP for security +CONFIG_BT_BLE_SMP_ENABLE=y + +# Select crypto library for EAD (Encrypted Advertising Data) +# Options: CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT or CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS +CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/CMakeLists.txt b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/CMakeLists.txt new file mode 100644 index 0000000000..b78df5cd06 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/CMakeLists.txt @@ -0,0 +1,8 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(enc_adv_data_prph) diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/README.md b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/README.md new file mode 100644 index 0000000000..95e86da46b --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/README.md @@ -0,0 +1,172 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | + +# BLE Encrypted Advertising Data Peripheral Example (Bluedroid) + +This example demonstrates how to use BLE Encrypted Advertising Data (EAD) feature with Bluedroid stack. + +## Overview + +The Encrypted Advertising Data feature (introduced in Bluetooth Core Specification 5.4) allows devices to encrypt portions of their advertising data using AES-CCM. This enables: + +- Privacy protection for sensitive advertising data +- Selective disclosure of advertising data to authorized devices +- Enhanced security for BLE advertising + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ PERIPHERAL (This Example) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │ +│ │ Original Data │───▶│ AES-CCM Encrypt │───▶│ Encrypted Adv Data │ │ +│ │ "prph" (name) │ │ (Session Key+IV)│ │ (Randomizer+Cipher │ │ +│ └─────────────────┘ └──────────────────┘ │ +MIC) │ │ +│ └──────────┬──────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ BLE Advertising Packet │ │ +│ ├──────────┬─────────────┬────────────────┬────────────────────────────┤ │ +│ │ Flags │ Name "key" │ UUID 0x2C01 │ Encrypted Data (AD 0x31) │ │ +│ │ (3B) │ (5B) │ (4B) │ (16B) │ │ +│ └──────────┴─────────────┴────────────────┴────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ GATT Server │ │ +│ ├──────────────────────────────────────────────────────────────────────┤ │ +│ │ GAP Service (0x1800) │ │ +│ │ └── Key Material Characteristic (0x2B88) │ │ +│ │ └── Value: [Session Key (16B)] [IV (8B)] │ │ +│ │ └── Permission: Read (Encrypted Link Required) │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## Encryption Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Encryption Process │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Generate Random Randomizer (5 bytes) │ +│ ┌─────────────────────────────────────────┐ │ +│ │ XX XX XX XX [D|XX] │ D = Direction Bit = 1 │ +│ └─────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 2. Build Nonce (13 bytes) │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Randomizer (5B) │ IV (8B) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ 3. AES-CCM Encryption │ +│ ┌─────────────────┐ │ +│ │ Plaintext │ + Session Key + Nonce + AAD (0xEA) │ +│ │ [05 09 p r p h]│ │ +│ └─────────────────┘ │ +│ │ │ +│ ▼ │ +│ 4. Output: Encrypted Payload │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Randomizer (5B) │ Ciphertext (6B) │ MIC (4B) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## Advertising Data Format + +``` +Complete Advertising Packet (29 bytes): + +Offset Length Type Data Description +────── ────── ──── ──── ─────────── +0 2 0x01 0x06 Flags: LE General Discoverable +3 4 0x09 'k' 'e' 'y' Complete Local Name +8 3 0x03 0x01 0x2C 16-bit Service UUID: 0x2C01 +12 16 0x31 [Encrypted Payload] Encrypted Advertising Data + +Encrypted Payload Detail: +┌───────────────────┬─────────────────────┬─────────────────┐ +│ Randomizer │ Ciphertext │ MIC │ +│ (5 bytes) │ (6 bytes) │ (4 bytes) │ +│ Random + Dir=1 │ AES-CCM output │ Auth Tag │ +└───────────────────┴─────────────────────┴─────────────────┘ +``` + +## How to Use Example + +### Hardware Required + +* A development board with ESP32/ESP32-C2/ESP32-C3/ESP32-C5/ESP32-C6/ESP32-C61/ESP32-H2/ESP32-S3 SoC +* A USB cable for power supply and programming + +### Configure the project + +```bash +idf.py set-target +idf.py menuconfig +``` + +### Build and Flash + +```bash +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type `Ctrl-]`.) + +### Example Output + +``` +I (XXX) ENC_ADV_PRPH: Encrypted Advertising Data Peripheral started +I (XXX) ENC_ADV_PRPH: Key Material (Session Key + IV): +I (XXX) ENC_ADV_PRPH: 19 6a 0a d1 2a 61 20 1e 13 6e 2e d1 12 da a9 57 9e 7a 00 ef b1 7a e7 46 +I (XXX) ENC_ADV_PRPH: Data before encryption: +I (XXX) ENC_ADV_PRPH: 05 09 70 72 70 68 +I (XXX) ENC_ADV_PRPH: Encryption of adv data done successfully +I (XXX) ENC_ADV_PRPH: Raw advertising data set complete +I (XXX) ENC_ADV_PRPH: Advertising start successfully +``` + +## Testing with Central + +Use the `enc_adv_data_cent` example as the central device: + +``` +┌──────────────────┐ ┌──────────────────┐ +│ PERIPHERAL │ │ CENTRAL │ +│ (This Example) │ │ (enc_adv_data_ │ +│ │ │ cent) │ +└────────┬─────────┘ └────────┬─────────┘ + │ │ + │ 1. Broadcast Encrypted Adv │ + │ ─────────────────────────────────────▶│ + │ │ + │ 2. Connect (first time only) │ + │ ◀═══════════════════════════════════ │ + │ │ + │ 3. Read Key Material (0x2B88) │ + │ ═══════════════════════════════════▶ │ + │ │ + │ 4. Return Session Key + IV │ + │ ◀═══════════════════════════════════ │ + │ │ + │ 5. Disconnect │ + │ ◀═══════════════════════════════════ │ + │ │ + │ 6. Future: Decrypt without connect │ + │ ─────────────────────────────────────▶│ + │ │ + ▼ ▼ +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/CMakeLists.txt b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/CMakeLists.txt new file mode 100644 index 0000000000..a9fa58c00e --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "enc_adv_data_prph.c" "ble_ead.c" + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/Kconfig.projbuild b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/Kconfig.projbuild new file mode 100644 index 0000000000..305d0bfab6 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/Kconfig.projbuild @@ -0,0 +1,14 @@ +menu "Example Configuration" + + config EXAMPLE_ENABLE_KEY_MATERIAL + bool "Enable Key Material characteristic in GAP Service" + default y + select BT_GATTS_KEY_MATERIAL_CHAR + help + Enable the Key Material characteristic in the built-in GAP service + (UUID 0x1800) using the Bluedroid stack's support for this feature. + + This is the standard-compliant approach as defined in Bluetooth + Core Specification Version 5.4. + +endmenu diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.c b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.c new file mode 100644 index 0000000000..b3e1dcc8f8 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.c @@ -0,0 +1,419 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "ble_ead.h" +#include "esp_random.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#define TAG "BLE_EAD" + +/* Select crypto library based on configuration */ +#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT) +#include "tinycrypt/aes.h" +#include "tinycrypt/ccm_mode.h" +#include "tinycrypt/constants.h" +#elif defined(CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS) +#include "psa/crypto.h" +#else +#error "Please select either CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT or CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS" +#endif + +/* Additional Authenticated Data for EAD - EA (Encrypted Advertising) */ +static const uint8_t ble_ead_aad[BLE_EAD_AAD_SIZE] = { 0xEA }; + +/** + * @brief Generate randomizer with direction bit set + * + * Per Bluetooth Core Spec Supplement v11, Part A 1.23.3: + * The MSB of the Randomizer shall be set to indicate direction + */ +static int ble_ead_generate_randomizer(uint8_t randomizer[BLE_EAD_RANDOMIZER_SIZE]) +{ + /* Generate random bytes */ + esp_fill_random(randomizer, BLE_EAD_RANDOMIZER_SIZE); + + /* Set direction bit (MSB of last byte) - required by spec */ + randomizer[BLE_EAD_RANDOMIZER_SIZE - 1] |= (1 << BLE_EAD_RANDOMIZER_DIRECTION_BIT); + + return 0; +} + +/** + * @brief Generate nonce from IV and randomizer + * + * Nonce = Randomizer (5 bytes) || IV (8 bytes) = 13 bytes + */ +static int ble_ead_generate_nonce(const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t randomizer[BLE_EAD_RANDOMIZER_SIZE], + uint8_t nonce[BLE_EAD_NONCE_SIZE]) +{ + if (iv == NULL || nonce == NULL) { + return -1; + } + + /* Randomizer in first 5 bytes */ + if (randomizer != NULL) { + memcpy(nonce, randomizer, BLE_EAD_RANDOMIZER_SIZE); + } else { + /* Generate new randomizer with direction bit */ + ble_ead_generate_randomizer(nonce); + } + + /* IV in last 8 bytes */ + memcpy(nonce + BLE_EAD_RANDOMIZER_SIZE, iv, BLE_EAD_IV_SIZE); + + return 0; +} + +/** + * @brief AES-CCM encryption using selected crypto library + */ +static int ble_aes_ccm_encrypt(const uint8_t *key, const uint8_t *nonce, + const uint8_t *plaintext, size_t plaintext_len, + const uint8_t *aad, size_t aad_len, + uint8_t *ciphertext, size_t tag_len) +{ +#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT) + struct tc_aes_key_sched_struct sched; + struct tc_ccm_mode_struct ccm_state; + int ret; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Set AES encryption key */ + ret = tc_aes128_set_encrypt_key(&sched, key); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_aes128_set_encrypt_key failed"); + memset(&sched, 0, sizeof(sched)); + return -1; + } + + /* Configure CCM mode */ + ccm_state.sched = &sched; + ccm_state.nonce = (uint8_t *)nonce; + ccm_state.mlen = tag_len; + + ret = tc_ccm_config(&ccm_state, &sched, (uint8_t *)nonce, BLE_EAD_NONCE_SIZE, tag_len); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_config failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Encrypt and generate tag */ + /* TinyCrypt outputs: ciphertext || tag */ + ret = tc_ccm_generation_encryption(ciphertext, plaintext_len + tag_len, + aad, aad_len, + plaintext, plaintext_len, + &ccm_state); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_generation_encryption failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Clear sensitive data from key schedule */ + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return 0; + +#elif defined(CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS) + psa_status_t status; + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t key_id = 0; + psa_algorithm_t alg = PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, tag_len); + size_t output_length = 0; + + /* Set key attributes */ + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT); + psa_set_key_algorithm(&attributes, alg); + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, BLE_EAD_KEY_SIZE * 8); + + /* Import key */ + status = psa_import_key(&attributes, key, BLE_EAD_KEY_SIZE, &key_id); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_import_key failed: %d", status); + psa_reset_key_attributes(&attributes); + return -1; + } + psa_reset_key_attributes(&attributes); + + /* Encrypt and authenticate */ + /* PSA AEAD encrypt outputs: ciphertext || tag */ + status = psa_aead_encrypt(key_id, alg, + nonce, BLE_EAD_NONCE_SIZE, + aad, aad_len, + plaintext, plaintext_len, + ciphertext, plaintext_len + tag_len, + &output_length); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_aead_encrypt failed: %d", status); + psa_destroy_key(key_id); + return -1; + } + + if (output_length != plaintext_len + tag_len) { + ESP_LOGE(TAG, "psa_aead_encrypt output length mismatch: expected %zu, got %zu", + plaintext_len + tag_len, output_length); + psa_destroy_key(key_id); + return -1; + } + + psa_destroy_key(key_id); + return 0; +#else + #error "No crypto library selected" +#endif +} + +/** + * @brief AES-CCM decryption with authentication using selected crypto library + */ +static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce, + const uint8_t *ciphertext, size_t ciphertext_len, + const uint8_t *aad, size_t aad_len, + uint8_t *plaintext, size_t tag_len) +{ +#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT) + struct tc_aes_key_sched_struct sched; + struct tc_ccm_mode_struct ccm_state; + int ret; + /* ciphertext_len here includes both ciphertext and tag */ + size_t plaintext_len; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL || plaintext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Check for integer underflow */ + if (ciphertext_len < tag_len) { + ESP_LOGE(TAG, "ciphertext_len (%zu) < tag_len (%zu)", ciphertext_len, tag_len); + return -1; + } + + plaintext_len = ciphertext_len - tag_len; + + /* Set AES encryption key */ + ret = tc_aes128_set_encrypt_key(&sched, key); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_aes128_set_encrypt_key failed"); + memset(&sched, 0, sizeof(sched)); + return -1; + } + + /* Configure CCM mode */ + ccm_state.sched = &sched; + ccm_state.nonce = (uint8_t *)nonce; + ccm_state.mlen = tag_len; + + ret = tc_ccm_config(&ccm_state, &sched, (uint8_t *)nonce, BLE_EAD_NONCE_SIZE, tag_len); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_config failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Decrypt and verify tag */ + /* TinyCrypt expects: ciphertext || tag */ + ret = tc_ccm_decryption_verification(plaintext, plaintext_len, + aad, aad_len, + (uint8_t *)ciphertext, ciphertext_len, + &ccm_state); + if (ret != TC_CRYPTO_SUCCESS) { + ESP_LOGE(TAG, "tc_ccm_decryption_verification failed"); + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return -1; + } + + /* Clear sensitive data from key schedule */ + memset(&sched, 0, sizeof(sched)); + memset(&ccm_state, 0, sizeof(ccm_state)); + return 0; + +#elif defined(CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS) + psa_status_t status; + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t key_id = 0; + psa_algorithm_t alg = PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, tag_len); + size_t output_length = 0; + /* ciphertext_len here includes both ciphertext and tag */ + size_t plaintext_len; + + /* Validate inputs */ + if (key == NULL || nonce == NULL || ciphertext == NULL || plaintext == NULL) { + ESP_LOGE(TAG, "Invalid input parameters"); + return -1; + } + + /* Check for integer underflow */ + if (ciphertext_len < tag_len) { + ESP_LOGE(TAG, "ciphertext_len (%zu) < tag_len (%zu)", ciphertext_len, tag_len); + return -1; + } + + plaintext_len = ciphertext_len - tag_len; + + /* Set key attributes */ + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, alg); + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, BLE_EAD_KEY_SIZE * 8); + + /* Import key */ + status = psa_import_key(&attributes, key, BLE_EAD_KEY_SIZE, &key_id); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_import_key failed: %d", status); + psa_reset_key_attributes(&attributes); + return -1; + } + psa_reset_key_attributes(&attributes); + + /* Decrypt and verify */ + /* PSA AEAD decrypt expects: ciphertext || tag */ + status = psa_aead_decrypt(key_id, alg, + nonce, BLE_EAD_NONCE_SIZE, + aad, aad_len, + ciphertext, ciphertext_len, + plaintext, plaintext_len, + &output_length); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "psa_aead_decrypt failed: %d", status); + psa_destroy_key(key_id); + return -1; + } + + if (output_length != plaintext_len) { + ESP_LOGE(TAG, "psa_aead_decrypt output length mismatch: expected %zu, got %zu", + plaintext_len, output_length); + psa_destroy_key(key_id); + return -1; + } + + psa_destroy_key(key_id); + return 0; +#else + #error "No crypto library selected" +#endif +} + +int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *payload, size_t payload_size, + uint8_t *encrypted_payload) +{ + int ret; + uint8_t nonce[BLE_EAD_NONCE_SIZE]; + + if (session_key == NULL) { + ESP_LOGE(TAG, "session_key is NULL"); + return -1; + } + + if (iv == NULL) { + ESP_LOGE(TAG, "iv is NULL"); + return -1; + } + + if (payload == NULL && payload_size > 0) { + ESP_LOGE(TAG, "payload is NULL but payload_size > 0"); + return -1; + } + + if (encrypted_payload == NULL) { + ESP_LOGE(TAG, "encrypted_payload is NULL"); + return -1; + } + + /* Generate nonce with random randomizer */ + ret = ble_ead_generate_nonce(iv, NULL, nonce); + if (ret != 0) { + return ret; + } + + /* Copy randomizer to the start of encrypted payload */ + memcpy(encrypted_payload, nonce, BLE_EAD_RANDOMIZER_SIZE); + + /* Encrypt: output = ciphertext + MIC */ + ret = ble_aes_ccm_encrypt(session_key, nonce, + payload, payload_size, + ble_ead_aad, BLE_EAD_AAD_SIZE, + &encrypted_payload[BLE_EAD_RANDOMIZER_SIZE], + BLE_EAD_MIC_SIZE); + + return ret; +} + +int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *encrypted_payload, size_t encrypted_payload_size, + uint8_t *payload) +{ + int ret; + uint8_t nonce[BLE_EAD_NONCE_SIZE]; + const uint8_t *randomizer; + const uint8_t *ciphertext; + size_t ciphertext_len; + + if (session_key == NULL) { + ESP_LOGE(TAG, "session_key is NULL"); + return -1; + } + + if (iv == NULL) { + ESP_LOGE(TAG, "iv is NULL"); + return -1; + } + + if (encrypted_payload == NULL) { + ESP_LOGE(TAG, "encrypted_payload is NULL"); + return -1; + } + + if (payload == NULL) { + ESP_LOGE(TAG, "payload is NULL"); + return -1; + } + + if (encrypted_payload_size < BLE_EAD_RANDOMIZER_SIZE + BLE_EAD_MIC_SIZE) { + ESP_LOGE(TAG, "encrypted_payload_size too small"); + return -1; + } + + /* Extract randomizer from the start of encrypted payload */ + randomizer = encrypted_payload; + + /* Ciphertext + MIC follows the randomizer */ + ciphertext = &encrypted_payload[BLE_EAD_RANDOMIZER_SIZE]; + /* ciphertext_len includes both ciphertext and MIC (tag) for PSA API */ + ciphertext_len = encrypted_payload_size - BLE_EAD_RANDOMIZER_SIZE; + + /* Generate nonce from randomizer and IV */ + ret = ble_ead_generate_nonce(iv, randomizer, nonce); + if (ret != 0) { + return ret; + } + + /* Decrypt and verify */ + ret = ble_aes_ccm_decrypt(session_key, nonce, + ciphertext, ciphertext_len, + ble_ead_aad, BLE_EAD_AAD_SIZE, + payload, BLE_EAD_MIC_SIZE); + + return ret; +} diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.h b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.h new file mode 100644 index 0000000000..a9bf8954eb --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/ble_ead.h @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef BLE_EAD_H +#define BLE_EAD_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief BLE Encrypted Advertising Data (EAD) definitions + * Based on Bluetooth Core Specification Version 5.4 + */ + +#define BLE_EAD_KEY_SIZE 16 /* 128-bit session key */ +#define BLE_EAD_IV_SIZE 8 /* 64-bit Initialization Vector */ +#define BLE_EAD_RANDOMIZER_SIZE 5 /* 40-bit Randomizer */ +#define BLE_EAD_MIC_SIZE 4 /* 32-bit Message Integrity Check */ +#define BLE_EAD_NONCE_SIZE 13 /* 104-bit Nonce (Randomizer + IV) */ +#define BLE_EAD_AAD_SIZE 1 /* Additional Authenticated Data size */ + +/* Direction bit position in Randomizer (MSB of last byte) + * Per Bluetooth Core Spec Supplement v11, Part A 1.23.3 + */ +#define BLE_EAD_RANDOMIZER_DIRECTION_BIT 7 + +/* AD Type for Encrypted Advertising Data (0x31) */ +#define ESP_BLE_AD_TYPE_ENC_ADV_DATA 0x31 + +/** + * @brief Calculate encrypted payload size from plaintext size + */ +#define BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(payload_size) \ + (BLE_EAD_RANDOMIZER_SIZE + (payload_size) + BLE_EAD_MIC_SIZE) + +/** + * @brief Calculate decrypted payload size from encrypted payload size + */ +#define BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_size) \ + ((encrypted_size) - BLE_EAD_RANDOMIZER_SIZE - BLE_EAD_MIC_SIZE) + +/** + * @brief Key material structure for EAD + */ +typedef struct { + uint8_t session_key[BLE_EAD_KEY_SIZE]; /* 128-bit session key */ + uint8_t iv[BLE_EAD_IV_SIZE]; /* 64-bit Initialization Vector */ +} ble_ead_key_material_t; + +/** + * @brief Encrypt advertising data using AES-CCM + * + * @param session_key 16-byte session key + * @param iv 8-byte Initialization Vector + * @param payload Plaintext advertising data to encrypt + * @param payload_size Size of plaintext data + * @param encrypted_payload Output buffer for encrypted data + * Size must be at least BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(payload_size) + * + * @return 0 on success, negative error code on failure + */ +int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *payload, size_t payload_size, + uint8_t *encrypted_payload); + +/** + * @brief Decrypt advertising data using AES-CCM + * + * @param session_key 16-byte session key + * @param iv 8-byte Initialization Vector + * @param encrypted_payload Encrypted advertising data (includes randomizer and MIC) + * @param encrypted_payload_size Size of encrypted data + * @param payload Output buffer for decrypted data + * Size must be at least BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size) + * + * @return 0 on success, negative error code on failure + */ +int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE], + const uint8_t iv[BLE_EAD_IV_SIZE], + const uint8_t *encrypted_payload, size_t encrypted_payload_size, + uint8_t *payload); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_EAD_H */ diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/enc_adv_data_prph.c b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/enc_adv_data_prph.c new file mode 100644 index 0000000000..7520bbc9d8 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/main/enc_adv_data_prph.c @@ -0,0 +1,353 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/** + * @brief BLE Encrypted Advertising Data Peripheral Example + * + * This example demonstrates how to: + * 1. Encrypt advertising data using AES-CCM + * 2. Broadcast encrypted advertising data + * 3. Provide Key Material characteristic for central devices to read + * + * Based on Bluetooth Core Specification Version 5.4 - Encrypted Advertising Data + */ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_gatts_api.h" +#include "esp_bt_defs.h" +#include "esp_bt_main.h" +#include "esp_gatt_common_api.h" +#include "ble_ead.h" + +#define TAG "ENC_ADV_PRPH" + +/* Device name */ +#define DEVICE_NAME "ENC_ADV_PRPH" +#define GAP_SERVICE_UUID 0x1800 /* GAP Service UUID */ + +/* Profile configuration */ +#define PROFILE_NUM 1 +#define PROFILE_APP_ID 0 + +/* Unencrypted advertising pattern to be encrypted */ +static uint8_t unencrypted_adv_pattern[] = { + 0x05, 0x09, 'p', 'r', 'p', 'h' /* Complete Local Name: "prph" */ +}; + +/* Session key and IV for encryption - in real application, generate securely! */ +static ble_ead_key_material_t key_material = { + .session_key = { + 0x19, 0x6a, 0x0a, 0xd1, 0x2a, 0x61, 0x20, 0x1e, + 0x13, 0x6e, 0x2e, 0xd1, 0x12, 0xda, 0xa9, 0x57 + }, + .iv = {0x9E, 0x7a, 0x00, 0xef, 0xb1, 0x7a, 0xe7, 0x46}, +}; + +/* GATT state */ +static esp_gatt_if_t gatts_if_stored = ESP_GATT_IF_NONE; +static uint16_t conn_id_stored = 0; +static bool is_connected = false; + +/* Advertising parameters */ +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +/* Calculate encrypted payload size */ +#define ENCRYPTED_ADV_DATA_LEN BLE_EAD_ENCRYPTED_PAYLOAD_SIZE(sizeof(unencrypted_adv_pattern)) + +/** + * @brief Encrypt advertising data and set raw advertising data + */ +static void set_encrypted_adv_data(void) +{ + esp_err_t ret; + uint8_t encrypted_adv_data[ENCRYPTED_ADV_DATA_LEN]; + int rc; + + ESP_LOGI(TAG, "Data before encryption:"); + ESP_LOG_BUFFER_HEX(TAG, unencrypted_adv_pattern, sizeof(unencrypted_adv_pattern)); + + /* Encrypt the advertising data */ + rc = ble_ead_encrypt(key_material.session_key, key_material.iv, + unencrypted_adv_pattern, sizeof(unencrypted_adv_pattern), + encrypted_adv_data); + if (rc != 0) { + ESP_LOGE(TAG, "Encryption of adv data failed: %d", rc); + return; + } + + ESP_LOGI(TAG, "Encryption of adv data done successfully"); + ESP_LOGI(TAG, "Data after encryption:"); + ESP_LOG_BUFFER_HEX(TAG, encrypted_adv_data, sizeof(encrypted_adv_data)); + + /* + * Build raw advertising data: + * - Flags (3 bytes) + * - Complete Local Name (device name) + * - Complete 16-bit Service UUIDs (for central to recognize) + * - Encrypted Advertising Data + */ + uint8_t raw_adv_data[31]; + uint8_t pos = 0; + + /* Flags */ + raw_adv_data[pos++] = 0x02; /* Length */ + raw_adv_data[pos++] = ESP_BLE_AD_TYPE_FLAG; + raw_adv_data[pos++] = ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT; + + /* Complete Local Name - "key" (short name for recognition) */ + raw_adv_data[pos++] = 0x04; /* Length */ + raw_adv_data[pos++] = ESP_BLE_AD_TYPE_NAME_CMPL; + raw_adv_data[pos++] = 'k'; + raw_adv_data[pos++] = 'e'; + raw_adv_data[pos++] = 'y'; + + /* Complete 16-bit Service UUIDs - GAP Service (0x1800) */ + raw_adv_data[pos++] = 0x03; /* Length */ + raw_adv_data[pos++] = ESP_BLE_AD_TYPE_16SRV_CMPL; + raw_adv_data[pos++] = GAP_SERVICE_UUID & 0xFF; + raw_adv_data[pos++] = (GAP_SERVICE_UUID >> 8) & 0xFF; + + /* Encrypted Advertising Data */ + raw_adv_data[pos++] = ENCRYPTED_ADV_DATA_LEN + 1; /* Length */ + raw_adv_data[pos++] = ESP_BLE_AD_TYPE_ENC_ADV_DATA; + memcpy(&raw_adv_data[pos], encrypted_adv_data, ENCRYPTED_ADV_DATA_LEN); + pos += ENCRYPTED_ADV_DATA_LEN; + + /* Set raw advertising data */ + ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, pos); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "config raw adv data failed: %s", esp_err_to_name(ret)); + } +} + +/** + * @brief Start advertising + */ +static void start_advertising(void) +{ + esp_err_t ret = esp_ble_gap_start_advertising(&adv_params); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "start advertising failed: %s", esp_err_to_name(ret)); + } +} + +/** + * @brief GAP event handler + */ +static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + ESP_LOGI(TAG, "Raw advertising data set complete"); + start_advertising(); + break; + + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "Advertising start failed: %d", param->adv_start_cmpl.status); + } else { + ESP_LOGI(TAG, "Advertising start successfully"); + } + break; + + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "Advertising stop failed: %d", param->adv_stop_cmpl.status); + } else { + ESP_LOGI(TAG, "Advertising stop successfully"); + } + break; + + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: + ESP_LOGI(TAG, "Connection params update, status %d, conn_int %d, latency %d, timeout %d", + param->update_conn_params.status, + param->update_conn_params.conn_int, + param->update_conn_params.latency, + param->update_conn_params.timeout); + break; + + case ESP_GAP_BLE_SEC_REQ_EVT: + ESP_LOGI(TAG, "Security request received"); + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_AUTH_CMPL_EVT: + ESP_LOGI(TAG, "Authentication complete, addr_type %d, addr "ESP_BD_ADDR_STR"", + param->ble_security.auth_cmpl.addr_type, + ESP_BD_ADDR_HEX(param->ble_security.auth_cmpl.bd_addr)); + if (param->ble_security.auth_cmpl.success) { + ESP_LOGI(TAG, "Authentication success, auth_mode %d", param->ble_security.auth_cmpl.auth_mode); + } else { + ESP_LOGW(TAG, "Authentication failed, reason 0x%x", param->ble_security.auth_cmpl.fail_reason); + } + break; + + default: + break; + } +} + +/** + * @brief GATTS profile event handler + */ +static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) +{ + switch (event) { + case ESP_GATTS_REG_EVT: + ESP_LOGI(TAG, "GATT server register, status %d, app_id %d, gatts_if %d", + param->reg.status, param->reg.app_id, gatts_if); + gatts_if_stored = gatts_if; + + /* Set device name */ + esp_ble_gap_set_device_name(DEVICE_NAME); + + /* Set encrypted advertising data */ + set_encrypted_adv_data(); + + /* Set Key Material in GAP service + * The Key Material characteristic is part of the built-in GAP service + */ + ESP_LOGI(TAG, "Setting Key Material in GAP service"); + esp_ble_gap_set_key_material(key_material.session_key, key_material.iv); + break; + + case ESP_GATTS_CONNECT_EVT: + ESP_LOGI(TAG, "Connected, conn_id %d, remote "ESP_BD_ADDR_STR"", + param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda)); + conn_id_stored = param->connect.conn_id; + is_connected = true; + + /* Update connection parameters */ + esp_ble_conn_update_params_t conn_params = {0}; + memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); + conn_params.latency = 0; + conn_params.max_int = 0x20; + conn_params.min_int = 0x10; + conn_params.timeout = 400; + esp_ble_gap_update_conn_params(&conn_params); + break; + + case ESP_GATTS_DISCONNECT_EVT: + ESP_LOGI(TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x", + ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason); + is_connected = false; + + /* Re-encrypt and restart advertising with new randomizer */ + set_encrypted_adv_data(); + break; + + case ESP_GATTS_MTU_EVT: + ESP_LOGI(TAG, "MTU exchange, MTU %d", param->mtu.mtu); + break; + + default: + break; + } +} + +void app_main(void) +{ + esp_err_t ret; + + /* Initialize NVS */ + ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + /* Release memory for Classic BT */ + ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); + + /* Initialize BT controller */ + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret) { + ESP_LOGE(TAG, "initialize controller failed: %s", esp_err_to_name(ret)); + return; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret) { + ESP_LOGE(TAG, "enable controller failed: %s", esp_err_to_name(ret)); + return; + } + + /* Initialize Bluedroid */ + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ret = esp_bluedroid_init_with_cfg(&cfg); + if (ret) { + ESP_LOGE(TAG, "init bluetooth failed: %s", esp_err_to_name(ret)); + return; + } + + ret = esp_bluedroid_enable(); + if (ret) { + ESP_LOGE(TAG, "enable bluetooth failed: %s", esp_err_to_name(ret)); + return; + } + + /* Register callbacks */ + ret = esp_ble_gatts_register_callback(gatts_event_handler); + if (ret) { + ESP_LOGE(TAG, "gatts register error: %x", ret); + return; + } + + ret = esp_ble_gap_register_callback(gap_event_handler); + if (ret) { + ESP_LOGE(TAG, "gap register error: %x", ret); + return; + } + + /* Register GATT application */ + ret = esp_ble_gatts_app_register(PROFILE_APP_ID); + if (ret) { + ESP_LOGE(TAG, "gatts app register error: %x", ret); + return; + } + + /* Set MTU */ + esp_ble_gatt_set_local_mtu(500); + + /* Configure security parameters for Key Material characteristic access + * Using SC (Secure Connections) with bonding, no MITM (since IO_CAP is NONE) + */ + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_BOND; /* SC + Bond, no MITM */ + esp_ble_io_cap_t io_cap = ESP_IO_CAP_NONE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(auth_req)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &io_cap, sizeof(io_cap)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(key_size)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(init_key)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(rsp_key)); + + ESP_LOGI(TAG, "Encrypted Advertising Data Peripheral started"); + ESP_LOGI(TAG, "Key Material (Session Key + IV):"); + ESP_LOG_BUFFER_HEX(TAG, &key_material, sizeof(key_material)); +} diff --git a/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/sdkconfig.defaults b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/sdkconfig.defaults new file mode 100644 index 0000000000..ae0ee30898 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_enc_adv_data/enc_adv_data_prph/sdkconfig.defaults @@ -0,0 +1,13 @@ +# Enable BLE +CONFIG_BT_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set + +# Enable SMP for security +CONFIG_BT_BLE_SMP_ENABLE=y + +# Select crypto library for EAD (Encrypted Advertising Data) +# Options: CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT or CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS +CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y