diff --git a/bluetooth/ble_secure_temp_sensor/CMakeLists.txt b/bluetooth/ble_secure_temp_sensor/CMakeLists.txt index 64db420ac..1cc4636d3 100644 --- a/bluetooth/ble_secure_temp_sensor/CMakeLists.txt +++ b/bluetooth/ble_secure_temp_sensor/CMakeLists.txt @@ -1,24 +1,6 @@ -# Select a security setting to explore the BLE security -# -# security setting 0: Just works (pairing), no MITM (Man In The Middle) protection -# client and server have no input or output support -# -# security setting 1: Numeric comparison with MITM protection -# client can query yes or no from the user, server has a display only -# server displays passkey -# client displays passkey and user can select Yes or No if they agree the passkey is from the server -# -# security setting 2: -# client has a keyboard and display, server has a display only -# server displays passkey -# client user enters the passkey displayed by the server -# -# security setting 3: -# client has a display only, server has a display and keyboard -# Client displays passkey -# server user enters the passkey displayed by the server +# Select a security setting to explore the BLE security. See README.md for details if (NOT DEFINED SECURITY_SETTING) - set(SECURITY_SETTING 1) + set(SECURITY_SETTING 0) endif() # Standalone example that reads from the on board temperature sensor and sends notifications via BLE diff --git a/bluetooth/ble_secure_temp_sensor/README.md b/bluetooth/ble_secure_temp_sensor/README.md index 9fa185202..3df1dc7dd 100644 --- a/bluetooth/ble_secure_temp_sensor/README.md +++ b/bluetooth/ble_secure_temp_sensor/README.md @@ -1,28 +1,137 @@ -### Secure temp sensor +# Secure temp sensor -This example uses BLE to communicate temperature between a pair of pico Ws. This example is a variant of temp sensor, using LE secure to provide a secure connection. +This example uses BLE to communicate temperature between a pair of Pico Ws. It is a variant of +the temp sensor example, using LE Secure Connections to provide a secure connection. -secure_temp_server is a peripheral or server that transmits its temperature to another device -secure_temp_client is a client that reads a temperature from another device +`secure_temp_server` is a peripheral/server that transmits its temperature to another device. +`secure_temp_client` is a central/client that reads temperature from another device. -In server.c and client.c there is a define SECURITY_SETTING which you can change to explore different security options: +## Security settings -security setting 0: Just works (pairing), no MITM (Man In The Middle) protection - client and server have no input or output support +In `server.c` and `client.c` there is a `SECURITY_SETTING` define which you can change to explore +different BLE security options. Both ends must be built with the same setting unless you are +deliberately testing an asymmetric combination (see the table below). -security setting 1: Numeric comparison with MITM protection - client can query yes or no from the user, server has a display only - server displays passkey - client displays passkey and user can select Yes or No if they agree the passkey is from the server +The settings map to Bluetooth IO capabilities as follows: -security setting 2: - client has a keyboard and display, server has a display only - server displays passkey - client user enters the passkey displayed by the server +| Setting | IO capability | Description | +|---------|--------------|-------------| +| 0 | `NO_INPUT_NO_OUTPUT` | Just Works - no MITM protection | +| 1 | `DISPLAY_YES_NO` | Numeric Comparison - MITM protection | +| 2 | `KEYBOARD_DISPLAY` | Passkey Entry - MITM protection | +| 3 | `DISPLAY_ONLY` | Display Only - MITM protection | -security setting 3: - client has a display only, server has a display and keyboard - Client displays passkey - server user enters the passkey displayed by the server +The actual pairing method used depends on the IO capabilities of *both* devices, not just one. +The Bluetooth SIG defines a matrix of initiator × responder capabilities that determines the +method. Setting `SM_AUTHREQ_MITM_PROTECTION` requests MITM protection but does not guarantee it — +if the negotiated method cannot provide it, pairing will fail. -You will need to use the console with both devices to see the passkeys and answer security prompts. Both stdio over UART and USB are enabled so you can use either. +### Working combinations + +| Client setting | Server setting | Pairing method | MITM? | +|---------------|---------------|----------------|-------| +| 0 | 0 | Just Works | No | +| 1 | 1 | Numeric Comparison | Yes | +| 2 | 2 | Passkey Entry | Yes | +| 2 | 3 | Passkey Display (server displays, client types) | Yes | +| 3 | 2 | Passkey Display (client displays, server types) | Yes | +| 3 | 3 | Fails (both display only, nobody can type) | — | +| 0 | >0 | Fails (MITM required but not achievable) | — | + +Settings 0, 1 and 2 work symmetrically with the same setting on both ends. Setting 3 is +`DISPLAY_ONLY` so it can only achieve MITM protection when paired with setting 2 on the other end. + +You will need a console on each device to see passkeys and answer prompts. Both stdio over UART +and USB are enabled so you can use either. + +## Support scripts + +Python scripts are provided to make it easier to test with just one Pico W. + +> **Note:** Run these scripts on a native Linux host or a Raspberry Pi. WSL2 with a +> usbip-attached Bluetooth adapter can connect and perform GATT discovery, but pairing +> fails during the LE Secure Connections exchange (the DHKey check fails with +> authentication failure / reason 12). This is a limitation of the WSL2 + usbip + BlueZ +> path, not the example code. + +### Client (`ble_temp_client.py`) + +Acts as a BLE central, connecting to a Pico W running `secure_temp_server`. + +``` +pip install bleak +python3 ble_temp_client.py [--security <0-3>] +``` + +- Works with BlueZ running normally — no special setup required. +- If connection fails with a disconnect during service discovery, clear stale bonding info: + ``` + bluetoothctl remove + ``` + The Pico clears its own bond automatically on key mismatch, but BlueZ needs to be told manually. + +### Server (`ble_temp_server.py`) + +Acts as a BLE peripheral, advertising a simulated temperature for a Pico W running +`secure_temp_client` to connect to. + +Uses [Bumble](https://github.com/google/bumble) which talks directly over HCI, bypassing BlueZ. + +``` +pip install bumble +``` + +#### Using a USB Bluetooth dongle (recommended) + +The easiest option — the dongle is claimed by Bumble leaving the built-in adapter free for BlueZ +and the client script. Find the transport ID with: + +``` +python3 -m bumble.apps.usb_probe +``` + +Then run: + +``` +./ble_temp_server.py --usb [--security <0-3>] +``` + +#### Using the built-in adapter + +BlueZ must be stopped first to release the adapter: + +``` +sudo systemctl stop bluetooth +sudo systemctl mask bluetooth +``` + +On Raspberry Pi OS, grant `cap_net_admin` to the Python binary so it can open the HCI socket +without running as root: + +``` +sudo setcap cap_net_admin+eip $(readlink -f venv/bin/python3) +``` + +Then run: + +``` +./ble_temp_server.py --builtin [--security <0-3>] +``` + +When done, restore BlueZ: + +``` +sudo systemctl unmask bluetooth +sudo systemctl start bluetooth +``` + +### Security mode prompts + +For settings 1-3 the scripts will prompt for interaction during pairing: + +- **Setting 1 (Numeric Comparison)**: The Pico displays a number; confirm it matches when prompted. +- **Setting 2 (Passkey Entry)**: The server displays a passkey; type it into the Pico console. +- **Setting 3 (Display Only)**: The Pico displays a passkey; type it when the script prompts. + +For asymmetric combinations (client=2, server=3 or client=3, server=2) the above still applies — +one side displays and the other types, determined by who has `KEYBOARD_DISPLAY` capability. \ No newline at end of file diff --git a/bluetooth/ble_secure_temp_sensor/ble_temp_client.py b/bluetooth/ble_secure_temp_sensor/ble_temp_client.py new file mode 100755 index 000000000..6f084db54 --- /dev/null +++ b/bluetooth/ble_secure_temp_sensor/ble_temp_client.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +BLE test client for the secure_picow_temp example. + +Scans for a Pico W running the secure_picow_temp server, connects, pairs, +and prints temperature notifications until Ctrl-C. + +Requirements: + pip install bleak + +Pairing (including any passkey entry or numeric comparison) is handled by the +host OS / BlueZ agent according to the security level the Pico requests, so no +security option is needed here - just follow any prompts from your OS. + +Depending on the security setting the Pico server was built with, expect: + 0 - Just Works: no passkey interaction needed. + 1 - Numeric Comparison: the Pico displays a number on its serial console; + your OS asks you to confirm it matches. + 2 - Passkey Entry: the Pico displays a 6-digit passkey on its serial + console; enter it when prompted by your OS. + 3 - Passkey Display: your OS displays a passkey; enter it on the Pico's + serial console. + +Usage: + python ble_temp_client.py [--scan-timeout ] [--debug] + +Tips: + - If you get a disconnect during service discovery, clear stale bonding info: + bluetoothctl remove + The Pico clears its own bond automatically on key mismatch. + - The Pico advertises as "Pico " not "secure_picow_temp"; the latter + is the GATT device name only readable after connecting. +""" + +import argparse +import asyncio +import logging +import struct +import sys + +try: + from bleak import BleakClient, BleakScanner + from bleak.backends.characteristic import BleakGATTCharacteristic +except ImportError: + print("ERROR: bleak is not installed. Run: pip install bleak") + sys.exit(1) + +# --------------------------------------------------------------------------- +# Constants matching the Pico example +# --------------------------------------------------------------------------- + +# Environmental Sensing Service (0x181A) and Temperature characteristic (0x2A6E) +ENVIRONMENTAL_SENSING_SERVICE_UUID = "0000181a-0000-1000-8000-00805f9b34fb" +TEMPERATURE_CHARACTERISTIC_UUID = "00002a6e-0000-1000-8000-00805f9b34fb" + +# Temperature is a signed 16-bit integer in units of 0.01 degC (Bluetooth SIG spec). +# The Pico stores current_temp = deg_c * 100 as uint16_t, which matches. +TEMP_SCALE = 100.0 + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def decode_temperature(data: bytes) -> float: + """Decode a 2-byte little-endian temperature value (units: 0.01 degC).""" + if len(data) != 2: + raise ValueError(f"Expected 2 bytes for temperature, got {len(data)}") + raw = struct.unpack_from(" None: + """Called each time the server sends a temperature notification.""" + try: + temp = decode_temperature(bytes(data)) + log.info("Temperature notification: %.2f degC (raw: %s)", temp, data.hex()) + except ValueError as exc: + log.error("Failed to decode notification: %s raw=%s", exc, data.hex()) + + +# --------------------------------------------------------------------------- +# Main client coroutine +# --------------------------------------------------------------------------- + +async def run(timeout: float) -> None: + # The Pico advertises the Environmental Sensing service UUID (0x181A). + # The advertised name is "Pico " so we match on service UUID, + # with a name-prefix fallback in case BlueZ does not surface the UUID. + log.info("Scanning for Environmental Sensing service (0x181A) or name prefix 'Pico '...") + + ESS_UUID_SHORT = "181a" + + def matches_pico(d, adv) -> bool: + by_uuid = any( + str(u).lower().replace("-", "").endswith(ESS_UUID_SHORT) + for u in adv.service_uuids + ) + by_name = (d.name or "").startswith("Pico ") + if d.name or adv.service_uuids: + log.debug(" Seen: %s name=%r uuids=%s by_uuid=%s by_name=%s", + d.address, d.name, [str(u) for u in adv.service_uuids], + by_uuid, by_name) + return by_uuid or by_name + + device = await BleakScanner.find_device_by_filter(matches_pico, timeout=timeout) + + if device is None: + log.error( + "No device found within %.0f s. Make sure the Pico W server is " + "running and advertising. Re-run with --debug to see all visible devices.", + timeout, + ) + return + + log.info("Found device: %s [%s]", device.name, device.address) + + def disconnected_callback(client: BleakClient) -> None: + log.warning("Device disconnected.") + + # pair_before_connect=True is essential: the BlueZ backend runs service + # discovery automatically inside connect(), before we can call pair() + # manually. The Temperature characteristic requires ENCRYPTION_KEY_SIZE_16, + # so the Pico rejects unencrypted ATT traffic and BlueZ drops the connection + # mid-discovery unless pairing is completed first. + log.info("Connecting and pairing...") + async with BleakClient(device, timeout=10.0, + pair_before_connect=True, + disconnected_callback=disconnected_callback) as client: + log.info("Connected and paired.") + + # Initial read to confirm the encrypted link is working before writing + # the CCCD. BTstack silently rejects ATT writes on insufficiently + # secure links, so doing a read first ensures we are definitely encrypted. + try: + data = await client.read_gatt_char(TEMPERATURE_CHARACTERISTIC_UUID) + log.info("Initial read: %.2f degC", decode_temperature(bytes(data))) + except Exception as exc: + log.warning("Initial read failed: %s", exc) + + log.info("Subscribing to temperature notifications...") + await client.start_notify(TEMPERATURE_CHARACTERISTIC_UUID, notification_handler) + log.info("Subscribed. Listening for notifications - press Ctrl-C to stop.") + + try: + while True: + await asyncio.sleep(5) + if not client.is_connected: + log.warning("Connection lost.") + break + except asyncio.CancelledError: + pass + finally: + if client.is_connected: + await client.stop_notify(TEMPERATURE_CHARACTERISTIC_UUID) + log.info("Unsubscribed and disconnecting.") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="BLE client for the secure_picow_temp example.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--scan-timeout", type=float, default=30.0, + help="Seconds to scan before giving up. Default: 30.", + ) + parser.add_argument( + "--debug", action="store_true", + help="Print every BLE advertisement seen during the scan.", + ) + args = parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + try: + asyncio.run(run(timeout=args.scan_timeout)) + except KeyboardInterrupt: + log.info("Interrupted - goodbye.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/bluetooth/ble_secure_temp_sensor/ble_temp_server.py b/bluetooth/ble_secure_temp_sensor/ble_temp_server.py new file mode 100755 index 000000000..07b0bc7b2 --- /dev/null +++ b/bluetooth/ble_secure_temp_sensor/ble_temp_server.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +""" +BLE test server for the secure_picow_temp example. + +Advertises an Environmental Sensing GATT service with a simulated temperature +characteristic (slowly varying sine wave), allowing the Pico W client to +connect and receive notifications. + +Uses Bumble (https://github.com/google/bumble) which talks directly over HCI, +bypassing BlueZ entirely. BlueZ must be stopped before running this script: + + sudo systemctl stop bluetooth + python ble_temp_server.py [--transport usb:0] + sudo systemctl start bluetooth # when done + +To find the right transport index for your adapter: + python -m bumble.apps.usb_probe + +Requirements: + pip install bumble + +Security settings (must match the SECURITY_SETTING the Pico client was built with): + 0 - Just Works (NO_INPUT_NO_OUTPUT, no MITM) + 1 - Numeric Comparison (DISPLAY_YES_NO, MITM) + 2 - Keyboard + Display (KEYBOARD_DISPLAY, MITM) + 3 - Display Only (DISPLAY_ONLY, MITM) + +Usage: + python ble_temp_server.py [--security <0-3>] [--temp ] + [--interval ] [--transport ] +""" + +import argparse +import asyncio +import logging +import math +import struct +import sys + +try: + from bumble.device import Device, Connection + from bumble.hci import Address + from bumble.transport import open_transport + from bumble.gatt import ( + Service, + Characteristic, + CharacteristicValue, + GATT_ENVIRONMENTAL_SENSING_SERVICE, + ) + from bumble.att import UUID + from bumble.core import AdvertisingData + from bumble.pairing import PairingConfig, PairingDelegate +except ImportError as e: + print(f"ERROR: failed to import bumble: {e}") + print("If bumble is installed, check you are using the right Python/venv.") + sys.exit(1) + +# Temperature characteristic UUID (0x2A6E) - not a named constant in bumble +GATT_TEMPERATURE_CHARACTERISTIC = UUID.from_16_bits(0x2A6E, 'Temperature') + +# --------------------------------------------------------------------------- +# Constants matching the Pico example +# --------------------------------------------------------------------------- + +SERVER_DEVICE_NAME = "secure_picow_temp" + +# Temperature is a signed 16-bit integer in units of 0.01 degC (Bluetooth SIG spec). +# The Pico client expects current_temp = deg_c * 100 as uint16_t, which matches. +TEMP_SCALE = 100.0 + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + +# Suppress noisy bumble internal warnings that fire harmlessly during shutdown +logging.getLogger("bumble.device").setLevel(logging.ERROR) +logging.getLogger("bumble.host").setLevel(logging.ERROR) + +# --------------------------------------------------------------------------- +# Security configuration +# --------------------------------------------------------------------------- + +class DisplayDelegate(PairingDelegate): + """PairingDelegate that displays a passkey to the user.""" + + def __init__(self): + super().__init__(io_capability=PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY) + + async def display_number(self, number: int, digits: int) -> None: + log.info("Passkey: %0*d (enter this on the client)", digits, number) + + +class KeyboardDelegate(PairingDelegate): + """PairingDelegate that prompts the user to enter a passkey from stdin.""" + + def __init__(self, io_capability=PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY): + super().__init__(io_capability=io_capability) + + async def get_number(self) -> int | None: + # Run the blocking input() call in a thread so we don't block the event loop. + loop = asyncio.get_event_loop() + try: + passkey_str = await loop.run_in_executor( + None, lambda: input("Enter passkey shown by the client: ") + ) + return int(passkey_str.strip()) + except (ValueError, EOFError): + log.warning("Invalid passkey input - declining pairing.") + return None + + +def make_pairing_config(security: int) -> PairingConfig: + """Return a Bumble PairingConfig matching the given security setting.""" + # All modes use Secure Connections (sc=True) and bonding, matching the + # sm_set_secure_connections_only_mode(true) call in the Pico client. + common = dict(sc=True, bonding=True) + if security == 0: + # Just Works: no I/O capability, no MITM + return PairingConfig(mitm=False, delegate=PairingDelegate( + io_capability=PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT, + ), **common) + elif security == 1: + # Numeric Comparison: server displays, client confirms yes/no + return PairingConfig(mitm=True, delegate=PairingDelegate( + io_capability=PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT, + ), **common) + elif security == 2: + # Keyboard + Display: matches IO_CAPABILITY_KEYBOARD_DISPLAY on the Pico. + # Pairing method depends on the other side's capabilities. + return PairingConfig(mitm=True, delegate=KeyboardDelegate( + io_capability=PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT, + ), **common) + else: + # security == 3: Display Only - matches IO_CAPABILITY_DISPLAY_ONLY on the Pico. + # Requires the Pico client to be built with SECURITY_SETTING=2 (KEYBOARD_DISPLAY) + # so it can enter the passkey we display here. + return PairingConfig(mitm=True, delegate=DisplayDelegate(), **common) + +# --------------------------------------------------------------------------- +# Main server coroutine +# --------------------------------------------------------------------------- + +async def run(temp_celsius: float, interval: float, security: int, + transport: str) -> None: + log.info("Base temperature: %.2f degC, notification interval: %.1f s, " + "security setting: %d, transport: %s", + temp_celsius, interval, security, transport) + + # Shared mutable temperature value, updated by the notification loop and + # read by the GATT read handler. + current_raw = [int(round(temp_celsius * TEMP_SCALE))] + + def read_temperature(_connection): + return struct.pack(" None: + parser = argparse.ArgumentParser( + description="BLE server for the secure_picow_temp example.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--temp", "-t", type=float, default=21.0, + help="Base temperature in degC. A +/-2 degC sine wave is added. Default: 21.0.", + ) + parser.add_argument( + "--interval", "-i", type=float, default=10.0, + help="Seconds between notifications. Default: 10 (matches the Pico server).", + ) + parser.add_argument( + "--security", "-s", type=int, default=0, choices=[0, 1, 2, 3], + help="Security setting (must match the Pico client build). Default: 0.", + ) + transport_group = parser.add_mutually_exclusive_group(required=True) + transport_group.add_argument( + "--usb", type=str, metavar="ID", + help=( + "Use a USB Bluetooth dongle as the adapter. " + "Find the ID with: python -m bumble.apps.usb_probe " + "(e.g. --usb 0 or --usb 2E8A:000C)" + ), + ) + transport_group.add_argument( + "--builtin", action="store_true", + help=( + "Use the built-in Bluetooth adapter via the kernel HCI socket. " + "Requires BlueZ to be stopped first: " + "sudo systemctl mask bluetooth && sudo systemctl stop bluetooth" + ), + ) + transport_group.add_argument( + "--transport", "-T", type=str, metavar="TRANSPORT", + help=( + "Advanced: specify a Bumble transport string directly " + "(e.g. hci-socket:0, usb:0, usb:2E8A:000C)." + ), + ) + args = parser.parse_args() + + if args.usb: + transport = f"usb:{args.usb}" + elif args.transport: + transport = args.transport + else: + transport = "hci-socket:0" + + try: + asyncio.run(run( + temp_celsius=args.temp, + interval=args.interval, + security=args.security, + transport=transport, + )) + except KeyboardInterrupt: + log.info("Interrupted - goodbye.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/bluetooth/ble_secure_temp_sensor/client.c b/bluetooth/ble_secure_temp_sensor/client.c index 36b331139..4fce158ba 100644 --- a/bluetooth/ble_secure_temp_sensor/client.c +++ b/bluetooth/ble_secure_temp_sensor/client.c @@ -38,7 +38,7 @@ static btstack_packet_callback_registration_t sm_event_callback_registration; static gc_state_t state = TC_OFF; static bd_addr_t server_addr; static bd_addr_type_t server_addr_type; -static hci_con_handle_t connection_handle; +static hci_con_handle_t con_handle; static gatt_client_service_t server_service; static gatt_client_characteristic_t server_characteristic; static bool listener_registered; @@ -65,15 +65,41 @@ static btstack_timer_source_t heartbeat; // Client generates and displays passkey // server user enters the passkey displayed by the server #ifndef SECURITY_SETTING -#define SECURITY_SETTING 1 +#error define SECURITY_SETTING #endif +static int choose_security(int setting) { + printf("Choose security in the next 5s (default %d)\n", setting); + printf("0: IO_CAPABILITY_NO_INPUT_NO_OUTPUT\n"); + printf("1: IO_CAPABILITY_DISPLAY_YES_NO and SM_AUTHREQ_MITM_PROTECTION\n"); + printf("2: IO_CAPABILITY_KEYBOARD_DISPLAY and SM_AUTHREQ_MITM_PROTECTION\n"); + printf("3: IO_CAPABILITY_DISPLAY_ONLY and SM_AUTHREQ_MITM_PROTECTION\n"); + printf("all are using SM_AUTHREQ_SECURE_CONNECTION\n"); + + int c = getchar_timeout_us(5000000); + if (c >= 0) { + if (c >= '0' && c <= '3') { + setting = c - '0'; + } else { + printf("Invalid input\n"); + } + } + printf("Using security setting %d\n", setting); + + return setting; +} + static void configure_security(int security_setting) { DEBUG_LOG("Security setting %u selected.\n", security_setting); sm_set_secure_connections_only_mode(true); switch (security_setting) { case 0: sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); + // Note: SM_AUTHREQ_BONDING is deliberately omitted. This means keys + // are not saved to persistent storage and the device re-pairs on every + // connection. This keeps the example simple and avoids flash wear and + // stale bonding issues during testing. Add SM_AUTHREQ_BONDING here and + // in the server if you want to persist bonds across power cycles. sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION); break; case 1: @@ -144,13 +170,13 @@ static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint att_status = gatt_event_query_complete_get_att_status(packet); if (att_status != ATT_ERROR_SUCCESS){ ERROR_LOG("SERVICE_QUERY_RESULT, ATT Error 0x%02x.\n", att_status); - gap_disconnect(connection_handle); + gap_disconnect(con_handle); break; } // service query complete, look for characteristic state = TC_W4_CHARACTERISTIC_RESULT; DEBUG_LOG("Search for env sensing characteristic.\n"); - gatt_client_discover_characteristics_for_service_by_uuid16(handle_gatt_client_event, connection_handle, &server_service, ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE); + gatt_client_discover_characteristics_for_service_by_uuid16(handle_gatt_client_event, con_handle, &server_service, ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE); break; default: break; @@ -166,16 +192,16 @@ static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint att_status = gatt_event_query_complete_get_att_status(packet); if (att_status != ATT_ERROR_SUCCESS){ ERROR_LOG("CHARACTERISTIC_QUERY_RESULT, ATT Error 0x%02x.\n", att_status); - gap_disconnect(connection_handle); + gap_disconnect(con_handle); break; } // register handler for notifications listener_registered = true; - gatt_client_listen_for_characteristic_value_updates(¬ification_listener, handle_gatt_client_event, connection_handle, &server_characteristic); + gatt_client_listen_for_characteristic_value_updates(¬ification_listener, handle_gatt_client_event, con_handle, &server_characteristic); // enable notifications DEBUG_LOG("Enable notify on characteristic.\n"); state = TC_W4_ENABLE_NOTIFICATIONS_COMPLETE; - gatt_client_write_client_characteristic_configuration(handle_gatt_client_event, connection_handle, + gatt_client_write_client_characteristic_configuration(handle_gatt_client_event, con_handle, &server_characteristic, GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION); break; default: @@ -223,7 +249,6 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p UNUSED(channel); bd_addr_t local_addr; if (packet_type != HCI_EVENT_PACKET) return; - hci_con_handle_t con_handle; uint8_t status; uint8_t event_type = hci_event_packet_get_type(packet); @@ -255,12 +280,12 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p switch (hci_event_le_meta_get_subevent_code(packet)) { case HCI_SUBEVENT_LE_CONNECTION_COMPLETE: if (state != TC_W4_CONNECT) return; - connection_handle = hci_subevent_le_connection_complete_get_connection_handle(packet); - // initialize gatt client context with handle, and add it to the list of active clients - // query primary services - DEBUG_LOG("Search for env sensing service.\n"); - state = TC_W4_SERVICE_RESULT; - gatt_client_discover_primary_services_by_uuid16(handle_gatt_client_event, connection_handle, ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING); + con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet); + // Note: GATT service discovery is NOT started here. The server requires + // ENCRYPTION_KEY_SIZE_16 so all ATT requests are rejected until the link + // is encrypted. Discovery is deferred to SM_EVENT_PAIRING_COMPLETE so + // that the link is always encrypted before any GATT traffic is sent. + DEBUG_LOG("Connection complete, waiting for pairing.\n"); break; default: break; @@ -296,7 +321,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p break; case HCI_EVENT_DISCONNECTION_COMPLETE: // unregister listener - connection_handle = HCI_CON_HANDLE_INVALID; + con_handle = HCI_CON_HANDLE_INVALID; if (listener_registered){ listener_registered = false; gatt_client_stop_listening_for_characteristic_value_updates(¬ification_listener); @@ -422,6 +447,10 @@ static void sm_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *pa switch (sm_event_pairing_complete_get_status(packet)){ case ERROR_CODE_SUCCESS: DEBUG_LOG("Pairing complete, success\n"); + // Now that the link is encrypted, start GATT service discovery. + DEBUG_LOG("Search for env sensing service.\n"); + state = TC_W4_SERVICE_RESULT; + gatt_client_discover_primary_services_by_uuid16(handle_gatt_client_event, con_handle, ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING); break; case ERROR_CODE_CONNECTION_TIMEOUT: ERROR_LOG("Pairing failed, timeout\n"); @@ -514,7 +543,8 @@ int main() { sm_add_event_handler(&sm_event_callback_registration); // apply security configuration settings - configure_security(SECURITY_SETTING); + int chosen_security_setting = choose_security(SECURITY_SETTING); + configure_security(chosen_security_setting); // set one-shot btstack timer heartbeat.process = &heartbeat_handler; diff --git a/bluetooth/ble_secure_temp_sensor/server.c b/bluetooth/ble_secure_temp_sensor/server.c index 42e9ac1eb..2778fad71 100644 --- a/bluetooth/ble_secure_temp_sensor/server.c +++ b/bluetooth/ble_secure_temp_sensor/server.c @@ -36,7 +36,7 @@ static uint8_t adv_data[] = { static const uint8_t adv_data_len = sizeof(adv_data); int le_notification_enabled; -static hci_con_handle_t con_handle; +static hci_con_handle_t con_handle = HCI_CON_HANDLE_INVALID; static uint16_t current_temp; extern uint8_t const profile_data[]; @@ -46,35 +46,43 @@ static btstack_timer_source_t heartbeat; static btstack_packet_callback_registration_t hci_event_callback_registration; static btstack_packet_callback_registration_t sm_event_callback_registration; -// Select a security setting to explore the BLE security -// -// security setting 0: Just works (pairing), no MITM (Man In The Middle) protection -// client and server have no input or output support -// -// security setting 1: Numeric comparison with MITM protection -// client can query yes or no from the user, server has a display only -// server generates and displays passkey -// client displays passkey and user can select Yes or No if they agree the passkey is from the server -// -// security setting 2: -// client has a keyboard and display, server has a display only -// server generates and displays passkey -// client user enters the passkey displayed by the server -// -// security setting 3: -// client has a display only, server has a display and keyboard -// Client generates and displays passkey -// server user enters the passkey displayed by the server +// Select a security setting to explore the BLE security. See README.md for details #ifndef SECURITY_SETTING -#define SECURITY_SETTING 1 +#error define SECURITY_SETTING #endif +static int choose_security(int setting) { + printf("Choose security in the next 5s (default %d)\n", setting); + printf("0: IO_CAPABILITY_NO_INPUT_NO_OUTPUT\n"); + printf("1: IO_CAPABILITY_DISPLAY_YES_NO and SM_AUTHREQ_MITM_PROTECTION\n"); + printf("2: IO_CAPABILITY_KEYBOARD_DISPLAY and SM_AUTHREQ_MITM_PROTECTION\n"); + printf("3: IO_CAPABILITY_DISPLAY_ONLY and SM_AUTHREQ_MITM_PROTECTION\n"); + printf("all are using SM_AUTHREQ_SECURE_CONNECTION\n"); + + int c = getchar_timeout_us(5000000); + if (c >= 0) { + if (c >= '0' && c <= '3') { + setting = c - '0'; + } else { + printf("Invalid input\n"); + } + } + printf("Using security setting %d\n", setting); + + return setting; +} + static void configure_security(int security_setting) { DEBUG_LOG("Security setting %u selected.\n", security_setting); sm_set_secure_connections_only_mode(true); switch (security_setting) { case 0: sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); + // Note: SM_AUTHREQ_BONDING is deliberately omitted. This means keys + // are not saved to persistent storage and the device re-pairs on every + // connection. This keeps the example simple and avoids flash wear and + // stale bonding issues during testing. Add SM_AUTHREQ_BONDING here and + // in the client if you want to persist bonds across power cycles. sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION); break; case 1: @@ -82,11 +90,11 @@ static void configure_security(int security_setting) { sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION); break; case 2: - sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY); + sm_set_io_capabilities(IO_CAPABILITY_KEYBOARD_DISPLAY); sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION); break; case 3: - sm_set_io_capabilities(IO_CAPABILITY_KEYBOARD_ONLY); + sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY); sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION); break; default: @@ -122,11 +130,20 @@ static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packe poll_temp(); + break; + case HCI_EVENT_META_GAP: + if (hci_event_gap_meta_get_subevent_code(packet) == GAP_SUBEVENT_LE_CONNECTION_COMPLETE) { + con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet); + INFO_LOG("Connection complete, con_handle=0x%04x\n", con_handle); + sm_request_pairing(con_handle); + } break; case HCI_EVENT_DISCONNECTION_COMPLETE: le_notification_enabled = 0; + con_handle = HCI_CON_HANDLE_INVALID; break; case ATT_EVENT_CAN_SEND_NOW: + INFO_LOG("Sending notification: temp=%d\n", current_temp); att_server_notify(con_handle, ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE_01_VALUE_HANDLE, (uint8_t*)¤t_temp, sizeof(current_temp)); break; default: @@ -184,22 +201,13 @@ static void sm_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *p if (packet_type != HCI_EVENT_PACKET) return; - hci_con_handle_t con_handle; bd_addr_t addr; bd_addr_type_t addr_type; uint8_t status; switch (hci_event_packet_get_type(packet)) { case HCI_EVENT_META_GAP: - switch (hci_event_gap_meta_get_subevent_code(packet)) { - case GAP_SUBEVENT_LE_CONNECTION_COMPLETE: - DEBUG_LOG("Connection complete\n"); - con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet); - sm_request_pairing(con_handle); - break; - default: - break; - } + // con_handle capture and sm_request_pairing handled in packet_handler break; case SM_EVENT_JUST_WORKS_REQUEST: INFO_LOG("Just Works requested\n"); @@ -324,10 +332,11 @@ static int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_h UNUSED(transaction_mode); UNUSED(offset); UNUSED(buffer_size); - + + INFO_LOG("ATT write: handle=0x%04x value=0x%04x\n", att_handle, little_endian_read_16(buffer, 0)); if (att_handle != ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE_01_CLIENT_CONFIGURATION_HANDLE) return 0; le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION; - con_handle = connection_handle; + INFO_LOG("Notifications %s\n", le_notification_enabled ? "enabled" : "disabled"); if (le_notification_enabled) { att_server_request_can_send_now_event(con_handle); } @@ -360,6 +369,7 @@ static void heartbeat_handler(struct btstack_timer_source *ts) { // Update the temp every 10s if (counter % 10 == 0) { poll_temp(); + INFO_LOG("Heartbeat: le_notification_enabled=%d con_handle=0x%04x\n", le_notification_enabled, con_handle); if (le_notification_enabled) { att_server_request_can_send_now_event(con_handle); } @@ -403,7 +413,8 @@ int main() { sm_add_event_handler(&sm_event_callback_registration); // apply security configuration settings - configure_security(SECURITY_SETTING); + int chosen_security_setting = choose_security(SECURITY_SETTING); + configure_security(chosen_security_setting); // register for ATT event att_server_register_packet_handler(packet_handler); @@ -432,4 +443,4 @@ int main() { } #endif return 0; -} +} \ No newline at end of file