Skip to content

Commit d370855

Browse files
committed
aioble/security: Add option to limit number of peers stored.
1 parent 335578a commit d370855

File tree

1 file changed

+91
-29
lines changed

1 file changed

+91
-29
lines changed

micropython/bluetooth/aioble/aioble/security.py

+91-29
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import uasyncio as 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

@@ -27,27 +27,57 @@
2727
_DEFAULT_PATH = "ble_secrets.json"
2828

2929
# Maintain list of known keys, newest at the bottom / end.
30-
_secrets = []
30+
_secrets = {}
3131
_modified = False
3232
_path = None
3333

34+
# If set, limit the pairing db to this many peers
35+
limit_peers = None
36+
37+
SEC_TYPES_SELF = (10, ) # todo add ident for btstack to tuple
38+
SEC_TYPES_PEER = (1, 2, )
39+
3440

3541
# Must call this before stack startup.
3642
def load_secrets(path=None):
37-
global _path, _secrets
43+
global _path, _secrets, limit_peers
3844

3945
# Use path if specified, otherwise use previous path, otherwise use
4046
# default path.
4147
_path = path or _path or _DEFAULT_PATH
4248

4349
# Reset old secrets.
44-
_secrets = []
50+
_secrets.clear()
4551
try:
4652
with open(_path, "r") as f:
4753
entries = json.load(f)
54+
# Newest entries at at the end, load them first
4855
for sec_type, key, value in entries:
56+
if sec_type not in _secrets:
57+
_secrets[sec_type] = []
4958
# Decode bytes from hex.
50-
_secrets.append(((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_info("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+
5181
except:
5282
log_warn("No secrets available")
5383

@@ -62,17 +92,33 @@ def _save_secrets(arg=None):
6292
# Only save if the secrets changed.
6393
return
6494

95+
_log_peers('save_secrets')
96+
6597
with open(_path, "w") as f:
6698
# Convert bytes to hex strings (otherwise JSON will treat them like
6799
# strings).
68100
json_secrets = [
69101
(sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value))
70-
for (sec_type, key), value in _secrets
102+
for sec_type in _secrets for key, value in _secrets[sec_type]
71103
]
72104
json.dump(json_secrets, f)
73105
_modified = False
74106

75107

108+
def _log_peers(heading=""):
109+
if core.log_level <= 2:
110+
return
111+
log_info("secrets:", heading)
112+
for sec_type in SEC_TYPES_PEER:
113+
log_info("-", sec_type)
114+
115+
if sec_type not in _secrets:
116+
continue
117+
secrets = _secrets[sec_type]
118+
for key, value in secrets:
119+
log_info(" - %s: %s..." % (key, value[0:16]))
120+
121+
76122
def _security_irq(event, data):
77123
global _modified
78124

@@ -91,26 +137,42 @@ def _security_irq(event, data):
91137

92138
elif event == _IRQ_SET_SECRET:
93139
sec_type, key, value = data
94-
key = sec_type, bytes(key)
140+
key = bytes(key)
95141
value = bytes(value) if value else None
96142

97-
log_info("set secret:", key, value)
98-
99143
if value is None:
100-
# Delete secret.
101-
for to_delete in [
102-
entry for entry in _secrets if entry[0] == key
103-
]:
104-
_secrets.remove(to_delete)
105-
break
106-
107-
else:
108-
# No entries to delete
144+
log_info("del secret:", key)
145+
else:
146+
log_info("set secret:", sec_type, key, value)
147+
148+
if sec_type not in _secrets:
149+
_secrets[sec_type] = []
150+
secrets = _secrets[sec_type]
151+
152+
# Delete existing secrets matching the type and key.
153+
for to_delete in [
154+
entry for entry in secrets if entry[0] == key
155+
]:
156+
log_info("Removing existing secret matching key")
157+
secrets.remove(to_delete)
158+
break
159+
160+
else:
161+
if value is None:
162+
# Explicit delete command: No entries to delete
109163
return False
110164

111-
else:
112-
# Save secret.
113-
_secrets.append((key, value))
165+
if value is not None:
166+
# Save new secret.
167+
if limit_peers and sec_type in SEC_TYPES_PEER and len(secrets) >= limit_peers:
168+
addr, _ = secrets[0]
169+
log_warn("Removing old peer to make space for new one")
170+
ble.gap_unpair(addr)
171+
log_info("Removed:", addr)
172+
# Add new value to database
173+
secrets.append((key, value))
174+
175+
_log_peers("set_secret")
114176

115177
# Queue up a save (don't synchronously write to flash).
116178
if not _modified:
@@ -124,20 +186,20 @@ def _security_irq(event, data):
124186

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

189+
secrets = _secrets.get(sec_type, [])
127190
if key is None:
128191
# Return the index'th secret of this type.
129-
i = 0
130-
for (t, _key), value in _secrets:
131-
if t == sec_type:
132-
if i == index:
133-
return value
134-
i += 1
192+
# This is used when loading "all" secrets at startup
193+
if len(secrets) > index:
194+
key, val = secrets[index]
195+
return val
196+
135197
return None
136198
else:
137199
# Return the secret for this key (or None).
138-
key = sec_type, bytes(key)
200+
key = bytes(key)
139201

140-
for k, v in _secrets:
202+
for k, v in secrets:
141203
if k == key:
142204
return v
143205
return None

0 commit comments

Comments
 (0)