Skip to content

Commit 1ed3db2

Browse files
authored
Stabilize BLE Security Implementation (#941)
* Refactor BLE broadcast encryption flags The original spec defines the flags that are used to indicate the keying material for BLE broadcasts: ... kBroadcastDataIsEncrypted = 1 << 3, kBroadcastKeyIsDeviceKey = 1 << 4, kBroadcastKeyIsUserKey = 1 << 5, ... Using this implementation only three states can be encoded: no encryption, encrypted with device key and encrypted with a user key. New implementation treats this three bytes like enum instead of seperate bools and introduce a new encryption state (encrypted with null key): kNoEncryption = 0, kEncryptedWithNullKey = 1, kEncryptedWithUserKey = 2, kEncryptedWithDeviceKey = 3, * Consolidate auth characteristics The BLE authentication flow is completely synchronous so there is no need for two separate characteristics for the client and the server. This commit unifies two authentication characteristics into one. * Modify Security Level 0 For security level 0, the root key is considered to be 32-bytes of zeros and authentication proceeds exactly the same as level 1 and 2 given that root key. * Stabilize flags/info ble characteristic Info characteristic holds 20 bytes 1 byte: version 2 byte: security flags 3-4 bytes: high flags 5-20 bytes: reserved flags Added RPC IN PROGRESS flag for tracking if the device handles a RPC and cannot handle another one * Distinguish password-based user-key and random data Introduce a 3rd AuthMethod and key: PasswordBasedAuthentication and PasswordBasedUserKey The primary benefit is that it distinguishes between when we are looking for a password that a user could enter from when we are looking for something that has to be stored in a server or key store somewhere. * Update comments in test_signed_list_report.py
1 parent 3150356 commit 1ed3db2

File tree

11 files changed

+193
-102
lines changed

11 files changed

+193
-102
lines changed

iotilecore/RELEASE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All major changes in each released version of `iotile-core` are listed here.
1010
- Add shim to make `iotile-core` compatible with Python 3.8.0 on Windows. There is a bug in
1111
that python version that breaks background event loops only on Windows. It is fixed in
1212
python 3.8.1.
13+
- Add new BLE broadcast encryption method: Encrypted with NullKey
1314

1415
## 5.0.11
1516

iotilecore/iotile/core/hw/auth/auth_provider.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ class AuthProvider:
1616
ReportKeyMagic = 0x00000002
1717

1818
NoKey = 0
19-
UserKey = 1
20-
DeviceKey = 2
19+
NullKey = 1
20+
UserKey = 2
21+
DeviceKey = 3
22+
PasswordBasedKey = 4
2123

2224
KnownKeyRoots = {
2325
NoKey: 'no_key',
26+
NullKey: 'null_key',
2427
UserKey: 'user_key',
25-
DeviceKey: 'device_key'
28+
DeviceKey: 'device_key',
29+
PasswordBasedKey: 'pasword_based_userkey'
2630
}
2731

2832
def __init__(self, args=None):
@@ -48,6 +52,19 @@ def verify_key(self, root_key_type):
4852
if root_key_type not in self.supported_keys:
4953
raise NotFoundError("Not supported key type", key=root_key_type)
5054

55+
@classmethod
56+
def DeriveRebootKeyFromPassword(cls, password):
57+
"""Derive the root key from the user password
58+
TODO hashlib.pbkdf2_hmac arguments needs to be revised,
59+
current values are not proved to be secure
60+
61+
Args:
62+
password (str): user password
63+
64+
Returns:
65+
bytes: derived key
66+
"""
67+
return hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), b'salt', 100000)
5168

5269
@classmethod
5370
def DeriveReportKey(cls, root_key, report_id, sent_timestamp):

iotilecore/iotile/core/hw/auth/cli_auth_provider.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import hashlib
77
from iotile.core.exceptions import NotFoundError
88
from .rootkey_auth_provider import RootKeyAuthProvider
9+
from .env_auth_provider import EnvAuthProvider
10+
from .auth_provider import AuthProvider
911
import getpass
1012

1113

@@ -18,36 +20,29 @@ def __init__(self, args=None):
1820
if args is None:
1921
args = {}
2022

21-
args['supported_keys'] = [self.UserKey]
23+
args['supported_keys'] = [self.PasswordBasedKey]
2224

2325
super().__init__(args)
2426

25-
@classmethod
26-
def derive_key(cls, password):
27-
"""Derive the root key from user password
28-
TODO hashlib.pbkdf2_hmac arguments needs to be revised,
29-
current values are not proved to be secure
30-
31-
Args:
32-
password (str): user password
33-
34-
Returns:
35-
bytes: derived key
36-
"""
37-
return hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), b'salt', 100000)
38-
3927
def get_root_key(self, key_type, device_id):
40-
"""Prompt user for the password and derive root key from it
28+
"""Prompt a user for the password and derive the root key from it
4129
4230
Args:
4331
key_type (int): see KnownKeyRoots
44-
device_id (int): uuid of the device
32+
device_id (int): the uuid of the device
4533
4634
Returns:
4735
bytes: the root key
4836
"""
4937
self.verify_key(key_type)
5038

51-
password = getpass.getpass("Please, input user password for device {} :".format(device_id))
39+
password = getpass.getpass("Please input the user password for the device {} :".format(device_id))
40+
userkey = AuthProvider.DeriveRebootKeyFromPassword(password)
41+
42+
if device_id:
43+
answer = input("Would you like to save the user-key until the end of the current session? (y/n)")
44+
if answer and answer[0].lower() == 'y':
45+
variable_name = EnvAuthProvider.construct_var_name(device_id)
46+
os.environ[variable_name] = userkey.hex()
5247

53-
return CliAuthProvider.derive_key(password)
48+
return userkey

iotilecore/iotile/core/hw/auth/env_auth_provider.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ def __init__(self, args=None):
1818

1919
super().__init__(args)
2020

21+
@classmethod
22+
def construct_var_name(cls, device_id):
23+
"""Build name of environment variable used to store userkey
24+
25+
Returns:
26+
str: variable name
27+
"""
28+
if isinstance(device_id, str):
29+
var_name = "USER_KEY_{}".format(device_id)
30+
else:
31+
var_name = "USER_KEY_{0:08X}".format(device_id)
32+
return var_name
33+
2134
def get_root_key(self, key_type, device_id):
2235
"""Attempt to get a user key from an environment variable
2336
@@ -30,10 +43,7 @@ def get_root_key(self, key_type, device_id):
3043
"""
3144
self.verify_key(key_type)
3245

33-
if isinstance(device_id, str):
34-
var_name = "USER_KEY_{}".format(device_id)
35-
else:
36-
var_name = "USER_KEY_{0:08X}".format(device_id)
46+
var_name = EnvAuthProvider.construct_var_name(device_id)
3747

3848
if var_name not in os.environ:
3949
raise NotFoundError("No key could be found for devices", device_id=device_id,
@@ -43,7 +53,6 @@ def get_root_key(self, key_type, device_id):
4353
if len(key_var) != 64:
4454
raise NotFoundError("Key in variable is not the correct length, should be 64 hex characters",
4555
device_id=device_id, key_value=key_var)
46-
4756
try:
4857
key = binascii.unhexlify(key_var)
4958
except ValueError as exc:

iotilecore/iotile/core/hw/auth/null_auth_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def __init__(self, args=None):
1010
if args is None:
1111
args = {}
1212

13-
args['supported_keys'] = [self.NoKey]
13+
args['supported_keys'] = [self.NullKey]
1414

1515
super().__init__(args)
1616

iotilecore/test/test_reports/test_signed_list_report.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@
55
from iotile.core.hw.reports.signed_list_format import SignedListReport
66
from iotile.core.hw.reports.report import IOTileReading
77
from iotile.core.hw.auth.env_auth_provider import EnvAuthProvider
8+
from iotile.core.hw.auth.auth_provider import AuthProvider
89

910

10-
def make_sequential(iotile_id, stream, num_readings, give_ids=False, root_key=0, signer=None):
11+
def make_sequential(iotile_id, stream, num_readings, give_ids=False, root_key=AuthProvider.NoKey, signer=None):
12+
"""Create sequaltial report from reading
13+
14+
Args:
15+
iotile_id (int): The uuid of the device that this report came from
16+
stream (int): The stream that these readings are part of
17+
num_readings(int): amount of readings
18+
give_ids(bool): whether to set sequantial id for every reading
19+
root_key(int): type of root key to sign the report
20+
signer (AuthProvider): An optional preconfigured AuthProvider that should be used to sign this
21+
report. If no AuthProvider is provided, the default ChainedAuthProvider is used.
22+
"""
1123
readings = []
1224

1325
for i in range(0, num_readings):
@@ -22,8 +34,7 @@ def make_sequential(iotile_id, stream, num_readings, give_ids=False, root_key=0,
2234
return report
2335

2436
def test_basic_parsing():
25-
"""Make sure we can decode a signed report
26-
"""
37+
"""Make sure we can decode a signed report"""
2738

2839
report = make_sequential(1, 0x1000, 10)
2940
encoded = report.encode()
@@ -41,8 +52,7 @@ def test_basic_parsing():
4152
assert report.signature_flags == 0
4253

4354
def test_footer_calculation():
44-
"""
45-
"""
55+
"""Test if make_sesuentail set properly ids"""
4656

4757
report1 = make_sequential(1, 0x1000, 10, give_ids=False)
4858
report2 = make_sequential(1, 0x1000, 10, give_ids=True)
@@ -60,15 +70,15 @@ def test_userkey_signing(monkeypatch):
6070
signer = EnvAuthProvider()
6171

6272
with pytest.raises(ExternalError):
63-
report1 = make_sequential(1, 0x1000, 10, give_ids=True, root_key=1, signer=signer)
73+
report1 = make_sequential(1, 0x1000, 10, give_ids=True, root_key=AuthProvider.UserKey, signer=signer)
6474

65-
report1 = make_sequential(2, 0x1000, 10, give_ids=True, root_key=1, signer=signer)
75+
report1 = make_sequential(2, 0x1000, 10, give_ids=True, root_key=AuthProvider.UserKey, signer=signer)
6676

6777
encoded = report1.encode()
6878
report2 = SignedListReport(encoded)
6979

70-
assert report1.signature_flags == 1
71-
assert report2.signature_flags == 1
80+
assert report1.signature_flags == AuthProvider.UserKey
81+
assert report2.signature_flags == AuthProvider.UserKey
7282
assert report1.verified
7383
assert report1.encrypted
7484
assert report2.verified

transport_plugins/bled112/RELEASE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All major changes in each released version of the bled112 transport plugin are listed here.
44

5+
## HEAD
6+
7+
- Refactor BLE broadcast encryption flags: three bits are treated as an enumeration
8+
- Consilidate two authentication characteristics into one
9+
- Update info characteristic
10+
- Remove separate logic for NullKey encryption temp key generation, the temp key is
11+
generated the same way it is done for user key
12+
- Add the password-based authentication method
13+
- Update the authentication flow
14+
515
## 3.0.3
616

717
- Fix bled112_auth error handling

transport_plugins/bled112/iotile_transport_bled112/bled112.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ def send_rpc_async(self, conn_id, address, rpc_id, payload, timeout, callback):
311311

312312
services = self._connections[found_handle]['services']
313313

314+
if self.check_is_rpc_in_progress(found_handle, services):
315+
callback(conn_id, self.id, False, 'RPC still in progress', None, None)
316+
return
317+
314318
self._command_task.async_command(['_send_rpc', found_handle, services, address, rpc_id, payload, timeout], self._send_rpc_finished,
315319
{'connection_id': conn_id, 'handle': found_handle,
316320
'callback': callback})
@@ -664,17 +668,13 @@ def _parse_v2_advertisement(self, rssi, sender, data):
664668
# bit 0: Has pending data to stream
665669
# bit 1: Low voltage indication
666670
# bit 2: User connected
667-
# bit 3: Broadcast data is encrypted
668-
# bit 4: Encryption key is device key
669-
# bit 5: Encryption key is user key
671+
# bit 3 - 5: Broadcast encryption key type
670672
# bit 6: broadcast data is time synchronized to avoid leaking
671673
# information about when it changes
672674
is_pending_data = bool(flags & (1 << 0))
673675
is_low_voltage = bool(flags & (1 << 1))
674676
is_user_connected = bool(flags & (1 << 2))
675-
is_encrypted = bool(flags & (1 << 3))
676-
is_device_key = bool(flags & (1 << 4))
677-
is_user_key = bool(flags & (1 << 5))
677+
broadcast_encryption_key_type = (flags >> 3) & 7
678678

679679
self._device_scan_counts.setdefault(device_id, {'v1': 0, 'v2': 0})['v2'] += 1
680680

@@ -691,24 +691,17 @@ def _parse_v2_advertisement(self, rssi, sender, data):
691691
'battery': battery / 32.0,
692692
'advertising_version':2}
693693

694-
key_type = AuthProvider.NoKey
695-
if is_encrypted:
696-
if is_device_key:
697-
key_type = AuthProvider.DeviceKey
698-
elif is_user_key:
699-
key_type = AuthProvider.UserKey
700-
701-
if is_encrypted:
694+
if broadcast_encryption_key_type:
702695
if not _HAS_CRYPTO:
703696
return info, timestamp, None, None, None, None, None
704697

705698
try:
706-
key = self._key_provider.get_rotated_key(key_type, device_id,
699+
key = self._key_provider.get_rotated_key(broadcast_encryption_key_type, device_id,
707700
reboot_counter=reboots,
708701
rotation_interval_power=EPHEMERAL_KEY_CYCLE_POWER,
709702
current_timestamp=timestamp)
710703
except NotFoundError:
711-
self._logger.warning("Key type {} is not found".format(key_type), exc_info=True)
704+
self._logger.warning("Key type {} is not found".format(broadcast_encryption_key_type), exc_info=True)
712705
return info, timestamp, None, None, None, None, None
713706

714707
nonce = generate_nonce(device_id, timestamp, reboot_low, reboot_high_packed, counter_packed)
@@ -870,6 +863,27 @@ def check_authentication(self, uuid, conn_id, handle, services):
870863
'handle': handle,
871864
'services': services})
872865

866+
def check_is_rpc_in_progress(self, handle, services):
867+
""" Discover if the device handles RPC at the moment
868+
869+
Another RPC should not be sent to the device if handling of previous
870+
is not finished
871+
872+
Args:
873+
handle (int): a handle to the connection on the BLED112 dongle
874+
services (dict): A dictionary of GATT services produced by probe_services()
875+
876+
Returns:
877+
bool: True if RPC is being handled at the moment
878+
"""
879+
RPC_IN_PROGRESS_FLAG = 0x0001
880+
try:
881+
value = self._command_task.sync_command(["get_info_flags", handle, services])
882+
version, _, high_flags = struct.unpack("BBH16x", value['data'])
883+
return (high_flags & RPC_IN_PROGRESS_FLAG) == 0x01
884+
except HardwareError:
885+
return False
886+
873887
def initialize_system_sync(self):
874888
"""Remove all active connections and query the maximum number of supported connections
875889
"""
@@ -1125,8 +1139,8 @@ def _on_authentication_check_response(self, result):
11251139
context = result['context']
11261140

11271141
if result['result']:
1128-
flags, = struct.unpack("B", result['return_value']['data'])
1129-
if flags == 0x01:
1142+
version, security_flags, _ = struct.unpack("BBH16x", result['return_value']['data'])
1143+
if security_flags == 0x01:
11301144
self._logger.debug("Authentication is required")
11311145

11321146
self.authenticate(context['uuid'], context['connection_id'],

0 commit comments

Comments
 (0)