Skip to content

Commit

Permalink
ble: allow configuring device privacy
Browse files Browse the repository at this point in the history
This allows using a random private resolvable address to prevent being tracked.

Currently only enabled on Bangle.js 2.
  • Loading branch information
ssievert42 committed May 31, 2024
1 parent 52352ef commit 9d1aa16
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 3 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
: Fix XON/OFF thresholds to be based off the correct buffer size
Bangle.js2: Allow configuring device privacy to use random BLE addresses

2v22 : Graphics: Ensure floodFill sets modified area correctly
nRF52: Lower expected BLE XTAL accuracy to 50ppm (can improve BLE stability on some Bangle.js 2)
Expand Down
1 change: 1 addition & 0 deletions boards/BANGLEJS2.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
'DEFINES += -DBUTTONPRESS_TO_REBOOT_BOOTLOADER',
'BOOTLOADER_SETTINGS_FAMILY=NRF52840',
'DEFINES += -DESPR_BOOTLOADER_SPIFLASH', # Allow bootloader to flash direct from SPI flash
'DEFINES += -DESPR_BLE_PRIVATE_ADDRESS_SUPPORT',
'NRF_SDK15=1'
]
}
Expand Down
4 changes: 4 additions & 0 deletions libs/bluetooth/bluetooth.h
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ void jsble_central_setWhitelist(bool whitelist);
void jsble_central_eraseBonds();
/// Try to resolve a bonded peer's address from a random private resolvable address
JsVar *jsble_resolveAddress(JsVar *address);
#ifdef ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
JsVar *jsble_getPrivacy();
void jsble_setPrivacy(JsVar *options);
#endif // ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
#endif

#endif // BLUETOOTH_H
107 changes: 107 additions & 0 deletions libs/bluetooth/bluetooth_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,113 @@ const char *bleVarToUUIDAndUnLock(ble_uuid_t *uuid, JsVar *v) {
return r;
}

#if PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
bool bleVarToPrivacy(JsVar *options, pm_privacy_params_t *privacy) {
memset(privacy, 0, sizeof(pm_privacy_params_t));
privacy->privacy_mode = BLE_GAP_PRIVACY_MODE_OFF;
privacy->private_addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE;
privacy->private_addr_cycle_s = 0; // use default address change cycle
privacy->p_device_irk = NULL; // use device default irk
// options may be either undefined, the string "off" or and object
if (jsvIsUndefined(options)) {
return true;
}
if (jsvIsString(options) && jsvIsStringEqual(options, "off")) {
return true;
}
if (jsvIsObject(options)) {
bool invalidOption = false;
// privacy mode
{
JsVar *privacyModeVar = jsvObjectGetChildIfExists(options, "mode");
if (privacyModeVar && jsvIsString(privacyModeVar)) {
if (jsvIsStringEqual(privacyModeVar, "off")) {
privacy->privacy_mode = BLE_GAP_PRIVACY_MODE_OFF;
} else if (jsvIsStringEqual(privacyModeVar, "device_privacy")) {
privacy->privacy_mode = BLE_GAP_PRIVACY_MODE_DEVICE_PRIVACY;
} else if (jsvIsStringEqual(privacyModeVar, "network_privacy")) {
privacy->privacy_mode = BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY;
} else {
invalidOption = true;
}
} else {
invalidOption = true;
}
jsvUnLock(privacyModeVar);
}
// other options are only relevant if privacy_mode is something other than off
if (privacy->privacy_mode != BLE_GAP_PRIVACY_MODE_OFF) {
// private addr type
{
JsVar *privacyAddrTypeVar = jsvObjectGetChildIfExists(options, "addr_type");
if (privacyAddrTypeVar && jsvIsString(privacyAddrTypeVar)) {
if (jsvIsStringEqual(privacyAddrTypeVar, "random_private_resolvable")) {
privacy->private_addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE;
} else if (jsvIsStringEqual(privacyAddrTypeVar, "random_private_non_resolvable")) {
privacy->private_addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE;
} else {
invalidOption = true;
}
} else {
invalidOption = true;
}
jsvUnLock(privacyAddrTypeVar);
}
// private addr cycle s
{
JsVar *privateAddrCycleSVar = jsvObjectGetChildIfExists(options, "addr_cycle_s");
if (privateAddrCycleSVar && jsvIsInt(privateAddrCycleSVar)) {
privacy->private_addr_cycle_s = jsvGetInteger(privateAddrCycleSVar);
} else {
invalidOption = true;
}
jsvUnLock(privateAddrCycleSVar);
}
}
return !invalidOption;
}
return false;
}

JsVar *blePrivacyToVar(pm_privacy_params_t *privacy) {
if (privacy) {
char *mode_str = "";
switch (privacy->privacy_mode) {
case BLE_GAP_PRIVACY_MODE_OFF:
mode_str = "off";
break;
case BLE_GAP_PRIVACY_MODE_DEVICE_PRIVACY:
mode_str = "device_privacy";
break;
case BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY:
mode_str = "network_privacy";
break;
}
char *addr_type_str = "";
switch (privacy->private_addr_type) {
case BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE:
addr_type_str = "random_private_resolvable";
break;
case BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE:
addr_type_str = "random_private_non_resolvable";
break;
}
// only return an object if privacy_mode is something other than off
if (privacy->privacy_mode == BLE_GAP_PRIVACY_MODE_OFF) {
return jsvNewFromString(mode_str);
} else {
JsVar *result = jsvNewObject();
if (!result) return 0;
jsvObjectSetChildAndUnLock(result, "mode", jsvNewFromString(mode_str));
jsvObjectSetChildAndUnLock(result, "addr_type", jsvNewFromString(addr_type_str));
jsvObjectSetChildAndUnLock(result, "addr_cycle_s", jsvNewFromInteger(privacy->private_addr_cycle_s));
return result;
}
}
return 0;
}
#endif // PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT

/// Queue an event on the 'NRF' object. Also calls jshHadEvent()
void bleQueueEventAndUnLock(const char *name, JsVar *data) {
//jsiConsolePrintf("[%s] %j\n", name, data);
Expand Down
8 changes: 8 additions & 0 deletions libs/bluetooth/bluetooth_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

#include "jsvar.h"
#include "bluetooth.h"
#if PEER_MANAGER_ENABLED
#include "peer_manager_types.h"
#endif

#define BLE_SCAN_EVENT JS_EVENT_PREFIX"blescan"
#define BLE_WRITE_EVENT JS_EVENT_PREFIX"blew"
Expand Down Expand Up @@ -70,6 +73,11 @@ const char *bleVarToUUID(ble_uuid_t *uuid, JsVar *v);
/// Same as bleVarToUUID, but unlocks v
const char *bleVarToUUIDAndUnLock(ble_uuid_t *uuid, JsVar *v);

#if PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
bool bleVarToPrivacy(JsVar *options, pm_privacy_params_t *privacy);
JsVar *blePrivacyToVar(pm_privacy_params_t *privacy);
#endif // PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT

/// Queue an event on the 'NRF' object. Also calls jshHadEvent()
void bleQueueEventAndUnLock(const char *name, JsVar *data);

Expand Down
49 changes: 49 additions & 0 deletions libs/bluetooth/jswrap_bluetooth.c
Original file line number Diff line number Diff line change
Expand Up @@ -3602,6 +3602,8 @@ NRF.setSecurity({
encryptUart : bool // default false (unless oob or passkey specified)
// This sets the BLE UART service such that it
// is encrypted and can only be used from a paired connection
privacy : // default "off", or an object defining BLE privacy / random address options - see below for more info
// only available if Espruino was compiled with private address support (like for example on Bangle.js 2)
});
```
Expand Down Expand Up @@ -3675,6 +3677,46 @@ NRF.setServices({
**Note:** If `passkey` or `oob` is specified, the Nordic UART service (if
enabled) will automatically be set to require encryption, but otherwise it is
open.
The `privacy` parameter can be used to set this devices BLE privacy / random address settings.
The privacy feature provides a way to avoid being tracked over a period of time.
This works by replacing the real BLE address with a random private address,
that automatically changes at a specified interval.
If a `"random_private_resolvable"` address is used, that address is generated with the help
of an identity resolving key (IRK), that is exchanged during bonding.
This allows a bonded device to still identify another device that is using a random private resolvable address.
Note that, while this can help against being tracked, there are other ways a Bluetooth device can reveal its identity.
For example, the name or services it advertises may be unique enough.
```
NRF.setPrivacy({
privacy: {
mode : string // The privacy mode that should be used.
addr_type : string // The type of address to use.
addr_cycle_s : number // How often the address should change, in seconds.
}
});
```
`mode` can be one of:
* `"off"` - Use the real address.
* `"device_privacy"` - Use a private address.
* `"network_privacy"` - Use a private address,
and reject a peer that uses its real address if we know that peer's IRK.
If `mode` is `"off"`, all other fields are ignored and become optional.
`addr_type` can be one of:
* `"random_private_resolvable"` - Address that can be resolved by a bonded peer that knows our IRK.
* `"random_private_non_resolvable"` - Address that cannot be resolved.
`addr_cycle_s` must be an integer. Pass `0` to use the default address change interval.
The default is usually to change the address every 15 minutes (or 900 seconds).
*/
void jswrap_ble_setSecurity(JsVar *options) {
if (!jsvIsObject(options) && !jsvIsUndefined(options))
Expand All @@ -3691,6 +3733,11 @@ void jswrap_ble_setSecurity(JsVar *options) {
/*TYPESCRIPT
type NRFSecurityStatus = {
advertising: boolean,
privacy?: "off" || {
mode: string,
addr_type: string,
addr_cycle_s: number,
},
} & (
{
connected: true,
Expand Down Expand Up @@ -3727,6 +3774,8 @@ peripheral connection:
bonded // The peer is bonded with us
advertising // Are we currently advertising?
connected_addr // If connected=true, the MAC address of the currently connected device
privacy // Current BLE privacy / random address settings.
// Only present if Espruino was compiled with private address support (like for example on Bangle.js 2).
}
```
Expand Down
74 changes: 71 additions & 3 deletions targets/nrf5x/bluetooth.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ static pm_peer_id_t m_whitelist_peers[BLE_GAP_WHITELIST_ADDR_MAX_COUNT]; /**<
static uint32_t m_whitelist_peer_cnt; /**< Number of peers currently in the whitelist. */
static bool m_is_wl_changed; /**< Indicates if the whitelist has been changed since last time it has been updated in the Peer Manager. */
static volatile ble_gap_sec_params_t m_sec_params; /**< Current security parameters. */
#ifdef ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
static pm_privacy_params_t m_privacy_params; /**< Current privacy parameters */
#endif // ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
// needed for peer_manager_init so we can smoothly upgrade from pre 1v92 firmwares
#include "fds_internal_defs.h"
// If we have peer manager we have central mode and NRF52
Expand Down Expand Up @@ -2185,6 +2188,12 @@ void jsble_update_security() {
}
uint32_t err_code = sd_ble_opt_set(BLE_GAP_OPT_PASSKEY, &pin_option);
jsble_check_error(err_code);
#ifdef ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
// privacy / random address
JsVar *privacy = jsvObjectGetChildIfExists(options, "privacy");
jsble_setPrivacy(privacy);
jsvUnLock(privacy);
#endif // ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
}
// If UART encryption or mitm protection status changed, we need to update flags and restart Bluetooth
if (((bleStatus & BLE_ENCRYPT_UART) != 0) != encryptUart || ((bleStatus & BLE_SECURITY_MITM) != 0) != mitmProtect) {
Expand Down Expand Up @@ -2291,6 +2300,14 @@ static void peer_manager_init(bool erase_bonds) {

jsble_update_security(); // pm_sec_params_set

#ifdef ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
memset(&m_privacy_params, 0, sizeof(pm_privacy_params_t));
m_privacy_params.privacy_mode = BLE_GAP_PRIVACY_MODE_OFF;
m_privacy_params.private_addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE;
m_privacy_params.private_addr_cycle_s = BLE_GAP_DEFAULT_PRIVATE_ADDR_CYCLE_INTERVAL_S;
m_privacy_params.p_device_irk = NULL;
#endif // ESPR_BLE_PRIVATE_ADDRESS_SUPPORT

err_code = pm_register(pm_evt_handler);
APP_ERROR_CHECK(err_code);

Expand Down Expand Up @@ -2867,18 +2884,31 @@ void jsble_advertising_stop() {
NRF_RADIO_NOTIFICATION_DISTANCE_NONE);
APP_ERROR_CHECK(err_code);

#if PEER_MANAGER_ENABLED
peer_manager_init(false /*don't erase_bonds*/);
#ifdef ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
err_code = pm_privacy_set(&m_privacy_params);
if (err_code) jsiConsolePrintf("pm_privacy_set failed: 0x%x\n", err_code);
#endif // ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
#endif // PEER_MANAGER_ENABLED

#ifdef NRF52_SERIES
// Set MAC address
JsVar *v = jsvObjectGetChildIfExists(execInfo.hiddenRoot, BLE_NAME_MAC_ADDRESS);
if (v) {
ble_gap_addr_t p_addr;
if (bleVarToAddr(v, &p_addr)) {
#if PEER_MANAGER_ENABLED
err_code = pm_id_addr_set(&p_addr);
if (err_code) jsiConsolePrintf("pm_id_addr_set failed: 0x%x\n", err_code);
#else
#if NRF_SD_BLE_API_VERSION < 3
err_code = sd_ble_gap_address_set(BLE_GAP_ADDR_CYCLE_MODE_NONE,&p_addr);
#else
err_code = sd_ble_gap_addr_set(&p_addr);
#endif
if (err_code) jsiConsolePrintf("sd_ble_gap_addr_set failed: 0x%x\n", err_code);
#endif // PEER_MANAGER_ENABLED
}
}
jsvUnLock(v);
Expand All @@ -2899,9 +2929,6 @@ void jsble_advertising_stop() {
*/
#endif

#if PEER_MANAGER_ENABLED
peer_manager_init(false /*don't erase_bonds*/);
#endif
gap_params_init();
services_init();
conn_params_init();
Expand Down Expand Up @@ -3387,6 +3414,9 @@ JsVar *jsble_get_security_status(uint16_t conn_handle) {
jsvObjectSetChildAndUnLock(result, "encrypted", jsvNewFromBool(status.encrypted));
jsvObjectSetChildAndUnLock(result, "mitm_protected", jsvNewFromBool(status.mitm_protected));
jsvObjectSetChildAndUnLock(result, "bonded", jsvNewFromBool(status.bonded));
#ifdef ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
jsvObjectSetChildAndUnLock(result, "privacy", jsble_getPrivacy());
#endif // ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
#ifndef SAVE_ON_FLASH
if (status.connected && conn_handle==m_peripheral_conn_handle)
jsvObjectSetChildAndUnLock(result, "connected_addr", bleAddrToStr(m_peripheral_addr));
Expand Down Expand Up @@ -3713,6 +3743,44 @@ JsVar *jsble_resolveAddress(JsVar *address) {
}
#endif // PEER_MANAGER_ENABLED

#if PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
JsVar *jsble_getPrivacy() {
pm_privacy_params_t privacy;
memset(&privacy, 0, sizeof(pm_privacy_params_t));
ble_gap_irk_t irk;
memset(&irk, 0, sizeof(ble_gap_irk_t));
privacy.p_device_irk = &irk;
uint32_t err_code = pm_privacy_get(&privacy);
if (!jsble_check_error(err_code)) {
return blePrivacyToVar(&privacy);
}
return 0;
}
#endif // PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT

#if PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT
void jsble_setPrivacy(JsVar *options) {
if (!jsvIsObject(options) && !(jsvIsString(options) && jsvIsStringEqual(options, "off")) && !jsvIsUndefined(options)) {
jsExceptionHere(JSET_TYPEERROR, "privacy: Expecting an object, the string \"off\" or undefined, got %j", options);
} else {
pm_privacy_params_t privacy;
if (!bleVarToPrivacy(options, &privacy)) {
jsExceptionHere(JSET_TYPEERROR, "privacy: Invalid or missing parameters, got %j", options);
} else {
// only update parameters if they are different
if (privacy.privacy_mode != m_privacy_params.privacy_mode ||
privacy.private_addr_type != m_privacy_params.private_addr_type ||
privacy.private_addr_cycle_s != m_privacy_params.private_addr_cycle_s) {
m_privacy_params = privacy;
// privacy settings can only be applied while not advertising, scanning or in a connection
// we only apply them when the softdevice (re)starts
bleStatus |= BLE_NEEDS_SOFTDEVICE_RESTART;
}
}
}
}
#endif // PEER_MANAGER_ENABLED && ESPR_BLE_PRIVATE_ADDRESS_SUPPORT

#endif // BLUETOOTH


0 comments on commit 9d1aa16

Please sign in to comment.