Bluetooth: Shell: Add support for EAD

Add new commands to manage the Encrypted Advertising Data feature.

Overview of those new commands:

- `bt encrypted-ad set-keys`: set key materials (session key and
  initialisation vector) used for encrypted and decryption of EAD;
- `bt encrypted-ad add-ad`: store a given advertising data structure;
- `bt encrypted-ad add-ead`: encrypt the given advertising data
  structres and store the generated AD structure;
- `bt encrypted-ad commit-ad`: set the AD of the selected advertiser
  with the stored AD;
- `bt encrypted-ad clear-ad`: remove all stored AD;
- `bt encrypted-ad decrypt-scan`: decrypt data using the previously set
  key materials when receiving AD with type `0x31`.

The documentation of the Bluetooth Shell has been updated to include
those new commands.

Signed-off-by: Théo Battrel <theo.battrel@nordicsemi.no>
This commit is contained in:
Théo Battrel 2024-03-07 16:04:25 +01:00 committed by Johan Hedberg
commit 4a55bc00f0
3 changed files with 426 additions and 0 deletions

View file

@ -228,6 +228,90 @@ Let's now have a look at some extended advertising features. To enable extended
This will create an extended advertiser, that is connectable and non-scannable.
Encrypted Advertising Data
==========================
Zephyr has support for the Encrypted Advertising Data feature. The :code:`bt encrypted-ad`
sub-commands allow managing the advertising data of a given advertiser.
To encrypt the advertising data, key materials need to be provided, that can be done with :code:`bt
encrypted-ad set-keys <session key> <init vector>`. The session key is 16 bytes long and the
initialisation vector is 8 bytes long.
You can add advertising data by using :code:`bt encrypted-ad add-ad` and :code:`bt encrypted-ad
add-ead`. The former will take add one advertising data structure (as defined in the Core
Specification), when the later will read the given data, encrypt them and then add the generated
encrypted advertising data structure. It's possible to mix encrypted and non-encrypted data, when
done adding advertising data, :code:`bt encrypted-ad commit-ad` can be used to apply the change to
the data to the selected advertiser. After that the advertiser can be started as described
previously. It's possible to clear the advertising data by using :code:`bt encrypted-ad clear-ad`.
On the Central side, it's possible to decrypt the received encrypted advertising data by setting the
correct keys material as described earlier and then enabling the decrypting of the data with
:code:`bt encrypted-ad decrypt-scan on`.
.. note::
To see the advertising data in the scan report :code:`bt scan-verbose-output` need to be
enabled.
.. note::
It's possible to increase the length of the advertising data by increasing the value of
:kconfig:option:`CONFIG_BT_CTLR_ADV_DATA_LEN_MAX` and
:kconfig:option:`CONFIG_BT_CTLR_SCAN_DATA_LEN_MAX`.
Here is a simple example demonstrating the usage of EAD:
.. tabs::
.. group-tab:: Peripheral
.. code-block:: console
uart:~$ bt init
...
uart:~$ bt adv-create conn-nscan ext-adv
Created adv id: 0, adv: 0x81769a0
uart:~$ bt encrypted-ad set-keys 9ba22d3824efc70feb800c80294cba38 2e83f3d4d47695b6
session key set to:
00000000: 9b a2 2d 38 24 ef c7 0f eb 80 0c 80 29 4c ba 38 |..-8$... ....)L.8|
initialisation vector set to:
00000000: 2e 83 f3 d4 d4 76 95 b6 |.....v.. |
uart:~$ bt encrypted-ad add-ad 06097368656C6C
uart:~$ bt encrypted-ad add-ead 03ffdead03ffbeef
uart:~$ bt encrypted-ad commit-ad
Advertising data for Advertiser[0] 0x81769a0 updated.
uart:~$ bt adv-start
Advertiser[0] 0x81769a0 set started
.. group-tab:: Central
.. code-block:: console
uart:~$ bt init
...
uart:~$ bt scan-verbose-output on
uart:~$ bt encrypted-ad set-keys 9ba22d3824efc70feb800c80294cba38 2e83f3d4d47695b6
session key set to:
00000000: 9b a2 2d 38 24 ef c7 0f eb 80 0c 80 29 4c ba 38 |..-8$... ....)L.8|
initialisation vector set to:
00000000: 2e 83 f3 d4 d4 76 95 b6 |.....v.. |
uart:~$ bt encrypted-ad decrypt-scan on
Received encrypted advertising data will now be decrypted using provided key materials.
uart:~$ bt scan on
Bluetooth active scan enabled
[DEVICE]: 68:49:30:68:49:30 (random), AD evt type 5, RSSI -59 shell C:1 S:0 D:0 SR:0 E:1 Prim: LE 1M, Secn: LE 2M, Interval: 0x0000 (0 us), SID: 0x0
[SCAN DATA START - EXT_ADV]
Type 0x09: shell
Type 0x31: Encrypted Advertising Data: 0xe2, 0x17, 0xed, 0x04, 0xe7, 0x02, 0x1d, 0xc9, 0x40, 0x07, uart:~0x18, 0x90, 0x6c, 0x4b, 0xfe, 0x34, 0xad
[START DECRYPTED DATA]
Type 0xff: 0xde, 0xad
Type 0xff: 0xbe, 0xef
[END DECRYPTED DATA]
[SCAN DATA END]
...
Filter Accept List
******************

View file

@ -32,6 +32,7 @@
#include <zephyr/bluetooth/classic/rfcomm.h>
#include <zephyr/bluetooth/classic/sdp.h>
#include <zephyr/bluetooth/iso.h>
#include <zephyr/bluetooth/ead.h>
#include <zephyr/shell/shell.h>
@ -216,6 +217,30 @@ static struct bt_scan_filter {
static const char scan_response_label[] = "[DEVICE]: ";
static bool scan_verbose_output;
#if defined(CONFIG_BT_EAD)
static uint8_t bt_shell_ead_session_key[BT_EAD_KEY_SIZE] = {0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5,
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB,
0xCC, 0xCD, 0xCE, 0xCF};
static uint8_t bt_shell_ead_iv[BT_EAD_IV_SIZE] = {0xFB, 0x56, 0xE1, 0xDA, 0xDC, 0x7E, 0xAD, 0xF5};
/* this is the number of ad struct allowed */
#define BT_SHELL_EAD_MAX_AD 10
static size_t bt_shell_ead_ad_len;
#if defined(CONFIG_BT_CTLR_ADV_DATA_LEN_MAX)
/* this is the maximum total size of the ad data */
#define BT_SHELL_EAD_DATA_MAX_SIZE CONFIG_BT_CTLR_ADV_DATA_LEN_MAX
#else
#define BT_SHELL_EAD_DATA_MAX_SIZE 31
#endif
static size_t bt_shell_ead_data_size;
static uint8_t bt_shell_ead_data[BT_SHELL_EAD_DATA_MAX_SIZE] = {0};
int ead_update_ad(void);
#endif
static bool bt_shell_ead_decrypt_scan;
/**
* @brief Compares two strings without case sensitivy
*
@ -383,6 +408,36 @@ static bool data_verbose_cb(struct bt_data *data, void *user_data)
case BT_DATA_CSIS_RSI:
print_data_set(3, data->data, data->data_len);
break;
case BT_DATA_ENCRYPTED_AD_DATA:
shell_fprintf(ctx_shell, SHELL_INFO, "Encrypted Advertising Data: ");
print_data_set(1, data->data, data->data_len);
if (bt_shell_ead_decrypt_scan) {
#if defined(CONFIG_BT_EAD)
shell_fprintf(ctx_shell, SHELL_INFO, "\n%*s[START DECRYPTED DATA]\n",
strlen(scan_response_label), "");
int ead_err;
struct net_buf_simple decrypted_buf;
size_t decrypted_data_size = BT_EAD_DECRYPTED_PAYLOAD_SIZE(data->data_len);
uint8_t decrypted_data[decrypted_data_size];
ead_err = bt_ead_decrypt(bt_shell_ead_session_key, bt_shell_ead_iv,
data->data, data->data_len, decrypted_data);
if (ead_err) {
shell_error(ctx_shell, "Error during decryption (err %d)", ead_err);
}
net_buf_simple_init_with_data(&decrypted_buf, &decrypted_data[0],
decrypted_data_size);
bt_data_parse(&decrypted_buf, &data_verbose_cb, user_data);
shell_fprintf(ctx_shell, SHELL_INFO, "%*s[END DECRYPTED DATA]",
strlen(scan_response_label), "");
#endif
}
break;
default:
print_data_set(1, data->data, data->data_len);
}
@ -581,6 +636,13 @@ static bool adv_rpa_expired(struct bt_le_ext_adv *adv)
adv_index, adv,
keep_rpa ? "not expired" : "expired");
#if defined(CONFIG_BT_EAD)
/* EAD must be updated each time the RPA is updated */
if (!keep_rpa) {
ead_update_ad();
}
#endif
return keep_rpa;
}
#endif /* defined(CONFIG_BT_PRIVACY) */
@ -4127,6 +4189,267 @@ static int cmd_auth_oob_tk(const struct shell *sh, size_t argc, char *argv[])
#endif /* !defined(CONFIG_BT_SMP_SC_PAIR_ONLY) */
#endif /* CONFIG_BT_SMP) || CONFIG_BT_CLASSIC */
#if defined(CONFIG_BT_EAD)
static int cmd_encrypted_ad_set_keys(const struct shell *sh, size_t argc, char *argv[])
{
size_t len;
const char *session_key = argv[1];
const char *iv = argv[2];
len = hex2bin(session_key, strlen(session_key), bt_shell_ead_session_key, BT_EAD_KEY_SIZE);
if (len != BT_EAD_KEY_SIZE) {
shell_error(sh, "Failed to set session key");
return -ENOEXEC;
}
len = hex2bin(iv, strlen(iv), bt_shell_ead_iv, BT_EAD_IV_SIZE);
if (len != BT_EAD_IV_SIZE) {
shell_error(sh, "Failed to set initialisation vector");
return -ENOEXEC;
}
shell_info(sh, "session key set to:");
shell_hexdump(sh, bt_shell_ead_session_key, BT_EAD_KEY_SIZE);
shell_info(sh, "initialisation vector set to:");
shell_hexdump(sh, bt_shell_ead_iv, BT_EAD_IV_SIZE);
return 0;
}
int encrypted_ad_store_ad(const struct shell *sh, uint8_t type, const uint8_t *data,
uint8_t data_len)
{
/* data_len is the size of the data, add two bytes for the size of the type
* and the length that will be stored with the data
*/
uint8_t new_data_size = data_len + 2;
if (bt_shell_ead_data_size + new_data_size > BT_SHELL_EAD_DATA_MAX_SIZE) {
shell_error(sh, "Failed to add data (trying to add %d but %d already used on %d)",
new_data_size, bt_shell_ead_data_size, BT_SHELL_EAD_DATA_MAX_SIZE);
return -ENOEXEC;
}
/* the length is the size of the data + the size of the type */
bt_shell_ead_data[bt_shell_ead_data_size] = data_len + 1;
bt_shell_ead_data[bt_shell_ead_data_size + 1] = type;
memcpy(&bt_shell_ead_data[bt_shell_ead_data_size + 2], data, data_len);
bt_shell_ead_data_size += new_data_size;
bt_shell_ead_ad_len += 1;
return 0;
}
bool is_payload_valid_ad(uint8_t *payload, size_t payload_size)
{
size_t idx = 0;
bool is_valid = true;
uint8_t ad_len;
while (idx < payload_size) {
ad_len = payload[idx];
if (payload_size <= ad_len) {
is_valid = false;
break;
}
idx += ad_len + 1;
}
if (idx != payload_size) {
is_valid = false;
}
return is_valid;
}
static int cmd_encrypted_ad_add_ead(const struct shell *sh, size_t argc, char *argv[])
{
size_t len;
char *hex_payload = argv[1];
size_t hex_payload_size = strlen(hex_payload);
uint8_t payload[BT_SHELL_EAD_DATA_MAX_SIZE] = {0};
uint8_t payload_size = hex_payload_size / 2 + hex_payload_size % 2;
uint8_t ead_size = BT_EAD_ENCRYPTED_PAYLOAD_SIZE(payload_size);
if (ead_size > BT_SHELL_EAD_DATA_MAX_SIZE) {
shell_error(sh,
"Failed to add data. Maximum AD size is %d, passed data size after "
"encryption is %d",
BT_SHELL_EAD_DATA_MAX_SIZE, ead_size);
return -ENOEXEC;
}
len = hex2bin(hex_payload, hex_payload_size, payload, BT_SHELL_EAD_DATA_MAX_SIZE);
if (len != payload_size) {
shell_error(sh, "Failed to add data");
return -ENOEXEC;
}
/* check that the given advertising data structures are valid before encrypting them */
if (!is_payload_valid_ad(payload, payload_size)) {
shell_error(sh, "Failed to add data. Advertising structure are malformed.");
return -ENOEXEC;
}
/* store not-yet encrypted AD but claim the expected size of encrypted AD */
return encrypted_ad_store_ad(sh, BT_DATA_ENCRYPTED_AD_DATA, payload, ead_size);
}
static int cmd_encrypted_ad_add_ad(const struct shell *sh, size_t argc, char *argv[])
{
size_t len;
uint8_t ad_len;
uint8_t ad_type;
char *hex_payload = argv[1];
size_t hex_payload_size = strlen(hex_payload);
uint8_t payload[BT_SHELL_EAD_DATA_MAX_SIZE] = {0};
uint8_t payload_size = hex_payload_size / 2 + hex_payload_size % 2;
len = hex2bin(hex_payload, hex_payload_size, payload, BT_SHELL_EAD_DATA_MAX_SIZE);
if (len != payload_size) {
shell_error(sh, "Failed to add data");
return -ENOEXEC;
}
/* the length received is the length of ad data + the length of the data
* type but `bt_data` struct `data_len` field is only the size of the
* data
*/
ad_len = payload[0] - 1;
ad_type = payload[1];
/* if the ad data is malformed or there is more than 1 ad data passed we
* fail
*/
if (len != ad_len + 2) {
shell_error(sh,
"Failed to add data. Data need to be formated as specified in the "
"Core Spec. Only one non-encrypted AD payload can be added at a time.");
return -ENOEXEC;
}
return encrypted_ad_store_ad(sh, ad_type, payload, payload_size);
}
static int cmd_encrypted_ad_clear_ad(const struct shell *sh, size_t argc, char *argv[])
{
memset(bt_shell_ead_data, 0, BT_SHELL_EAD_DATA_MAX_SIZE);
bt_shell_ead_ad_len = 0;
bt_shell_ead_data_size = 0;
shell_info(sh, "Advertising data has been cleared.");
return 0;
}
int ead_encrypt_ad(const uint8_t *payload, uint8_t payload_size, uint8_t *encrypted_payload)
{
int err;
err = bt_ead_encrypt(bt_shell_ead_session_key, bt_shell_ead_iv, payload, payload_size,
encrypted_payload);
if (err != 0) {
shell_error(ctx_shell, "Failed to encrypt AD.");
return -1;
}
return 0;
}
int ead_update_ad(void)
{
int err;
size_t idx = 0;
struct bt_le_ext_adv *adv = adv_sets[selected_adv];
struct bt_data *ad;
size_t ad_structs_idx = 0;
struct bt_data ad_structs[BT_SHELL_EAD_MAX_AD] = {0};
size_t encrypted_data_buf_len = 0;
uint8_t encrypted_data_buf[BT_SHELL_EAD_DATA_MAX_SIZE] = {0};
while (idx < bt_shell_ead_data_size && ad_structs_idx < BT_SHELL_EAD_MAX_AD) {
ad = &ad_structs[ad_structs_idx];
/* the data_len from bt_data struct doesn't include the size of the type */
ad->data_len = bt_shell_ead_data[idx] - 1;
if (ad->data_len < 0) {
/* if the len is less than 0 that mean there is not even a type field */
shell_error(ctx_shell, "Failed to update AD due to malformed AD.");
return -ENOEXEC;
}
ad->type = bt_shell_ead_data[idx + 1];
if (ad->data_len > 0) {
if (ad->type == BT_DATA_ENCRYPTED_AD_DATA) {
/* for EAD the size used to store the non-encrypted data
* is the size of those data when encrypted
*/
ead_encrypt_ad(&bt_shell_ead_data[idx + 2],
BT_EAD_DECRYPTED_PAYLOAD_SIZE(ad->data_len),
&encrypted_data_buf[encrypted_data_buf_len]);
ad->data = &encrypted_data_buf[encrypted_data_buf_len];
encrypted_data_buf_len += ad->data_len;
} else {
ad->data = &bt_shell_ead_data[idx + 2];
}
}
ad_structs_idx += 1;
idx += ad->data_len + 2;
}
err = bt_le_ext_adv_set_data(adv, ad_structs, bt_shell_ead_ad_len, NULL, 0);
if (err != 0) {
shell_error(ctx_shell, "Failed to set advertising data (err %d)", err);
return -ENOEXEC;
}
shell_info(ctx_shell, "Advertising data for Advertiser[%d] %p updated.", selected_adv, adv);
return 0;
}
static int cmd_encrypted_ad_commit_ad(const struct shell *sh, size_t argc, char *argv[])
{
return ead_update_ad();
}
static int cmd_encrypted_ad_decrypt_scan(const struct shell *sh, size_t argc, char *argv[])
{
const char *action = argv[1];
if (strcmp(action, "on") == 0) {
bt_shell_ead_decrypt_scan = true;
shell_info(sh, "Received encrypted advertising data will now be decrypted using "
"provided key materials.");
} else if (strcmp(action, "off") == 0) {
bt_shell_ead_decrypt_scan = false;
shell_info(sh, "Received encrypted advertising data will now longer be decrypted.");
} else {
shell_error(sh, "Invalid option.");
return -ENOEXEC;
}
return 0;
}
#endif
static int cmd_default_handler(const struct shell *sh, size_t argc, char **argv)
{
if (argc == 1) {
@ -4174,6 +4497,19 @@ SHELL_STATIC_SUBCMD_SET_CREATE(bt_scan_filter_clear_cmds,
);
#endif /* CONFIG_BT_OBSERVER */
#if defined(CONFIG_BT_EAD)
SHELL_STATIC_SUBCMD_SET_CREATE(
bt_encrypted_ad_cmds,
SHELL_CMD_ARG(set-keys, NULL, "<session key> <init vector>", cmd_encrypted_ad_set_keys, 3,
0),
SHELL_CMD_ARG(add-ead, NULL, "<advertising data>", cmd_encrypted_ad_add_ead, 2, 0),
SHELL_CMD_ARG(add-ad, NULL, "<advertising data>", cmd_encrypted_ad_add_ad, 2, 0),
SHELL_CMD(clear-ad, NULL, HELP_NONE, cmd_encrypted_ad_clear_ad),
SHELL_CMD(commit-ad, NULL, HELP_NONE, cmd_encrypted_ad_commit_ad),
SHELL_CMD_ARG(decrypt-scan, NULL, HELP_ONOFF, cmd_encrypted_ad_decrypt_scan, 2, 0),
SHELL_SUBCMD_SET_END);
#endif
SHELL_STATIC_SUBCMD_SET_CREATE(bt_cmds,
SHELL_CMD_ARG(init, NULL, "[no-settings-load], [sync]",
cmd_init, 1, 2),
@ -4262,6 +4598,10 @@ SHELL_STATIC_SUBCMD_SET_CREATE(bt_cmds,
cmd_per_adv_sync_delete, 1, 1),
SHELL_CMD_ARG(per-adv-sync-select, NULL, "[adv]", cmd_per_adv_sync_select, 1, 1),
#endif /* defined(CONFIG_BT_PER_ADV_SYNC) */
#if defined(CONFIG_BT_EAD)
SHELL_CMD(encrypted-ad, &bt_encrypted_ad_cmds, "Manage advertiser with encrypted data",
cmd_default_handler),
#endif /* CONFIG_BT_EAD */
#if defined(CONFIG_BT_CONN)
#if defined(CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER)
SHELL_CMD_ARG(past-subscribe, NULL, "[conn] [skip <count>] "

View file

@ -46,6 +46,8 @@ CONFIG_BT_PER_ADV_SYNC=y
CONFIG_BT_USER_DATA_LEN_UPDATE=y
CONFIG_BT_AUTO_DATA_LEN_UPDATE=y
CONFIG_BT_EAD=y
CONFIG_BT_USER_PHY_UPDATE=y
CONFIG_BT_AUTO_PHY_UPDATE=y