Skip to content

extmod/modbluetooth: Add gap_unpair command. #7845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/library/bluetooth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,11 @@ Pairing and bonding

On successful pairing, the ``_IRQ_ENCRYPTION_UPDATE`` event will be raised.

.. method:: BLE.gap_unpair(key, /)

Removes pairing details from the bond database, where ``key`` is the entry key
as provided in _IRQ_GET_SECRET/_IRQ_SET_SECRET events.

.. method:: BLE.gap_passkey(conn_handle, action, passkey, /)

Respond to a ``_IRQ_PASSKEY_ACTION`` event for the specified *conn_handle*
Expand Down
21 changes: 21 additions & 0 deletions extmod/btstack/modbluetooth_btstack.c
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,27 @@ int mp_bluetooth_gap_pair(uint16_t conn_handle) {
return 0;
}

int mp_bluetooth_gap_unpair(uint8_t *key, size_t key_len) {
DEBUG_printf("mp_bluetooth_gap_unpair\n");
if (BD_ADDR_LEN != key_len) {
mp_raise_ValueError(MP_ERROR_TEXT("Incorrect key length"));
}

int addr_type;
bd_addr_t addr;
sm_key_t irk;
for (int i = 0; i < MAX_NR_LE_DEVICE_DB_ENTRIES; i++) {
le_device_db_info(i, &addr_type, addr, irk);
if (addr_type != BD_ADDR_TYPE_UNKNOWN) {
if (0 == memcmp(key, addr, BD_ADDR_LEN)) {
le_device_db_remove(i);
return 0;
}
}
}
return MP_ENOENT;
}

int mp_bluetooth_gap_passkey(uint16_t conn_handle, uint8_t action, mp_int_t passkey) {
DEBUG_printf("mp_bluetooth_gap_passkey: conn_handle=%d action=%d passkey=%d\n", conn_handle, action, (int)passkey);
return MP_EOPNOTSUPP;
Expand Down
16 changes: 16 additions & 0 deletions extmod/modbluetooth.c
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,21 @@ static mp_obj_t bluetooth_ble_gap_pair(mp_obj_t self_in, mp_obj_t conn_handle_in
}
static MP_DEFINE_CONST_FUN_OBJ_2(bluetooth_ble_gap_pair_obj, bluetooth_ble_gap_pair);

static mp_obj_t bluetooth_ble_gap_unpair(mp_obj_t self_in, mp_obj_t key_buff) {
(void)self_in;

uint8_t *key = NULL;
size_t key_len = 0;

mp_buffer_info_t key_bufinfo = {0};
mp_get_buffer_raise(key_buff, &key_bufinfo, MP_BUFFER_READ);
key = key_bufinfo.buf;
key_len = key_bufinfo.len;

return bluetooth_handle_errno(mp_bluetooth_gap_unpair(key, key_len));
}
static MP_DEFINE_CONST_FUN_OBJ_2(bluetooth_ble_gap_unpair_obj, bluetooth_ble_gap_unpair);

static mp_obj_t bluetooth_ble_gap_passkey(size_t n_args, const mp_obj_t *args) {
uint16_t conn_handle = mp_obj_get_int(args[1]);
uint8_t action = mp_obj_get_int(args[2]);
Expand Down Expand Up @@ -945,6 +960,7 @@ static const mp_rom_map_elem_t bluetooth_ble_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_gap_disconnect), MP_ROM_PTR(&bluetooth_ble_gap_disconnect_obj) },
#if MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING
{ MP_ROM_QSTR(MP_QSTR_gap_pair), MP_ROM_PTR(&bluetooth_ble_gap_pair_obj) },
{ MP_ROM_QSTR(MP_QSTR_gap_unpair), MP_ROM_PTR(&bluetooth_ble_gap_unpair_obj) },
{ MP_ROM_QSTR(MP_QSTR_gap_passkey), MP_ROM_PTR(&bluetooth_ble_gap_passkey_obj) },
#endif
// GATT Server
Expand Down
3 changes: 3 additions & 0 deletions extmod/modbluetooth.h
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ int mp_bluetooth_set_preferred_mtu(uint16_t mtu);
// Initiate pairing on the specified connection.
int mp_bluetooth_gap_pair(uint16_t conn_handle);

// Remove a specific pairing key from the radio.
int mp_bluetooth_gap_unpair(uint8_t *key, size_t key_len);

// Respond to a pairing request.
int mp_bluetooth_gap_passkey(uint16_t conn_handle, uint8_t action, mp_int_t passkey);
#endif // MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING
Expand Down
9 changes: 9 additions & 0 deletions extmod/nimble/modbluetooth_nimble.c
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,15 @@ int mp_bluetooth_gap_pair(uint16_t conn_handle) {
return ble_hs_err_to_errno(ble_gap_security_initiate(conn_handle));
}

int mp_bluetooth_gap_unpair(uint8_t *key, size_t key_len) {
if (sizeof(ble_addr_t) != key_len) {
mp_raise_ValueError(MP_ERROR_TEXT("Incorrect key length"));
}

DEBUG_printf("mp_bluetooth_gap_unpair: specific\n");
return ble_hs_err_to_errno(ble_gap_unpair((ble_addr_t *)key));
}

int mp_bluetooth_gap_passkey(uint16_t conn_handle, uint8_t action, mp_int_t passkey) {
struct ble_sm_io io = {0};

Expand Down
194 changes: 194 additions & 0 deletions tests/multi_bluetooth/ble_gap_unpair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Test BLE GAP unpair functionality
# Tests the new gap_unpair method added to MicroPython
# gap_unpair expects a key from _IRQ_GET_SECRET/_IRQ_SET_SECRET events

from micropython import const
import time, machine, bluetooth

if not hasattr(bluetooth.BLE, "gap_unpair"):
print("SKIP")
raise SystemExit

TIMEOUT_MS = 4000

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_ENCRYPTION_UPDATE = const(28)
_IRQ_GET_SECRET = const(29)
_IRQ_SET_SECRET = const(30)

_FLAG_READ = const(0x0002)
_FLAG_READ_ENCRYPTED = const(0x0200)

SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
CHAR = (CHAR_UUID, _FLAG_READ | _FLAG_READ_ENCRYPTED)
SERVICE = (SERVICE_UUID, (CHAR,))

waiting_events = {}
bond_keys = [] # Store bond keys for unpair testing


def irq(event, data):
if event == _IRQ_CENTRAL_CONNECT:
print("_IRQ_CENTRAL_CONNECT")
waiting_events[event] = data[0]
elif event == _IRQ_CENTRAL_DISCONNECT:
print("_IRQ_CENTRAL_DISCONNECT")
elif event == _IRQ_GATTS_READ_REQUEST:
print("_IRQ_GATTS_READ_REQUEST")
elif event == _IRQ_PERIPHERAL_CONNECT:
print("_IRQ_PERIPHERAL_CONNECT")
waiting_events[event] = data[0]
elif event == _IRQ_PERIPHERAL_DISCONNECT:
print("_IRQ_PERIPHERAL_DISCONNECT")
elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
if data[-1] == CHAR_UUID:
print("_IRQ_GATTC_CHARACTERISTIC_RESULT", data[-1])
waiting_events[event] = data[2]
else:
return
elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
print("_IRQ_GATTC_CHARACTERISTIC_DONE")
elif event == _IRQ_GATTC_READ_RESULT:
print("_IRQ_GATTC_READ_RESULT", bytes(data[-1]))
elif event == _IRQ_ENCRYPTION_UPDATE:
print("_IRQ_ENCRYPTION_UPDATE", data[1], data[2], data[3])
elif event == _IRQ_GET_SECRET:
print("_IRQ_GET_SECRET", "key:", data[1])
bond_keys.append(data[1]) # Store the key for unpair testing
elif event == _IRQ_SET_SECRET:
print("_IRQ_SET_SECRET", "key:", data[1])
bond_keys.append(data[1]) # Store the key for unpair testing

if event not in waiting_events:
waiting_events[event] = None


def wait_for_event(event, timeout_ms):
t0 = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), t0) < timeout_ms:
if event in waiting_events:
return waiting_events.pop(event)
machine.idle()
raise ValueError("Timeout waiting for {}".format(event))


# Acting in peripheral role.
def instance0():
multitest.globals(BDADDR=ble.config("mac"))
((char_handle,),) = ble.gatts_register_services((SERVICE,))
ble.gatts_write(char_handle, "encrypted")
print("gap_advertise")
ble.gap_advertise(20_000, b"\x02\x01\x06\x04\xffMPY")
multitest.next()
try:
# Wait for central to connect.
wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)

# Wait for pairing event.
wait_for_event(_IRQ_ENCRYPTION_UPDATE, TIMEOUT_MS)

# Wait for GATTS read request.
wait_for_event(_IRQ_GATTS_READ_REQUEST, TIMEOUT_MS)

multitest.next()

# Wait for central to disconnect after initial pairing.
wait_for_event(_IRQ_CENTRAL_DISCONNECT, TIMEOUT_MS)

# Test gap_unpair functionality
print("gap_unpair_test")
print("bond_keys_captured:", len(bond_keys))

# Test gap_unpair with captured bond keys
if bond_keys:
for i, key in enumerate(bond_keys):
try:
result = ble.gap_unpair(key)
print(f"gap_unpair_key_{i}_result:", result)
except Exception as e:
print(f"gap_unpair_key_{i}_error:", type(e).__name__, str(e))
else:
print("gap_unpair_no_keys_captured")

# Test unpair with non-existent key
fake_key = b'\x01\x12\x34\x56\x78\x9a\xbc\xde\xf0\x11\x22\x33\x44\x55\x66\x77'
try:
result = ble.gap_unpair(fake_key)
print("gap_unpair_fake_key_result:", result)
except Exception as e:
print("gap_unpair_fake_key_error:", type(e).__name__, str(e))

# Test unpair with wrong key format (should fail)
try:
result = ble.gap_unpair(b'\x01\x02\x03\x04\x05\x06') # Too short
print("gap_unpair_wrong_key_result:", result)
except Exception as e:
print("gap_unpair_wrong_key_error:", type(e).__name__)

finally:
ble.active(0)


# Acting in central role.
def instance1():
multitest.next()
try:
# Connect to peripheral.
print("gap_connect")
ble.gap_connect(*BDADDR)
conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)

# Discover characteristics (before pairing, doesn't need to be encrypted).
ble.gattc_discover_characteristics(conn_handle, 1, 65535)
value_handle = wait_for_event(_IRQ_GATTC_CHARACTERISTIC_RESULT, TIMEOUT_MS)
wait_for_event(_IRQ_GATTC_CHARACTERISTIC_DONE, TIMEOUT_MS)

# Pair with the peripheral.
print("gap_pair")
ble.gap_pair(conn_handle)

# Wait for the pairing event.
wait_for_event(_IRQ_ENCRYPTION_UPDATE, TIMEOUT_MS)

# Read the peripheral's characteristic, should be encrypted.
print("gattc_read")
ble.gattc_read(conn_handle, value_handle)
wait_for_event(_IRQ_GATTC_READ_RESULT, TIMEOUT_MS)

multitest.next()

# Disconnect from the peripheral.
print("gap_disconnect:", ble.gap_disconnect(conn_handle))
wait_for_event(_IRQ_PERIPHERAL_DISCONNECT, TIMEOUT_MS)

# Test gap_unpair on central side too
print("central_gap_unpair_test")
print("central_bond_keys_captured:", len(bond_keys))

# Test gap_unpair with captured bond keys on central side
if bond_keys:
for i, key in enumerate(bond_keys):
try:
result = ble.gap_unpair(key)
print(f"central_gap_unpair_key_{i}_result:", result)
except Exception as e:
print(f"central_gap_unpair_key_{i}_error:", type(e).__name__, str(e))
else:
print("central_gap_unpair_no_keys")

finally:
ble.active(0)


ble = bluetooth.BLE()
ble.config(mitm=True, le_secure=True, bond=True) # Enable bonding for unpair test
ble.active(1)
ble.irq(irq)
41 changes: 41 additions & 0 deletions tests/multi_bluetooth/ble_gap_unpair.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--- instance0 ---
gap_advertise
_IRQ_CENTRAL_CONNECT
_IRQ_GET_SECRET key: 0
_IRQ_GET_SECRET key: 0
_IRQ_GET_SECRET key: 0
_IRQ_SET_SECRET key: <memoryview>
_IRQ_GET_SECRET key: 0
_IRQ_SET_SECRET key: <memoryview>
_IRQ_GET_SECRET key: 0
_IRQ_ENCRYPTION_UPDATE 1 0 1
_IRQ_GATTS_READ_REQUEST
_IRQ_CENTRAL_DISCONNECT
gap_unpair_test
bond_keys_captured: 3
gap_unpair_key_0_result: None
gap_unpair_key_1_result: None
gap_unpair_key_2_result: None
gap_unpair_fake_key_error: ValueError
gap_unpair_wrong_key_error: ValueError
--- instance1 ---
gap_connect
_IRQ_PERIPHERAL_CONNECT
_IRQ_GATTC_CHARACTERISTIC_RESULT UUID('00000000-1111-2222-3333-444444444444')
_IRQ_GATTC_CHARACTERISTIC_DONE
gap_pair
_IRQ_GET_SECRET key: 0
_IRQ_ENCRYPTION_UPDATE 1 0 1
_IRQ_SET_SECRET key: <memoryview>
_IRQ_GET_SECRET key: 0
_IRQ_SET_SECRET key: <memoryview>
_IRQ_GET_SECRET key: 0
gattc_read
_IRQ_GATTC_READ_RESULT b'encrypted'
gap_disconnect: True
_IRQ_PERIPHERAL_DISCONNECT
central_gap_unpair_test
central_bond_keys_captured: 3
central_gap_unpair_key_0_result: None
central_gap_unpair_key_1_result: None
central_gap_unpair_key_2_result: None
Loading