Skip to content

Commit ade5914

Browse files
committed
extmod/modbluetooth: Add gap_unpair command.
This function deletes the pairing details from the DB on both the application and the radio, so can be used to free up IRK slots. Tests included for both single-device interface validation and multi-device pairing/unpairing scenarios with real bond keys. Signed-off-by: Andrew Leech <[email protected]>
1 parent 27b7bf3 commit ade5914

File tree

7 files changed

+289
-0
lines changed

7 files changed

+289
-0
lines changed

docs/library/bluetooth.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,11 @@ Pairing and bonding
733733

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

736+
.. method:: BLE.gap_unpair(key, /)
737+
738+
Removes pairing details from the bond database, where ``key`` is the entry key
739+
as provided in _IRQ_GET_SECRET/_IRQ_SET_SECRET events.
740+
736741
.. method:: BLE.gap_passkey(conn_handle, action, passkey, /)
737742

738743
Respond to a ``_IRQ_PASSKEY_ACTION`` event for the specified *conn_handle*

extmod/btstack/modbluetooth_btstack.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,27 @@ int mp_bluetooth_gap_pair(uint16_t conn_handle) {
12651265
return 0;
12661266
}
12671267

1268+
int mp_bluetooth_gap_unpair(uint8_t *key, size_t key_len) {
1269+
DEBUG_printf("mp_bluetooth_gap_unpair\n");
1270+
if (BD_ADDR_LEN != key_len) {
1271+
mp_raise_ValueError(MP_ERROR_TEXT("Incorrect key length"));
1272+
}
1273+
1274+
int addr_type;
1275+
bd_addr_t addr;
1276+
sm_key_t irk;
1277+
for (int i = 0; i < MAX_NR_LE_DEVICE_DB_ENTRIES; i++) {
1278+
le_device_db_info(i, &addr_type, addr, irk);
1279+
if (addr_type != BD_ADDR_TYPE_UNKNOWN) {
1280+
if (0 == memcmp(key, addr, BD_ADDR_LEN)) {
1281+
le_device_db_remove(i);
1282+
return 0;
1283+
}
1284+
}
1285+
}
1286+
return MP_ENOENT;
1287+
}
1288+
12681289
int mp_bluetooth_gap_passkey(uint16_t conn_handle, uint8_t action, mp_int_t passkey) {
12691290
DEBUG_printf("mp_bluetooth_gap_passkey: conn_handle=%d action=%d passkey=%d\n", conn_handle, action, (int)passkey);
12701291
return MP_EOPNOTSUPP;

extmod/modbluetooth.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,21 @@ static mp_obj_t bluetooth_ble_gap_pair(mp_obj_t self_in, mp_obj_t conn_handle_in
717717
}
718718
static MP_DEFINE_CONST_FUN_OBJ_2(bluetooth_ble_gap_pair_obj, bluetooth_ble_gap_pair);
719719

720+
static mp_obj_t bluetooth_ble_gap_unpair(mp_obj_t self_in, mp_obj_t key_buff) {
721+
(void)self_in;
722+
723+
uint8_t *key = NULL;
724+
size_t key_len = 0;
725+
726+
mp_buffer_info_t key_bufinfo = {0};
727+
mp_get_buffer_raise(key_buff, &key_bufinfo, MP_BUFFER_READ);
728+
key = key_bufinfo.buf;
729+
key_len = key_bufinfo.len;
730+
731+
return bluetooth_handle_errno(mp_bluetooth_gap_unpair(key, key_len));
732+
}
733+
static MP_DEFINE_CONST_FUN_OBJ_2(bluetooth_ble_gap_unpair_obj, bluetooth_ble_gap_unpair);
734+
720735
static mp_obj_t bluetooth_ble_gap_passkey(size_t n_args, const mp_obj_t *args) {
721736
uint16_t conn_handle = mp_obj_get_int(args[1]);
722737
uint8_t action = mp_obj_get_int(args[2]);
@@ -945,6 +960,7 @@ static const mp_rom_map_elem_t bluetooth_ble_locals_dict_table[] = {
945960
{ MP_ROM_QSTR(MP_QSTR_gap_disconnect), MP_ROM_PTR(&bluetooth_ble_gap_disconnect_obj) },
946961
#if MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING
947962
{ MP_ROM_QSTR(MP_QSTR_gap_pair), MP_ROM_PTR(&bluetooth_ble_gap_pair_obj) },
963+
{ MP_ROM_QSTR(MP_QSTR_gap_unpair), MP_ROM_PTR(&bluetooth_ble_gap_unpair_obj) },
948964
{ MP_ROM_QSTR(MP_QSTR_gap_passkey), MP_ROM_PTR(&bluetooth_ble_gap_passkey_obj) },
949965
#endif
950966
// GATT Server

extmod/modbluetooth.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ int mp_bluetooth_set_preferred_mtu(uint16_t mtu);
358358
// Initiate pairing on the specified connection.
359359
int mp_bluetooth_gap_pair(uint16_t conn_handle);
360360

361+
// Remove a specific pairing key from the radio.
362+
int mp_bluetooth_gap_unpair(uint8_t *key, size_t key_len);
363+
361364
// Respond to a pairing request.
362365
int mp_bluetooth_gap_passkey(uint16_t conn_handle, uint8_t action, mp_int_t passkey);
363366
#endif // MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING

extmod/nimble/modbluetooth_nimble.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,15 @@ int mp_bluetooth_gap_pair(uint16_t conn_handle) {
11111111
return ble_hs_err_to_errno(ble_gap_security_initiate(conn_handle));
11121112
}
11131113

1114+
int mp_bluetooth_gap_unpair(uint8_t *key, size_t key_len) {
1115+
if (sizeof(ble_addr_t) != key_len) {
1116+
mp_raise_ValueError(MP_ERROR_TEXT("Incorrect key length"));
1117+
}
1118+
1119+
DEBUG_printf("mp_bluetooth_gap_unpair: specific\n");
1120+
return ble_hs_err_to_errno(ble_gap_unpair((ble_addr_t *)key));
1121+
}
1122+
11141123
int mp_bluetooth_gap_passkey(uint16_t conn_handle, uint8_t action, mp_int_t passkey) {
11151124
struct ble_sm_io io = {0};
11161125

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Test BLE GAP unpair functionality
2+
# Tests the new gap_unpair method added to MicroPython
3+
# gap_unpair expects a key from _IRQ_GET_SECRET/_IRQ_SET_SECRET events
4+
5+
from micropython import const
6+
import time, machine, bluetooth
7+
8+
if not hasattr(bluetooth.BLE, "gap_unpair"):
9+
print("SKIP")
10+
raise SystemExit
11+
12+
TIMEOUT_MS = 4000
13+
14+
_IRQ_CENTRAL_CONNECT = const(1)
15+
_IRQ_CENTRAL_DISCONNECT = const(2)
16+
_IRQ_GATTS_READ_REQUEST = const(4)
17+
_IRQ_PERIPHERAL_CONNECT = const(7)
18+
_IRQ_PERIPHERAL_DISCONNECT = const(8)
19+
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
20+
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
21+
_IRQ_GATTC_READ_RESULT = const(15)
22+
_IRQ_ENCRYPTION_UPDATE = const(28)
23+
_IRQ_GET_SECRET = const(29)
24+
_IRQ_SET_SECRET = const(30)
25+
26+
_FLAG_READ = const(0x0002)
27+
_FLAG_READ_ENCRYPTED = const(0x0200)
28+
29+
SERVICE_UUID = bluetooth.UUID("A5A5A5A5-FFFF-9999-1111-5A5A5A5A5A5A")
30+
CHAR_UUID = bluetooth.UUID("00000000-1111-2222-3333-444444444444")
31+
CHAR = (CHAR_UUID, _FLAG_READ | _FLAG_READ_ENCRYPTED)
32+
SERVICE = (SERVICE_UUID, (CHAR,))
33+
34+
waiting_events = {}
35+
bond_keys = [] # Store bond keys for unpair testing
36+
37+
38+
def irq(event, data):
39+
if event == _IRQ_CENTRAL_CONNECT:
40+
print("_IRQ_CENTRAL_CONNECT")
41+
waiting_events[event] = data[0]
42+
elif event == _IRQ_CENTRAL_DISCONNECT:
43+
print("_IRQ_CENTRAL_DISCONNECT")
44+
elif event == _IRQ_GATTS_READ_REQUEST:
45+
print("_IRQ_GATTS_READ_REQUEST")
46+
elif event == _IRQ_PERIPHERAL_CONNECT:
47+
print("_IRQ_PERIPHERAL_CONNECT")
48+
waiting_events[event] = data[0]
49+
elif event == _IRQ_PERIPHERAL_DISCONNECT:
50+
print("_IRQ_PERIPHERAL_DISCONNECT")
51+
elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
52+
if data[-1] == CHAR_UUID:
53+
print("_IRQ_GATTC_CHARACTERISTIC_RESULT", data[-1])
54+
waiting_events[event] = data[2]
55+
else:
56+
return
57+
elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
58+
print("_IRQ_GATTC_CHARACTERISTIC_DONE")
59+
elif event == _IRQ_GATTC_READ_RESULT:
60+
print("_IRQ_GATTC_READ_RESULT", bytes(data[-1]))
61+
elif event == _IRQ_ENCRYPTION_UPDATE:
62+
print("_IRQ_ENCRYPTION_UPDATE", data[1], data[2], data[3])
63+
elif event == _IRQ_GET_SECRET:
64+
print("_IRQ_GET_SECRET", "key:", data[1])
65+
bond_keys.append(data[1]) # Store the key for unpair testing
66+
elif event == _IRQ_SET_SECRET:
67+
print("_IRQ_SET_SECRET", "key:", data[1])
68+
bond_keys.append(data[1]) # Store the key for unpair testing
69+
70+
if event not in waiting_events:
71+
waiting_events[event] = None
72+
73+
74+
def wait_for_event(event, timeout_ms):
75+
t0 = time.ticks_ms()
76+
while time.ticks_diff(time.ticks_ms(), t0) < timeout_ms:
77+
if event in waiting_events:
78+
return waiting_events.pop(event)
79+
machine.idle()
80+
raise ValueError("Timeout waiting for {}".format(event))
81+
82+
83+
# Acting in peripheral role.
84+
def instance0():
85+
multitest.globals(BDADDR=ble.config("mac"))
86+
((char_handle,),) = ble.gatts_register_services((SERVICE,))
87+
ble.gatts_write(char_handle, "encrypted")
88+
print("gap_advertise")
89+
ble.gap_advertise(20_000, b"\x02\x01\x06\x04\xffMPY")
90+
multitest.next()
91+
try:
92+
# Wait for central to connect.
93+
wait_for_event(_IRQ_CENTRAL_CONNECT, TIMEOUT_MS)
94+
95+
# Wait for pairing event.
96+
wait_for_event(_IRQ_ENCRYPTION_UPDATE, TIMEOUT_MS)
97+
98+
# Wait for GATTS read request.
99+
wait_for_event(_IRQ_GATTS_READ_REQUEST, TIMEOUT_MS)
100+
101+
multitest.next()
102+
103+
# Wait for central to disconnect after initial pairing.
104+
wait_for_event(_IRQ_CENTRAL_DISCONNECT, TIMEOUT_MS)
105+
106+
# Test gap_unpair functionality
107+
print("gap_unpair_test")
108+
print("bond_keys_captured:", len(bond_keys))
109+
110+
# Test gap_unpair with captured bond keys
111+
if bond_keys:
112+
for i, key in enumerate(bond_keys):
113+
try:
114+
result = ble.gap_unpair(key)
115+
print(f"gap_unpair_key_{i}_result:", result)
116+
except Exception as e:
117+
print(f"gap_unpair_key_{i}_error:", type(e).__name__, str(e))
118+
else:
119+
print("gap_unpair_no_keys_captured")
120+
121+
# Test unpair with non-existent key
122+
fake_key = b'\x01\x12\x34\x56\x78\x9a\xbc\xde\xf0\x11\x22\x33\x44\x55\x66\x77'
123+
try:
124+
result = ble.gap_unpair(fake_key)
125+
print("gap_unpair_fake_key_result:", result)
126+
except Exception as e:
127+
print("gap_unpair_fake_key_error:", type(e).__name__, str(e))
128+
129+
# Test unpair with wrong key format (should fail)
130+
try:
131+
result = ble.gap_unpair(b'\x01\x02\x03\x04\x05\x06') # Too short
132+
print("gap_unpair_wrong_key_result:", result)
133+
except Exception as e:
134+
print("gap_unpair_wrong_key_error:", type(e).__name__)
135+
136+
finally:
137+
ble.active(0)
138+
139+
140+
# Acting in central role.
141+
def instance1():
142+
multitest.next()
143+
try:
144+
# Connect to peripheral.
145+
print("gap_connect")
146+
ble.gap_connect(*BDADDR)
147+
conn_handle = wait_for_event(_IRQ_PERIPHERAL_CONNECT, TIMEOUT_MS)
148+
149+
# Discover characteristics (before pairing, doesn't need to be encrypted).
150+
ble.gattc_discover_characteristics(conn_handle, 1, 65535)
151+
value_handle = wait_for_event(_IRQ_GATTC_CHARACTERISTIC_RESULT, TIMEOUT_MS)
152+
wait_for_event(_IRQ_GATTC_CHARACTERISTIC_DONE, TIMEOUT_MS)
153+
154+
# Pair with the peripheral.
155+
print("gap_pair")
156+
ble.gap_pair(conn_handle)
157+
158+
# Wait for the pairing event.
159+
wait_for_event(_IRQ_ENCRYPTION_UPDATE, TIMEOUT_MS)
160+
161+
# Read the peripheral's characteristic, should be encrypted.
162+
print("gattc_read")
163+
ble.gattc_read(conn_handle, value_handle)
164+
wait_for_event(_IRQ_GATTC_READ_RESULT, TIMEOUT_MS)
165+
166+
multitest.next()
167+
168+
# Disconnect from the peripheral.
169+
print("gap_disconnect:", ble.gap_disconnect(conn_handle))
170+
wait_for_event(_IRQ_PERIPHERAL_DISCONNECT, TIMEOUT_MS)
171+
172+
# Test gap_unpair on central side too
173+
print("central_gap_unpair_test")
174+
print("central_bond_keys_captured:", len(bond_keys))
175+
176+
# Test gap_unpair with captured bond keys on central side
177+
if bond_keys:
178+
for i, key in enumerate(bond_keys):
179+
try:
180+
result = ble.gap_unpair(key)
181+
print(f"central_gap_unpair_key_{i}_result:", result)
182+
except Exception as e:
183+
print(f"central_gap_unpair_key_{i}_error:", type(e).__name__, str(e))
184+
else:
185+
print("central_gap_unpair_no_keys")
186+
187+
finally:
188+
ble.active(0)
189+
190+
191+
ble = bluetooth.BLE()
192+
ble.config(mitm=True, le_secure=True, bond=True) # Enable bonding for unpair test
193+
ble.active(1)
194+
ble.irq(irq)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--- instance0 ---
2+
gap_advertise
3+
_IRQ_CENTRAL_CONNECT
4+
_IRQ_GET_SECRET key: 0
5+
_IRQ_GET_SECRET key: 0
6+
_IRQ_GET_SECRET key: 0
7+
_IRQ_SET_SECRET key: <memoryview>
8+
_IRQ_GET_SECRET key: 0
9+
_IRQ_SET_SECRET key: <memoryview>
10+
_IRQ_GET_SECRET key: 0
11+
_IRQ_ENCRYPTION_UPDATE 1 0 1
12+
_IRQ_GATTS_READ_REQUEST
13+
_IRQ_CENTRAL_DISCONNECT
14+
gap_unpair_test
15+
bond_keys_captured: 3
16+
gap_unpair_key_0_result: None
17+
gap_unpair_key_1_result: None
18+
gap_unpair_key_2_result: None
19+
gap_unpair_fake_key_error: ValueError
20+
gap_unpair_wrong_key_error: ValueError
21+
--- instance1 ---
22+
gap_connect
23+
_IRQ_PERIPHERAL_CONNECT
24+
_IRQ_GATTC_CHARACTERISTIC_RESULT UUID('00000000-1111-2222-3333-444444444444')
25+
_IRQ_GATTC_CHARACTERISTIC_DONE
26+
gap_pair
27+
_IRQ_GET_SECRET key: 0
28+
_IRQ_ENCRYPTION_UPDATE 1 0 1
29+
_IRQ_SET_SECRET key: <memoryview>
30+
_IRQ_GET_SECRET key: 0
31+
_IRQ_SET_SECRET key: <memoryview>
32+
_IRQ_GET_SECRET key: 0
33+
gattc_read
34+
_IRQ_GATTC_READ_RESULT b'encrypted'
35+
gap_disconnect: True
36+
_IRQ_PERIPHERAL_DISCONNECT
37+
central_gap_unpair_test
38+
central_bond_keys_captured: 3
39+
central_gap_unpair_key_0_result: None
40+
central_gap_unpair_key_1_result: None
41+
central_gap_unpair_key_2_result: None

0 commit comments

Comments
 (0)