Skip to content

Commit 25af2e3

Browse files
committed
aioble/security: Add option to limit number of peers stored.
1 parent 5b496e9 commit 25af2e3

File tree

1 file changed

+114
-25
lines changed

1 file changed

+114
-25
lines changed

micropython/bluetooth/aioble/aioble/security.py

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import asyncio
66
import binascii
77
import json
8-
8+
from . import core
99
from .core import log_info, log_warn, ble, register_irq_handler
1010
from .device import DeviceConnection
1111

@@ -26,27 +26,58 @@
2626

2727
_DEFAULT_PATH = "ble_secrets.json"
2828

29+
# Maintain list of known keys, newest at the bottom / end.
2930
_secrets = {}
3031
_modified = False
3132
_path = None
3233

34+
# If set, limit the pairing db to this many peers
35+
limit_peers = None
36+
37+
SEC_TYPES_SELF = (10, )
38+
SEC_TYPES_PEER = (1, 2, 3, 4)
39+
3340

3441
# Must call this before stack startup.
3542
def load_secrets(path=None):
36-
global _path, _secrets
43+
global _path, _secrets, limit_peers
3744

3845
# Use path if specified, otherwise use previous path, otherwise use
3946
# default path.
4047
_path = path or _path or _DEFAULT_PATH
4148

4249
# Reset old secrets.
43-
_secrets = {}
50+
_secrets.clear()
4451
try:
4552
with open(_path, "r") as f:
4653
entries = json.load(f)
54+
# Newest entries at at the end, load them first
4755
for sec_type, key, value in entries:
56+
if sec_type not in _secrets:
57+
_secrets[sec_type] = []
4858
# Decode bytes from hex.
49-
_secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
59+
_secrets[sec_type].append((binascii.a2b_base64(key), binascii.a2b_base64(value)))
60+
61+
if limit_peers:
62+
# If we need to limit loaded keys, ensure the same addresses of each type are loaded
63+
keep_keys = None
64+
for sec_type in SEC_TYPES_PEER:
65+
if sec_type not in _secrets:
66+
continue
67+
secrets = _secrets[sec_type]
68+
if len(secrets) > limit_peers:
69+
if not keep_keys:
70+
keep_keys = [key for key, _ in secrets[-limit_peers:]]
71+
log_warn("Limiting keys to", keep_keys)
72+
73+
keep_entries = [entry for entry in secrets if entry[0] in keep_keys]
74+
while len(keep_entries) < limit_peers:
75+
for entry in reversed(secrets):
76+
if entry not in keep_entries:
77+
keep_entries.append(entry)
78+
_secrets[sec_type] = keep_entries
79+
_log_peers("loaded")
80+
5081
except:
5182
log_warn("No secrets available")
5283

@@ -61,17 +92,48 @@ def _save_secrets(arg=None):
6192
# Only save if the secrets changed.
6293
return
6394

95+
_log_peers('save_secrets')
96+
6497
with open(_path, "w") as f:
6598
# Convert bytes to hex strings (otherwise JSON will treat them like
6699
# strings).
67100
json_secrets = [
68101
(sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value))
69-
for (sec_type, key), value in _secrets.items()
102+
for sec_type in _secrets for key, value in _secrets[sec_type]
70103
]
71104
json.dump(json_secrets, f)
72105
_modified = False
73106

74107

108+
def _remove_entry(sec_type, key):
109+
secrets = _secrets[sec_type]
110+
111+
# Delete existing secrets matching the type and key.
112+
deleted = False
113+
for to_delete in [
114+
entry for entry in secrets if entry[0] == key
115+
]:
116+
log_info("Removing existing secret matching key")
117+
secrets.remove(to_delete)
118+
deleted = True
119+
120+
return deleted
121+
122+
123+
def _log_peers(heading=""):
124+
if core.log_level <= 2:
125+
return
126+
log_info("secrets:", heading)
127+
for sec_type in SEC_TYPES_PEER:
128+
log_info("-", sec_type)
129+
130+
if sec_type not in _secrets:
131+
continue
132+
secrets = _secrets[sec_type]
133+
for key, value in secrets:
134+
log_info(" - %s: %s..." % (key, value[0:16]))
135+
136+
75137
def _security_irq(event, data):
76138
global _modified
77139

@@ -90,20 +152,43 @@ def _security_irq(event, data):
90152

91153
elif event == _IRQ_SET_SECRET:
92154
sec_type, key, value = data
93-
key = sec_type, bytes(key)
155+
key = bytes(key)
94156
value = bytes(value) if value else None
95157

96-
log_info("set secret:", key, value)
97-
98-
if value is None:
99-
# Delete secret.
100-
if key not in _secrets:
101-
return False
102-
103-
del _secrets[key]
104-
else:
105-
# Save secret.
106-
_secrets[key] = value
158+
is_saving = value is not None
159+
is_deleting = not is_saving
160+
161+
if core.log_level > 2:
162+
if is_deleting:
163+
log_info("del secret:", key)
164+
else:
165+
shortval = value
166+
if len(value) > 16:
167+
shortval = value[0:16] + b"..."
168+
log_info("set secret:", sec_type, key, shortval)
169+
170+
if sec_type not in _secrets:
171+
_secrets[sec_type] = []
172+
secrets = _secrets[sec_type]
173+
174+
# Delete existing secrets matching the type and key.
175+
removed = _remove_entry(sec_type, key)
176+
177+
if is_deleting and not removed:
178+
# Delete mode, but no entries were deleted
179+
return False
180+
181+
if is_saving:
182+
# Save new secret.
183+
if limit_peers and sec_type in SEC_TYPES_PEER and len(secrets) >= limit_peers:
184+
addr, _ = secrets[0]
185+
log_warn("Removing old peer to make space for new one")
186+
ble.gap_unpair(addr)
187+
log_info("Removed:", addr)
188+
# Add new value to database
189+
secrets.append((key, value))
190+
191+
_log_peers("set_secret")
107192

108193
# Queue up a save (don't synchronously write to flash).
109194
_modified = True
@@ -116,19 +201,23 @@ def _security_irq(event, data):
116201

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

204+
secrets = _secrets.get(sec_type, [])
119205
if key is None:
120206
# Return the index'th secret of this type.
121-
i = 0
122-
for (t, _key), value in _secrets.items():
123-
if t == sec_type:
124-
if i == index:
125-
return value
126-
i += 1
207+
# This is used when loading "all" secrets at startup
208+
if len(secrets) > index:
209+
key, val = secrets[index]
210+
return val
211+
127212
return None
128213
else:
129214
# Return the secret for this key (or None).
130-
key = sec_type, bytes(key)
131-
return _secrets.get(key, None)
215+
key = bytes(key)
216+
217+
for k, v in secrets:
218+
if k == key:
219+
return v
220+
return None
132221

133222
elif event == _IRQ_PASSKEY_ACTION:
134223
conn_handle, action, passkey = data

0 commit comments

Comments
 (0)