5
5
import uasyncio as asyncio
6
6
import binascii
7
7
import json
8
-
8
+ from . import core
9
9
from .core import log_info , log_warn , ble , register_irq_handler
10
10
from .device import DeviceConnection
11
11
26
26
27
27
_DEFAULT_PATH = "ble_secrets.json"
28
28
29
+ # Maintain list of known keys, newest at the bottom / end.
29
30
_secrets = {}
30
31
_modified = False
31
32
_path = None
32
33
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
+
33
40
34
41
# Must call this before stack startup.
35
42
def load_secrets (path = None ):
36
- global _path , _secrets
43
+ global _path , _secrets , limit_peers
37
44
38
45
# Use path if specified, otherwise use previous path, otherwise use
39
46
# default path.
40
47
_path = path or _path or _DEFAULT_PATH
41
48
42
49
# Reset old secrets.
43
- _secrets = {}
50
+ _secrets . clear ()
44
51
try :
45
52
with open (_path , "r" ) as f :
46
53
entries = json .load (f )
54
+ # Newest entries at at the end, load them first
47
55
for sec_type , key , value in entries :
56
+ if sec_type not in _secrets :
57
+ _secrets [sec_type ] = []
48
58
# 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
+
50
81
except :
51
82
log_warn ("No secrets available" )
52
83
@@ -61,17 +92,48 @@ def _save_secrets(arg=None):
61
92
# Only save if the secrets changed.
62
93
return
63
94
95
+ _log_peers ('save_secrets' )
96
+
64
97
with open (_path , "w" ) as f :
65
98
# Convert bytes to hex strings (otherwise JSON will treat them like
66
99
# strings).
67
100
json_secrets = [
68
101
(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 ]
70
103
]
71
104
json .dump (json_secrets , f )
72
105
_modified = False
73
106
74
107
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
+
75
137
def _security_irq (event , data ):
76
138
global _modified
77
139
@@ -90,20 +152,43 @@ def _security_irq(event, data):
90
152
91
153
elif event == _IRQ_SET_SECRET :
92
154
sec_type , key , value = data
93
- key = sec_type , bytes (key )
155
+ key = bytes (key )
94
156
value = bytes (value ) if value else None
95
157
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" )
107
192
108
193
# Queue up a save (don't synchronously write to flash).
109
194
_modified = True
@@ -116,19 +201,23 @@ def _security_irq(event, data):
116
201
117
202
log_info ("get secret:" , sec_type , index , bytes (key ) if key else None )
118
203
204
+ secrets = _secrets .get (sec_type , [])
119
205
if key is None :
120
206
# 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
+
127
212
return None
128
213
else :
129
214
# 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
132
221
133
222
elif event == _IRQ_PASSKEY_ACTION :
134
223
conn_handle , action , passkey = data
0 commit comments