Skip to content

Commit 41c38e0

Browse files
committed
Introduced different key stores for LE secure, using json file or NVS
1 parent 75c95bb commit 41c38e0

4 files changed

Lines changed: 140 additions & 53 deletions

File tree

hid_keystores.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import esp32
2+
import json
3+
import binascii
4+
5+
# Class that represents a generic keystore
6+
class KeyStore(object):
7+
8+
def __init__(self):
9+
self.secrets = {}
10+
11+
def add_secret(self, type, key, value):
12+
_key = (type, bytes(key))
13+
self.secrets[_key] = bytes(value)
14+
15+
def get_secret(self, type, index, key):
16+
_key = (type, bytes(key) if key else None)
17+
value = None
18+
19+
if key is None:
20+
i = 0
21+
for (t, _k), _val in self.secrets.items():
22+
if t == type:
23+
if i == index:
24+
value = _val
25+
i += 1
26+
else:
27+
value = self.secrets.get(_key, None)
28+
29+
return value
30+
31+
def remove_secret(self, type, key):
32+
_key = (type, bytes(key))
33+
del self.secrets[_key]
34+
35+
def has_secret(self, type, key):
36+
_key = (type, bytes(key))
37+
return _key in self.secrets
38+
39+
def json_dump(self):
40+
json_secrets = [
41+
(sec_type, binascii.b2a_base64(key, newline=False), binascii.b2a_base64(value, newline=False))
42+
for (sec_type, key), value in self.secrets.items()
43+
]
44+
return json_secrets
45+
46+
def add_json_entries(self, entries):
47+
for sec_type, key, value in entries:
48+
self.secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
49+
50+
def load_secrets(self):
51+
return
52+
53+
def save_secrets(self):
54+
return
55+
56+
57+
# Class that uses a JSON file as keystore
58+
class JSONKeyStore(KeyStore):
59+
60+
def __init__(self):
61+
super(JSONKeyStore, self).__init__()
62+
63+
def load_secrets(self):
64+
try:
65+
with open("keys.json", "r") as file:
66+
self.add_json_entries(json.load(file))
67+
except:
68+
print("No secrets available")
69+
70+
def save_secrets(self):
71+
try:
72+
with open("keys.json", "w") as file:
73+
json.dump(self.json_dump(), file)
74+
except:
75+
print("Failed to save secrets")
76+
77+
78+
# Class that uses non-volatile storage as keystore
79+
class NVSKeyStore(KeyStore):
80+
81+
def __init__(self):
82+
super(NVSKeyStore, self).__init__()
83+
self.nvsdata = esp32.NVS("BLE")
84+
85+
# Load bonding keys from non-volatile storage.
86+
def load_secrets(self):
87+
data = bytearray()
88+
num_bytes = 0
89+
90+
try:
91+
num_bytes = self.nvsdata.get_blob("Keys", data)
92+
data = bytearray(num_bytes)
93+
self.nvsdata.get_blob("Keys", data)
94+
except:
95+
print("Failed to read NVS")
96+
97+
if num_bytes > 0:
98+
s = str(data, 'utf-8')
99+
try:
100+
entries = json.loads(s)
101+
self.add_json_entries(entries)
102+
except:
103+
print("Failed to load secrets")
104+
else:
105+
print("No secrets available")
106+
107+
# Save bonding keys to non-volatile storage.
108+
def save_secrets(self):
109+
try:
110+
self.nvsdata.set_blob("Keys", json.dumps(self.json_dump()))
111+
self.nvsdata.commit()
112+
except:
113+
print("Failed to save secrets")
114+

hid_services.py

Lines changed: 20 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from micropython import const
1919
import struct
2020
import bluetooth
21-
import json
22-
import binascii
2321
from bluetooth import UUID
22+
from hid_keystores import JSONKeyStore
23+
from hid_keystores import NVSKeyStore
2424

2525
F_READ = bluetooth.FLAG_READ
2626
F_WRITE = bluetooth.FLAG_WRITE
@@ -180,7 +180,6 @@ def stop_advertising(self):
180180
self._ble.gap_advertise(0, adv_data=self._payload)
181181
print("Stopped advertising")
182182

183-
184183
# Class that represents a general HID device services.
185184
class HumanInterfaceDevice(object):
186185
# Define device states
@@ -205,9 +204,7 @@ def __init__(self, device_name="Generic HID Device"):
205204
self.key_size = 0 # The encryption key size.
206205

207206
self.passkey = 1234 # The standard passkey for pairing. Only used when io capability allows so. Use the set_passkey(passkey) function to overwrite.
208-
self.secrets = {} # The key store for bonding
209-
210-
self.load_secrets() # Call the function to load the known keys for bonding into the key store.
207+
self.secrets = NVSKeyStore() # The key store for bonding
211208

212209
# General characteristics.
213210
self.device_name = device_name # The device name.
@@ -350,36 +347,24 @@ def ble_irq(self, event, data):
350347
print("Unknown passkey action")
351348
elif event == _IRQ_SET_SECRET: # Set secret for bonding.
352349
sec_type, key, value = data
353-
key = (sec_type, bytes(key))
354-
value = bytes(value) if value else None
355350
if value is None: # If value is empty, and
356-
if key in self.secrets: # If key is known then
357-
del self.secrets[key] # Forget key
358-
self.save_secrets()
359-
print("Removing secret:", key)
351+
if self.secrets.has_secret(sec_type, key): # If key is known then
352+
self.secrets.remove_secret(sec_type, key) # Forget key
353+
self.secrets.save_secrets()
354+
print("Removing secret:", bytes(key))
360355
return True
361356
else:
362-
print("Secret not found:", key)
357+
print("Secret not found:", bytes(key))
363358
return False
364359
else:
365-
self.secrets[key] = value # Remember key/value
366-
self.save_secrets()
367-
print("Saving secret:", key, value)
360+
self.secrets.add_secret(sec_type, key, value) # Remember key/value
361+
self.secrets.save_secrets()
362+
print("Saving secret:", bytes(key), bytes(value))
368363
return True
369364
elif event == _IRQ_GET_SECRET: # Get secret for bonding
370365
sec_type, index, key = data
371-
_key = (sec_type, bytes(key) if key else None)
372-
value = None
373-
if key is None:
374-
i = 0
375-
for (t, _k), _val in self.secrets.items():
376-
if t == sec_type:
377-
if i == index:
378-
value = _val
379-
i += 1
380-
else:
381-
value = self.secrets.get(_key, None)
382-
print("Returning secret:", bytes(value) if value else None, "for", "key" if key else "index", _key if key else index, "with type", sec_type)
366+
value = self.secrets.get_secret(sec_type, index, key)
367+
print("Returning secret:", bytes(value) if value else None, "for", "key" if key else "index", bytes(key) if key else index, "with type", sec_type)
383368
return value
384369
else:
385370
print("Unhandled IRQ event:", event)
@@ -389,6 +374,8 @@ def ble_irq(self, event, data):
389374
# the overwritten function by using super(Subclass, self).start().
390375
def start(self):
391376
if self.device_state is HumanInterfaceDevice.DEVICE_STOPPED:
377+
self.secrets.load_secrets() # Call the function to load the known keys for bonding into the key store.
378+
392379
self._ble.irq(self.ble_irq) # Set interrupt request callback function.
393380
self._ble.active(1) # Turn on BLE radio.
394381

@@ -464,28 +451,6 @@ def write_service_characteristics(self):
464451
for handle, (name, value) in self.characteristics.items():
465452
self._ble.gatts_write(handle, value)
466453

467-
# Load bonding keys from json file.
468-
def load_secrets(self):
469-
try:
470-
with open("keys.json", "r") as file:
471-
entries = json.load(file)
472-
for sec_type, key, value in entries:
473-
self.secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
474-
except:
475-
print("No secrets available")
476-
477-
# Save bonding keys to json file.
478-
def save_secrets(self):
479-
try:
480-
with open("keys.json", "w") as file:
481-
json_secrets = [
482-
(sec_type, binascii.b2a_base64(key, newline=False), binascii.b2a_base64(value, newline=False))
483-
for (sec_type, key), value in self.secrets.items()
484-
]
485-
json.dump(json_secrets, file)
486-
except:
487-
print("Failed to save secrets")
488-
489454
# Returns whether the device is not stopped.
490455
def is_running(self):
491456
return self.device_state is not HumanInterfaceDevice.DEVICE_STOPPED
@@ -607,6 +572,11 @@ def set_le_secure(self, le_secure=True):
607572
def set_io_capability(self, io_capability):
608573
self.io_capability = io_capability
609574

575+
# Set the keystore class to use.
576+
# Must be called before calling Start().
577+
def set_keystore(self, keystore):
578+
self.secrets = keystore
579+
610580
# Set callback function for pairing events.
611581
# Depending on the I/O capability used, the callback function should return either a
612582
# - boolean to accept or deny a connection, or a

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"urls": [
3-
["hid_services.py", "github:Heerkog/MicroPythonBLEHID/hid_services.py"]
3+
["hid_services.py", "github:Heerkog/MicroPythonBLEHID/hid_services.py"],
4+
["hid_keystore.py", "github:Heerkog/MicroPythonBLEHID/hid_keystore.py"]
45
],
5-
"version": "1.0.0",
6+
"version": "2.1.0",
67
"deps": []
78
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ The repository is structured as followed:
9191
* `mouse_example.py`
9292
* `tinypico/` directory containing TinyPICO specific examples. These are mostly personal projects.
9393
* `hid_services.py` the library.
94+
* `hid_keystores.py` different key stores to use with the library.
9495
* `LICENSE` the license.
9596
* `readme.md`
9697

@@ -135,14 +136,15 @@ The library consists of five classes with the following functions:
135136
* `set_io_capability(io_capability)` (Set input/output capability of this device Determines the pairing procedure, e.g., accept connection/passkey entry/just works. Must be called before calling `start()`)
136137
* `set_passkey_callback(passkey_callback)` (Set callback function for pairing events. Callback function should return boolean to accept connection or passkey depending on I/O capability used)
137138
* `set_passkey(passkey)` (Set the passkey to use for pairing)
139+
* `set_keystore(keystore)` (Sets the key store to use from `hid_keystores.py`. Default `JSONKeyStore`)
138140
* `set_battery_level(level)` (Sets the battery level internally)
139141
* `notify_battery_level()` (Notifies the central of the current battery level. Call after setting battery level)
140142
* `notify_hid_report()` (Function for subclasses to override)
141143

142144
* `Joystick` (subclass of `HumanInterfaceDevice`, implements joystick service)
143145
* `__init__(name)` (Initialize the joystick)
144146
* `start()` (Starts the HID service using joystick characteristics. Calls `HumanInterfaceDevice.start()`)
145-
* `write_service_characteristics(handles)` (Writes the joystick HID service characteristics. Calls `HumanInterfaceDevice.write_service_characteristics(handles)`)
147+
* `write_service_characteristics(handles)` (Writes the joystick HID service characteristics. Calls `HumanInterfaceDevice.write_service_characteristics(handles)`)
146148
* `notify_hid_report()` (Notifies the central of the internal HID joystick status)
147149
* `set_axes(x, y)` (Sets the joystick axes internally)
148150
* `set_buttons(b1, b2, b3, b4, b5, b6, b7, b8)` (Sets the joystick buttons internally)

0 commit comments

Comments
 (0)