Thanks to visit codestin.com
Credit goes to github.com

Skip to content

aioble/security: Control order of bond database and allow limiting of the number of pairs. #448

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

Closed
wants to merge 8 commits into from
20 changes: 17 additions & 3 deletions micropython/bluetooth/aioble/aioble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,15 @@ class DeviceConnection:
_connected = {}

def __init__(self, device):
self.device = device
self.device: Device = device
device._connection = self

self.encrypted = False
self.authenticated = False
self.bonded = False
self.key_size = False
self.mtu = None
self.pairing_in_progress = False

self._conn_handle = None

Expand Down Expand Up @@ -255,14 +256,26 @@ async def pair(self, *args, **kwargs):

await pair(self, *args, **kwargs)

def unpair(self):
from .security import unpair
ident = bytearray(7)
ident[0] = self.device.addr_type
ident[1:] = bytes(reversed(self.device.addr))
unpair(ident)

def indicate_service_changed(self):
handle = self._conn_handle
if handle is not None:
ble.gap_indicate_service_changed(handle)

def is_connected(self):
return self._conn_handle is not None

# Use with `with` to simplify disconnection and timeout handling.
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")

Expand All @@ -271,7 +284,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.
Expand Down
168 changes: 139 additions & 29 deletions micropython/bluetooth/aioble/aioble/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import uasyncio as asyncio
import binascii
import json

from . import core
from .core import log_info, log_warn, ble, register_irq_handler
from .device import DeviceConnection
from .device import DeviceConnection, Device

_IRQ_ENCRYPTION_UPDATE = const(28)
_IRQ_GET_SECRET = const(29)
Expand All @@ -26,27 +26,58 @@

_DEFAULT_PATH = "ble_secrets.json"

# Maintain list of known keys, newest at the bottom / end.
_secrets = {}
_modified = False
_path = None

# If set, limit the pairing db to this many peers
limit_peers = None

SEC_TYPES_SELF = (10, )
SEC_TYPES_PEER = (1, 2, 3, 4)


# Must call this before stack startup.
def load_secrets(path=None):
global _path, _secrets
global _path, _secrets, limit_peers

# Use path if specified, otherwise use previous path, otherwise use
# default path.
_path = path or _path or _DEFAULT_PATH

# Reset old secrets.
_secrets = {}
_secrets.clear()
try:
with open(_path, "r") as f:
entries = json.load(f)
# Newest entries at at the end, load them first
for sec_type, key, value in entries:
if sec_type not in _secrets:
_secrets[sec_type] = []
# Decode bytes from hex.
_secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
_secrets[sec_type].append((binascii.a2b_base64(key), binascii.a2b_base64(value)))

if limit_peers:
# If we need to limit loaded keys, ensure the same addresses of each type are loaded
keep_keys = None
for sec_type in SEC_TYPES_PEER:
if sec_type not in _secrets:
continue
secrets = _secrets[sec_type]
if len(secrets) > limit_peers:
if not keep_keys:
keep_keys = [key for key, _ in secrets[-limit_peers:]]
log_warn("Limiting keys to", keep_keys)

keep_entries = [entry for entry in secrets if entry[0] in keep_keys]
while len(keep_entries) < limit_peers:
for entry in reversed(secrets):
if entry not in keep_entries:
keep_entries.append(entry)
_secrets[sec_type] = keep_entries
_log_peers("loaded")

except:
log_warn("No secrets available")

Expand All @@ -61,53 +92,120 @@ def _save_secrets(arg=None):
# Only save if the secrets changed.
return

_log_peers('save_secrets')

with open(_path, "w") as f:
# 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()
for sec_type in _secrets for key, value in _secrets[sec_type]
]
json.dump(json_secrets, f)
_modified = False


def _remove_entry(sec_type, key):
secrets = _secrets[sec_type]

# Delete existing secrets matching the type and key.
deleted = False
for to_delete in [
entry for entry in secrets if entry[0] == key
]:
log_info("Removing existing secret matching key")
secrets.remove(to_delete)
deleted = True

return deleted


def _log_peers(heading=""):
if core.log_level <= 2:
return
log_info("secrets:", heading)
for sec_type in SEC_TYPES_PEER:
log_info("-", sec_type)

if sec_type not in _secrets:
continue
secrets = _secrets[sec_type]
for key, value in secrets:
log_info(" - %s: %s..." % (key, value[0:16]))


def _get_connection(key) -> DeviceConnection:
if not key:
return None
addr = bytes(reversed(key[-6:]))
for connection in DeviceConnection._connected.values():
if connection.device.addr == addr:
return connection


def _security_irq(event, data):
global _modified

if event == _IRQ_ENCRYPTION_UPDATE:
# Connection has updated (usually due to pairing).
conn_handle, encrypted, authenticated, bonded, key_size = data
log_info("encryption update", conn_handle, encrypted, authenticated, bonded, key_size)
log_info("encryption update - handle:", conn_handle,
"enc:", encrypted, "auth:", authenticated,
"bond:", bonded, "key:", key_size)
if connection := DeviceConnection._connected.get(conn_handle, None):
connection.encrypted = encrypted
connection.authenticated = authenticated
connection.bonded = bonded
connection.key_size = key_size
connection.pairing_in_progress = False
# TODO: Handle failure.
if encrypted and connection._pair_event:
connection._pair_event.set()

elif event == _IRQ_SET_SECRET:
sec_type, key, value = data
key = sec_type, bytes(key)
key = bytes(key)
value = bytes(value) if value else None

log_info("set secret:", key, value)

if value is None:
# Delete secret.
if key not in _secrets:
return False

del _secrets[key]
else:
# Save secret.
_secrets[key] = value
is_saving = value is not None
is_deleting = not is_saving

if core.log_level > 2:
if is_deleting:
log_info("del secret:", key)
else:
shortval = value
if len(value) > 16:
shortval = value[0:16] + b"..."
log_info("set secret:", sec_type, key, shortval)

if sec_type not in _secrets:
_secrets[sec_type] = []
secrets = _secrets[sec_type]

# Delete existing secrets matching the type and key.
removed = _remove_entry(sec_type, key)

if is_deleting and not removed:
# Delete mode, but no entries were deleted
return False

if is_saving:
# Save new secret.
if limit_peers and sec_type in SEC_TYPES_PEER and len(secrets) >= limit_peers:
addr, _ = secrets[0]
log_warn("Removing old peer to make space for new one")
ble.gap_unpair(addr)
log_info("Removed:", addr)
# Add new value to database
secrets.append((key, value))

_log_peers("set_secret")

# Queue up a save (don't synchronously write to flash).
_modified = True
schedule(_save_secrets, None)
if not _modified:
_modified = True
schedule(_save_secrets, None)

return True

Expand All @@ -116,19 +214,27 @@ def _security_irq(event, data):

log_info("get secret:", sec_type, index, bytes(key) if key else None)

secrets = _secrets.get(sec_type, [])
if key is None:
# Return the index'th secret of this type.
i = 0
for (t, _key), value in _secrets.items():
if t == sec_type:
if i == index:
return value
i += 1
# This is used when loading "all" secrets at startup
if len(secrets) > index:
key, val = secrets[index]
return val

return None
else:
# Return the secret for this key (or None).
key = sec_type, bytes(key)
return _secrets.get(key, None)
key = bytes(key)

if conn := _get_connection(key):
log_info("encryption / pairing started", conn)
conn.pairing_in_progress = True

for k, v in secrets:
if k == key:
return v
return None

elif event == _IRQ_PASSKEY_ACTION:
conn_handle, action, passkey = data
Expand All @@ -152,6 +258,10 @@ def _security_irq(event, data):
register_irq_handler(_security_irq)


# Use device.unpair() rather than calling this directly.
def unpair(addr):
ble.gap_unpair(addr)

# Use device.pair() rather than calling this directly.
async def pair(
connection,
Expand Down
17 changes: 11 additions & 6 deletions micropython/bluetooth/aioble/aioble/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down