diff --git a/micropython/bluetooth/aioble/aioble/device.py b/micropython/bluetooth/aioble/aioble/device.py index 9634f6d65..9d967c7cd 100644 --- a/micropython/bluetooth/aioble/aioble/device.py +++ b/micropython/bluetooth/aioble/aioble/device.py @@ -262,7 +262,7 @@ def is_connected(self): def timeout(self, timeout_ms): return DeviceTimeout(self, timeout_ms) - async def exchange_mtu(self, mtu=None): + async def exchange_mtu(self, mtu=None, timeout_ms=1000): if not self.is_connected(): raise ValueError("Not connected") @@ -271,7 +271,8 @@ async def exchange_mtu(self, mtu=None): self._mtu_event = self._mtu_event or asyncio.ThreadSafeFlag() ble.gattc_exchange_mtu(self._conn_handle) - await self._mtu_event.wait() + with self.timeout(timeout_ms): + await self._mtu_event.wait() return self.mtu # Wait for a connection on an L2CAP connection-oriented-channel. diff --git a/micropython/bluetooth/aioble/aioble/security.py b/micropython/bluetooth/aioble/aioble/security.py index 9ca4651d9..ab5fa0aa2 100644 --- a/micropython/bluetooth/aioble/aioble/security.py +++ b/micropython/bluetooth/aioble/aioble/security.py @@ -4,6 +4,7 @@ from micropython import const, schedule import uasyncio as asyncio import binascii +import ustruct import json from .core import log_info, log_warn, ble, register_irq_handler @@ -26,10 +27,14 @@ _DEFAULT_PATH = "ble_secrets.json" -_secrets = {} +# Maintain list of known keys, newest at the top. +_secrets = [] _modified = False _path = None +connected_sec = None +gatt_svc = None + # Must call this before stack startup. def load_secrets(path=None): @@ -40,13 +45,14 @@ def load_secrets(path=None): _path = path or _path or _DEFAULT_PATH # Reset old secrets. - _secrets = {} + _secrets = [] try: with open(_path, "r") as f: entries = json.load(f) - for sec_type, key, value in entries: + for sec_type, key, value, *digest in entries: + digest = digest[0] or None # Decode bytes from hex. - _secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value) + _secrets.append(((sec_type, binascii.a2b_base64(key)), binascii.a2b_base64(value), digest)) except: log_warn("No secrets available") @@ -65,15 +71,15 @@ def _save_secrets(arg=None): # Convert bytes to hex strings (otherwise JSON will treat them like # strings). json_secrets = [ - (sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value)) - for (sec_type, key), value in _secrets.items() + (sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value), digest) + for (sec_type, key), value, digest in _secrets ] json.dump(json_secrets, f) _modified = False def _security_irq(event, data): - global _modified + global _modified, connected_sec, gatt_svc if event == _IRQ_ENCRYPTION_UPDATE: # Connection has updated (usually due to pairing). @@ -88,6 +94,19 @@ def _security_irq(event, data): if encrypted and connection._pair_event: connection._pair_event.set() + if bonded and \ + None not in (gatt_svc, connected_sec) and \ + connected_sec[2] != gatt_svc.hexdigest: + gatt_svc.send_changed(connection) + + # Update the hash in the database + _secrets.remove(connected_sec) + updated_sec = connected_sec[:-1] + (gatt_svc.hexdigest,) + _secrets.insert(0, updated_sec) + # Queue up a save (don't synchronously write to flash). + _modified = True + schedule(_save_secrets, None) + elif event == _IRQ_SET_SECRET: sec_type, key, value = data key = sec_type, bytes(key) @@ -97,13 +116,15 @@ def _security_irq(event, data): if value is None: # Delete secret. - if key not in _secrets: - return False + for to_delete in [ + entry for entry in _secrets if entry[0] == key + ]: + _secrets.remove(to_delete) - del _secrets[key] else: # Save secret. - _secrets[key] = value + current_digest = gatt_svc.hexdigest if gatt_svc else None + _secrets.insert(0, (key, value, current_digest)) # Queue up a save (don't synchronously write to flash). _modified = True @@ -119,7 +140,7 @@ def _security_irq(event, data): if key is None: # Return the index'th secret of this type. i = 0 - for (t, _key), value in _secrets.items(): + for (t, _key), value, digest in _secrets: if t == sec_type: if i == index: return value @@ -128,7 +149,11 @@ def _security_irq(event, data): else: # Return the secret for this key (or None). key = sec_type, bytes(key) - return _secrets.get(key, None) + + for k, v, d in _secrets: + if k == key: + return v + return None elif event == _IRQ_PASSKEY_ACTION: conn_handle, action, passkey = data diff --git a/micropython/bluetooth/aioble/aioble/server.py b/micropython/bluetooth/aioble/aioble/server.py index 0aeb442c3..8f62599e3 100644 --- a/micropython/bluetooth/aioble/aioble/server.py +++ b/micropython/bluetooth/aioble/aioble/server.py @@ -83,11 +83,15 @@ def read(self): return ble.gatts_read(self._value_handle) # Write value to local db. - def write(self, data): + def write(self, data, send_update=False): if self._value_handle is None: self._initial = data else: - ble.gatts_write(self._value_handle, data) + if send_update: + # Send_update arg only added in 1.17, don't pass this arg unless required. + ble.gatts_write(self._value_handle, data, True) + else: + ble.gatts_write(self._value_handle, data) # Wait for a write on this characteristic. # Returns the device that did the write. @@ -186,10 +190,11 @@ def _indicate_done(conn_handle, value_handle, status): # Timeout. return # See TODO in __init__ to support multiple concurrent indications. - assert connection == characteristic._indicate_connection - characteristic._indicate_status = status - characteristic._indicate_event.set() - + if connection == characteristic._indicate_connection: + characteristic._indicate_status = status + characteristic._indicate_event.set() + else: + log_warn("Received indication for unexpected connection") class BufferedCharacteristic(Characteristic): def __init__(self, service, uuid, max_len=20, append=False): @@ -223,9 +228,13 @@ def __init__(self, characteristic, uuid, read=False, write=False, initial=None): # Turn the Service/Characteristic/Descriptor classes into a registration tuple # and then extract their value handles. -def register_services(*services): +def register_services(*services, include_gatt_svc=True): ensure_active() _registered_characteristics.clear() + if include_gatt_svc: + from .services.generic_attribute_service import GenericAttributeService + gatt_svc = GenericAttributeService(services) + services = (gatt_svc,) + services handles = ble.gatts_register_services(tuple(s._tuple() for s in services)) for i in range(len(services)): service_handles = handles[i] diff --git a/micropython/bluetooth/aioble/aioble/services/__init__.py b/micropython/bluetooth/aioble/aioble/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/micropython/bluetooth/aioble/aioble/services/generic_attribute_service.py b/micropython/bluetooth/aioble/aioble/services/generic_attribute_service.py new file mode 100644 index 000000000..3998e39c0 --- /dev/null +++ b/micropython/bluetooth/aioble/aioble/services/generic_attribute_service.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# @file +# @brief: Bluetooth Generic Attribute Service +# +# Copyright (c) 2021, Planet Innovation +# 436 Elgar Road, Box Hill, 3128, VIC, Australia +# Phone: +61 3 9945 7510 +# +# The copyright to the computer program(s) herein is the property of +# Planet Innovation, Australia. +# The program(s) may be used and/or copied only with the written permission +# of Planet Innovation or in accordance with the terms and conditions +# stipulated in the agreement/contract under which the program(s) have been +# supplied. +# + +import ustruct +import bluetooth +from aioble import Service, Characteristic, security +from aioble.core import ble, log_info +from hashlib import md5 +from ubinascii import hexlify +try: + from utyping import * +except: + pass + + +class GenericAttributeService(Service): + # Generic Attribute service UUID + SERVICE_UUID = bluetooth.UUID(0x1801) + + # Service Changed Characteristic + UUID_SERVICE_CHANGED = bluetooth.UUID(0x2A05) + # Database Hash Characteristic (New in BLE 5.1) + UUID_DATABASE_HASH = bluetooth.UUID(0x2B2A) + + def __init__(self, services: Tuple[Service]): + + super().__init__(self.SERVICE_UUID) + + # Database hash is typically a 128bit AES-CMAC value, however + # is generally only monitored for change as an opaque value. + # MD5 is also 128 bit, faster and builtin + hasher = md5() + for service in services: + for char in service.characteristics: + hasher.update(char.uuid) + hasher.update(str(char.flags)) + self.digest = hasher.digest() + self.hexdigest = hexlify(self.digest).decode() + log_info("BLE: DB Hash=", self.hexdigest) + security.current_digest = self.hexdigest + security.gatt_svc = self + + self.SERVICE_CHANGED = Characteristic( + service=self, + uuid=self.UUID_SERVICE_CHANGED, + read=True, + indicate=True, + initial='' + ) + + self.DATABASE_HASH = Characteristic( + service=self, + uuid=self.UUID_DATABASE_HASH, + read=True, + initial=self.digest + ) + + def send_changed(self, connection, start=0, end=0xFFFF): + self.SERVICE_CHANGED.write(ustruct.pack('!HH', start, end)) + log_info("Indicate Service Changed") + ble.gatts_indicate(connection._conn_handle, self.SERVICE_CHANGED._value_handle)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: