diff --git a/docs/library/bluetooth.rst b/docs/library/bluetooth.rst index b09c370abd46d..f5c5fa979d7a4 100644 --- a/docs/library/bluetooth.rst +++ b/docs/library/bluetooth.rst @@ -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* diff --git a/extmod/btstack/modbluetooth_btstack.c b/extmod/btstack/modbluetooth_btstack.c index 7694a1874f40b..26fc546f1ac28 100644 --- a/extmod/btstack/modbluetooth_btstack.c +++ b/extmod/btstack/modbluetooth_btstack.c @@ -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; diff --git a/extmod/modbluetooth.c b/extmod/modbluetooth.c index ffa407809aa71..d5daab5f4ae41 100644 --- a/extmod/modbluetooth.c +++ b/extmod/modbluetooth.c @@ -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]); @@ -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 diff --git a/extmod/modbluetooth.h b/extmod/modbluetooth.h index 24f063fa5d617..6218f1f366576 100644 --- a/extmod/modbluetooth.h +++ b/extmod/modbluetooth.h @@ -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 diff --git a/extmod/nimble/modbluetooth_nimble.c b/extmod/nimble/modbluetooth_nimble.c index 5e7030e36fab4..b31a6ee15e62c 100644 --- a/extmod/nimble/modbluetooth_nimble.c +++ b/extmod/nimble/modbluetooth_nimble.c @@ -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}; diff --git a/tests/multi_bluetooth/ble_gap_unpair.py b/tests/multi_bluetooth/ble_gap_unpair.py new file mode 100644 index 0000000000000..c9e57b5a683c2 --- /dev/null +++ b/tests/multi_bluetooth/ble_gap_unpair.py @@ -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) diff --git a/tests/multi_bluetooth/ble_gap_unpair.py.exp b/tests/multi_bluetooth/ble_gap_unpair.py.exp new file mode 100644 index 0000000000000..72f3b5fc4eb75 --- /dev/null +++ b/tests/multi_bluetooth/ble_gap_unpair.py.exp @@ -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: +_IRQ_GET_SECRET key: 0 +_IRQ_SET_SECRET key: +_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: +_IRQ_GET_SECRET key: 0 +_IRQ_SET_SECRET key: +_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 \ No newline at end of file