forked from auracaster/bumble_mirror
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3040df3179 | |||
| c66b357de6 | |||
| 0ffed3deff | |||
| 2f949a1182 | |||
| 4e2fae5145 | |||
| 2b58364c51 | |||
| e3bf7c4b53 | |||
| b64fa65921 | |||
| 7d87c3cc3a |
@@ -227,3 +227,17 @@ def g2(u, v, x, y):
|
||||
aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), bytes(reversed(x)))[-4:],
|
||||
byteorder='big'
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def h6(w, key_id):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.10 Link key conversion function h6
|
||||
'''
|
||||
return aes_cmac(key_id, w)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def h7(salt, w):
|
||||
'''
|
||||
See Bluetooth spec, Vol 3, Part H - 2.2.11 Link key conversion function h7
|
||||
'''
|
||||
return aes_cmac(w, salt)
|
||||
|
||||
+29
-6
@@ -265,6 +265,11 @@ class DeviceConfiguration:
|
||||
self.scan_response_data = DEVICE_DEFAULT_SCAN_RESPONSE_DATA
|
||||
self.advertising_interval_min = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||
self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||
self.le_enabled = True
|
||||
# LE host enable 2nd parameter
|
||||
self.le_simultaneous_enabled = True
|
||||
self.classic_sc_enabled = True
|
||||
self.classic_ssp_enabled = True
|
||||
self.advertising_data = bytes(
|
||||
AdvertisingData([(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))])
|
||||
)
|
||||
@@ -278,7 +283,11 @@ class DeviceConfiguration:
|
||||
self.class_of_device = config.get('class_of_device', self.class_of_device)
|
||||
self.advertising_interval_min = config.get('advertising_interval', self.advertising_interval_min)
|
||||
self.advertising_interval_max = self.advertising_interval_min
|
||||
self.keystore = config.get('keystore')
|
||||
self.keystore = config.get('keystore')
|
||||
self.le_enabled = config.get('le_enabled', self.le_enabled)
|
||||
self.le_simultaneous_enabled = config.get('le_simultaneous_enabled', self.le_simultaneous_enabled)
|
||||
self.classic_sc_enabled = config.get('classic_sc_enabled', self.classic_sc_enabled)
|
||||
self.classic_ssp_enabled = config.get('classic_ssp_enabled', self.classic_ssp_enabled)
|
||||
|
||||
# Load or synthesize an IRK
|
||||
irk = config.get('irk')
|
||||
@@ -398,7 +407,6 @@ class Device(CompositeEventEmitter):
|
||||
self.connecting = False
|
||||
self.disconnecting = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
self.le_enabled = True
|
||||
self.classic_enabled = False
|
||||
self.discoverable = False
|
||||
self.connectable = False
|
||||
@@ -418,6 +426,10 @@ class Device(CompositeEventEmitter):
|
||||
self.advertising_interval_max = config.advertising_interval_max
|
||||
self.keystore = keys.KeyStore.create_for_device(config)
|
||||
self.irk = config.irk
|
||||
self.le_enabled = config.le_enabled
|
||||
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
||||
self.classic_ssp_enabled = config.classic_ssp_enabled
|
||||
self.classic_sc_enabled = config.classic_sc_enabled
|
||||
|
||||
# If a name is passed, override the name from the config
|
||||
if name:
|
||||
@@ -483,10 +495,11 @@ class Device(CompositeEventEmitter):
|
||||
if connection := self.connections.get(connection_handle):
|
||||
return connection
|
||||
|
||||
def find_connection_by_bd_addr(self, bd_addr):
|
||||
def find_connection_by_bd_addr(self, bd_addr, transport=None):
|
||||
for connection in self.connections.values():
|
||||
if connection.peer_address == bd_addr:
|
||||
return connection
|
||||
if connection.peer_address.get_bytes() == bd_addr.get_bytes():
|
||||
if transport is None or connection.transport == transport:
|
||||
return connection
|
||||
|
||||
def register_l2cap_server(self, psm, server):
|
||||
self.l2cap_channel_manager.register_server(psm, server)
|
||||
@@ -515,6 +528,11 @@ class Device(CompositeEventEmitter):
|
||||
logger.debug(color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow'))
|
||||
self.public_address = response.return_parameters.bd_addr
|
||||
|
||||
|
||||
await self.send_command(HCI_Write_LE_Host_Support_Command(
|
||||
le_supported_host = int(self.le_enabled),
|
||||
simultaneous_le_host = int(self.le_simultaneous_enabled),
|
||||
))
|
||||
if self.le_enabled:
|
||||
# Set the controller address
|
||||
await self.send_command(HCI_LE_Set_Random_Address_Command(
|
||||
@@ -552,7 +570,12 @@ class Device(CompositeEventEmitter):
|
||||
HCI_Write_Class_Of_Device_Command(class_of_device = self.class_of_device)
|
||||
)
|
||||
await self.send_command(
|
||||
HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode = 0x01)
|
||||
HCI_Write_Simple_Pairing_Mode_Command(
|
||||
simple_pairing_mode=int(self.classic_ssp_enabled))
|
||||
)
|
||||
await self.send_command(
|
||||
HCI_Write_Secure_Connections_Host_Support_Command(
|
||||
secure_connections_host_support=int(self.classic_sc_enabled))
|
||||
)
|
||||
|
||||
# Let the SMP manager know about the address
|
||||
|
||||
+15
-5
@@ -134,13 +134,21 @@ GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC = UUID.from_16_bits(0x2A2
|
||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2A2A, 'IEEE 11073-20601 Regulatory Certification Data List')
|
||||
GATT_PNP_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A50, 'PnP ID')
|
||||
|
||||
# Human Interface Device
|
||||
# Human Interface Device Service
|
||||
GATT_HID_INFORMATION_CHARACTERISTIC = UUID.from_16_bits(0x2A4A, 'HID Information')
|
||||
GATT_REPORT_MAP_CHARACTERISTIC = UUID.from_16_bits(0x2A4B, 'Report Map')
|
||||
GATT_HID_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A4C, 'HID Control Point')
|
||||
GATT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A4D, 'Report')
|
||||
GATT_PROTOCOL_MODE_CHARACTERISTIC = UUID.from_16_bits(0x2A4E, 'Protocol Mode')
|
||||
|
||||
# Heart Rate Service
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC = UUID.from_16_bits(0x2A37, 'Heart Rate Measurement')
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC = UUID.from_16_bits(0x2A38, 'Body Sensor Location')
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A39, 'Heart Rate Control Point')
|
||||
|
||||
# Battery Service
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
|
||||
|
||||
# Misc
|
||||
GATT_DEVICE_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2A00, 'Device Name')
|
||||
GATT_APPEARANCE_CHARACTERISTIC = UUID.from_16_bits(0x2A01, 'Appearance')
|
||||
@@ -150,7 +158,6 @@ GATT_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.from_16_bi
|
||||
GATT_SERVICE_CHANGED_CHARACTERISTIC = UUID.from_16_bits(0x2A05, 'Service Changed')
|
||||
GATT_ALERT_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A06, 'Alert Level')
|
||||
GATT_TX_POWER_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A07, 'Tx Power Level')
|
||||
GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
|
||||
GATT_BOOT_KEYBOARD_INPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A22, 'Boot Keyboard Input Report')
|
||||
GATT_CURRENT_TIME_CHARACTERISTIC = UUID.from_16_bits(0x2A2B, 'Current Time')
|
||||
GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A32, 'Boot Keyboard Output Report')
|
||||
@@ -339,16 +346,19 @@ class CharacteristicAdapter:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DelegatedCharacteristicAdapter(CharacteristicAdapter):
|
||||
def __init__(self, characteristic, encode, decode):
|
||||
'''
|
||||
Adapter that converts bytes values using an encode and a decode function.
|
||||
'''
|
||||
def __init__(self, characteristic, encode=None, decode=None):
|
||||
super().__init__(characteristic)
|
||||
self.encode = encode
|
||||
self.decode = decode
|
||||
|
||||
def encode_value(self, value):
|
||||
return self.encode(value)
|
||||
return self.encode(value) if self.encode else value
|
||||
|
||||
def decode_value(self, value):
|
||||
return self.decode(value)
|
||||
return self.decode(value) if self.decode else value
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from enum import IntEnum
|
||||
import struct
|
||||
|
||||
from ..gatt_client import ProfileServiceProxy
|
||||
from ..att import ATT_Error
|
||||
from ..gatt import (
|
||||
GATT_HEART_RATE_SERVICE,
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
TemplateService,
|
||||
Characteristic,
|
||||
CharacteristicValue,
|
||||
DelegatedCharacteristicAdapter,
|
||||
PackedCharacteristicAdapter
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HeartRateService(TemplateService):
|
||||
UUID = GATT_HEART_RATE_SERVICE
|
||||
HEART_RATE_CONTROL_POINT_FORMAT = 'B'
|
||||
CONTROL_POINT_NOT_SUPPORTED = 0x80
|
||||
RESET_ENERGY_EXPENDED = 0x01
|
||||
|
||||
class BodySensorLocation(IntEnum):
|
||||
OTHER = 0,
|
||||
CHEST = 1,
|
||||
WRIST = 2,
|
||||
FINGER = 3,
|
||||
HAND = 4,
|
||||
EAR_LOBE = 5,
|
||||
FOOT = 6
|
||||
|
||||
class HeartRateMeasurement:
|
||||
def __init__(
|
||||
self,
|
||||
heart_rate,
|
||||
sensor_contact_detected=None,
|
||||
energy_expended=None,
|
||||
rr_intervals=None
|
||||
):
|
||||
if heart_rate < 0 or heart_rate > 0xFFFF:
|
||||
raise ValueError('heart_rate out of range')
|
||||
|
||||
if energy_expended is not None and (energy_expended < 0 or energy_expended > 0xFFFF):
|
||||
raise ValueError('energy_expended out of range')
|
||||
|
||||
if rr_intervals:
|
||||
for rr_interval in rr_intervals:
|
||||
if rr_interval < 0 or rr_interval * 1024 > 0xFFFF:
|
||||
raise ValueError('rr_intervals out of range')
|
||||
|
||||
self.heart_rate = heart_rate
|
||||
self.sensor_contact_detected = sensor_contact_detected
|
||||
self.energy_expended = energy_expended
|
||||
self.rr_intervals = rr_intervals
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
flags = data[0]
|
||||
offset = 1
|
||||
|
||||
if flags & 1:
|
||||
hr = struct.unpack_from('<H', data, offset)[0]
|
||||
offset += 2
|
||||
else:
|
||||
hr = struct.unpack_from('B', data, offset)[0]
|
||||
offset += 1
|
||||
|
||||
if flags & (1 << 2):
|
||||
sensor_contact_detected = (flags & (1 << 1) != 0)
|
||||
else:
|
||||
sensor_contact_detected = None
|
||||
|
||||
if flags & (1 << 3):
|
||||
energy_expended = struct.unpack_from('<H', data, offset)[0]
|
||||
offset += 2
|
||||
else:
|
||||
energy_expended = None
|
||||
|
||||
if flags & (1 << 4):
|
||||
rr_intervals = tuple(
|
||||
struct.unpack_from('<H', data, offset + i * 2)[0] / 1024
|
||||
for i in range((len(data) - offset) // 2)
|
||||
)
|
||||
else:
|
||||
rr_intervals = ()
|
||||
|
||||
return cls(hr, sensor_contact_detected, energy_expended, rr_intervals)
|
||||
|
||||
def __bytes__(self):
|
||||
if self.heart_rate < 256:
|
||||
flags = 0
|
||||
data = struct.pack('B', self.heart_rate)
|
||||
else:
|
||||
flags = 1
|
||||
data = struct.pack('<H', self.heart_rate)
|
||||
|
||||
if self.sensor_contact_detected is not None:
|
||||
flags |= ((1 if self.sensor_contact_detected else 0) << 1) | (1 << 2)
|
||||
|
||||
if self.energy_expended is not None:
|
||||
flags |= (1 << 3)
|
||||
data += struct.pack('<H', self.energy_expended)
|
||||
|
||||
if self.rr_intervals:
|
||||
flags |= (1 << 4)
|
||||
data += b''.join([
|
||||
struct.pack('<H', int(rr_interval * 1024))
|
||||
for rr_interval in self.rr_intervals
|
||||
])
|
||||
|
||||
return bytes([flags]) + data
|
||||
|
||||
def __str__(self):
|
||||
return f'HeartRateMeasurement(heart_rate={self.heart_rate},'\
|
||||
f' sensor_contact_detected={self.sensor_contact_detected},'\
|
||||
f' energy_expended={self.energy_expended},'\
|
||||
f' rr_intervals={self.rr_intervals})'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
read_heart_rate_measurement,
|
||||
body_sensor_location=None,
|
||||
reset_energy_expended=None
|
||||
):
|
||||
self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||
Characteristic.NOTIFY,
|
||||
0,
|
||||
CharacteristicValue(read=read_heart_rate_measurement)
|
||||
),
|
||||
encode=lambda value: bytes(value)
|
||||
)
|
||||
characteristics = [self.heart_rate_measurement_characteristic]
|
||||
|
||||
if body_sensor_location is not None:
|
||||
self.body_sensor_location_characteristic = Characteristic(
|
||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||
Characteristic.READ,
|
||||
Characteristic.READABLE,
|
||||
bytes([int(body_sensor_location)])
|
||||
)
|
||||
characteristics.append(self.body_sensor_location_characteristic)
|
||||
|
||||
if reset_energy_expended:
|
||||
def write_heart_rate_control_point_value(connection, value):
|
||||
if value == self.RESET_ENERGY_EXPENDED:
|
||||
if reset_energy_expended is not None:
|
||||
reset_energy_expended(connection)
|
||||
else:
|
||||
raise ATT_Error(self.CONTROL_POINT_NOT_SUPPORTED)
|
||||
|
||||
self.heart_rate_control_point_characteristic = PackedCharacteristicAdapter(
|
||||
Characteristic(
|
||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||
Characteristic.WRITE,
|
||||
Characteristic.WRITEABLE,
|
||||
CharacteristicValue(write=write_heart_rate_control_point_value)
|
||||
),
|
||||
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT
|
||||
)
|
||||
characteristics.append(self.heart_rate_control_point_characteristic)
|
||||
|
||||
super().__init__(characteristics)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HeartRateServiceProxy(ProfileServiceProxy):
|
||||
SERVICE_CLASS = HeartRateService
|
||||
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC):
|
||||
self.heart_rate_measurement = DelegatedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
decode=HeartRateService.HeartRateMeasurement.from_bytes
|
||||
)
|
||||
else:
|
||||
self.heart_rate_measurement = None
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC):
|
||||
self.body_sensor_location = DelegatedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
decode=lambda value: HeartRateService.BodySensorLocation(value[0])
|
||||
)
|
||||
else:
|
||||
self.body_sensor_location = None
|
||||
|
||||
if characteristics := service_proxy.get_characteristics_by_uuid(GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC):
|
||||
self.heart_rate_control_point = PackedCharacteristicAdapter(
|
||||
characteristics[0],
|
||||
format=HeartRateService.HEART_RATE_CONTROL_POINT_FORMAT
|
||||
)
|
||||
else:
|
||||
self.heart_rate_control_point = None
|
||||
|
||||
async def reset_energy_expended(self):
|
||||
if self.heart_rate_control_point is not None:
|
||||
return await self.heart_rate_control_point.write_value(HeartRateService.RESET_ENERGY_EXPENDED)
|
||||
+58
-27
@@ -150,6 +150,8 @@ SMP_SC_AUTHREQ = 0b00001000
|
||||
SMP_KEYPRESS_AUTHREQ = 0b00010000
|
||||
SMP_CT2_AUTHREQ = 0b00100000
|
||||
|
||||
# Crypto salt
|
||||
SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
@@ -457,9 +459,17 @@ class PairingDelegate:
|
||||
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
||||
DEFAULT_KEY_DISTRIBUTION = (SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG)
|
||||
|
||||
def __init__(self, io_capability = NO_OUTPUT_NO_INPUT):
|
||||
def __init__(
|
||||
self,
|
||||
io_capability=NO_OUTPUT_NO_INPUT,
|
||||
local_initiator_key_distribution=DEFAULT_KEY_DISTRIBUTION,
|
||||
local_responder_key_distribution=DEFAULT_KEY_DISTRIBUTION
|
||||
):
|
||||
self.io_capability = io_capability
|
||||
self.local_initiator_key_distribution = local_initiator_key_distribution
|
||||
self.local_responder_key_distribution = local_responder_key_distribution
|
||||
|
||||
async def accept(self):
|
||||
return True
|
||||
@@ -473,6 +483,14 @@ class PairingDelegate:
|
||||
async def display_number(self, number, digits=6):
|
||||
pass
|
||||
|
||||
async def key_distribution_response(self, peer_initiator_key_distribution, peer_responder_key_distribution):
|
||||
return (
|
||||
(peer_initiator_key_distribution &
|
||||
self.local_initiator_key_distribution),
|
||||
(peer_responder_key_distribution &
|
||||
self.local_responder_key_distribution)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PairingConfig:
|
||||
@@ -559,6 +577,7 @@ class Session:
|
||||
self.ltk = None
|
||||
self.ltk_ediv = 0
|
||||
self.ltk_rand = bytes(8)
|
||||
self.link_key = None
|
||||
self.initiator_key_distribution = 0
|
||||
self.responder_key_distribution = 0
|
||||
self.peer_random_value = None
|
||||
@@ -596,11 +615,8 @@ class Session:
|
||||
self.pairing_result = None
|
||||
|
||||
# Key Distribution (default values before negotiation)
|
||||
self.initiator_key_distribution = (
|
||||
SMP_ENC_KEY_DISTRIBUTION_FLAG |
|
||||
SMP_ID_KEY_DISTRIBUTION_FLAG # |SMP_SIGN_KEY_DISTRIBUTION_FLAG
|
||||
)
|
||||
self.responder_key_distribution = self.initiator_key_distribution
|
||||
self.initiator_key_distribution = pairing_config.delegate.local_initiator_key_distribution
|
||||
self.responder_key_distribution = pairing_config.delegate.local_responder_key_distribution
|
||||
|
||||
# Authentication Requirements Flags - Vol 3, Part H, Figure 3.3
|
||||
self.bonding = pairing_config.bonding
|
||||
@@ -870,47 +886,56 @@ class Session:
|
||||
self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk))
|
||||
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
||||
|
||||
# Distribute IRK
|
||||
# Distribute IRK & BD ADDR
|
||||
if self.initiator_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG:
|
||||
self.send_command(
|
||||
SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk)
|
||||
)
|
||||
|
||||
# Distribute BD ADDR
|
||||
self.send_command(SMP_Identity_Address_Information_Command(
|
||||
addr_type = self.manager.address.address_type,
|
||||
bd_addr = self.manager.address
|
||||
))
|
||||
self.send_command(SMP_Identity_Address_Information_Command(
|
||||
addr_type = self.manager.address.address_type,
|
||||
bd_addr = self.manager.address
|
||||
))
|
||||
|
||||
# Distribute CSRK
|
||||
csrk = bytes(16) # FIXME: testing
|
||||
if self.initiator_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG:
|
||||
self.send_command(SMP_Signing_Information_Command(signature_key=csrk))
|
||||
|
||||
# CTKD, calculate BR/EDR link key
|
||||
if self.initiator_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG:
|
||||
ilk = crypto.h7(
|
||||
salt=SMP_CTKD_H7_LEBR_SALT,
|
||||
w=self.ltk) if self.ct2 else crypto.h6(self.ltk, b'tmp1')
|
||||
self.link_key = crypto.h6(ilk, b'lebr')
|
||||
|
||||
else:
|
||||
# Distribute the LTK
|
||||
# Distribute the LTK, EDIV and RAND
|
||||
if not self.sc:
|
||||
if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG:
|
||||
self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk))
|
||||
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
||||
|
||||
# Distribute EDIV and RAND
|
||||
self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand))
|
||||
|
||||
# Distribute IRK
|
||||
# Distribute IRK & BD ADDR
|
||||
if self.responder_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG:
|
||||
self.send_command(
|
||||
SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk)
|
||||
)
|
||||
|
||||
# Distribute BD ADDR
|
||||
self.send_command(SMP_Identity_Address_Information_Command(
|
||||
addr_type = self.manager.address.address_type,
|
||||
bd_addr = self.manager.address
|
||||
))
|
||||
self.send_command(SMP_Identity_Address_Information_Command(
|
||||
addr_type = self.manager.address.address_type,
|
||||
bd_addr = self.manager.address
|
||||
))
|
||||
|
||||
# Distribute CSRK
|
||||
csrk = bytes(16) # FIXME: testing
|
||||
if self.responder_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG:
|
||||
self.send_command(SMP_Signing_Information_Command(signature_key=csrk))
|
||||
|
||||
# CTKD, calculate BR/EDR link key
|
||||
if self.responder_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG:
|
||||
ilk = crypto.h7(
|
||||
salt=SMP_CTKD_H7_LEBR_SALT,
|
||||
w=self.ltk) if self.ct2 else crypto.h6(self.ltk, b'tmp1')
|
||||
self.link_key = crypto.h6(ilk, b'lebr')
|
||||
|
||||
def compute_peer_expected_distributions(self, key_distribution_flags):
|
||||
# Set our expectations for what to wait for in the key distribution phase
|
||||
@@ -945,7 +970,7 @@ class Session:
|
||||
# Nothing left to expect, we're done
|
||||
self.on_pairing()
|
||||
else:
|
||||
logger.warn(color('!!! unexpected key distribution command', 'red'))
|
||||
logger.warn(color(f'!!! unexpected key distribution command: {command_class.__name__}', 'red'))
|
||||
self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR)
|
||||
|
||||
async def pair(self):
|
||||
@@ -1029,6 +1054,11 @@ class Session:
|
||||
value = self.peer_signature_key,
|
||||
authenticated = authenticated
|
||||
)
|
||||
if self.link_key is not None:
|
||||
keys.link_key = PairingKeys.Key(
|
||||
value = self.link_key,
|
||||
authenticated = authenticated
|
||||
)
|
||||
|
||||
self.manager.on_pairing(self, peer_address, keys)
|
||||
|
||||
@@ -1076,6 +1106,7 @@ class Session:
|
||||
# Bonding and SC require both sides to request/support it
|
||||
self.bonding = self.bonding and (command.auth_req & SMP_BONDING_AUTHREQ != 0)
|
||||
self.sc = self.sc and (command.auth_req & SMP_SC_AUTHREQ != 0)
|
||||
self.ct2 = self.ct2 and (command.auth_req & SMP_CT2_AUTHREQ != 0)
|
||||
|
||||
# Check for OOB
|
||||
if command.oob_data_flag != 0:
|
||||
@@ -1091,8 +1122,8 @@ class Session:
|
||||
logger.debug(f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}')
|
||||
|
||||
# Key distribution
|
||||
self.initiator_key_distribution &= command.initiator_key_distribution
|
||||
self.responder_key_distribution &= command.responder_key_distribution
|
||||
self.initiator_key_distribution, self.responder_key_distribution = await self.pairing_config.delegate.key_distribution_response(
|
||||
command.initiator_key_distribution, command.responder_key_distribution)
|
||||
self.compute_peer_expected_distributions(self.initiator_key_distribution)
|
||||
|
||||
# The pairing is now starting
|
||||
|
||||
@@ -49,7 +49,7 @@ async def main():
|
||||
# Discover the Battery Service
|
||||
peer = Peer(connection)
|
||||
print('=== Discovering Battery Service')
|
||||
battery_service = await peer.discover_and_create_service_proxy(BatteryServiceProxy)
|
||||
battery_service = await peer.discover_service_and_create_proxy(BatteryServiceProxy)
|
||||
|
||||
# Check that the service was found
|
||||
if not battery_service:
|
||||
|
||||
@@ -38,7 +38,7 @@ async def main():
|
||||
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||
|
||||
# Add a Device Information Service and Battery Service to the GATT sever
|
||||
# Add a Battery Service to the GATT sever
|
||||
battery_service = BatteryService(lambda _: random.randint(0, 100))
|
||||
device.add_service(battery_service)
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from colors import color
|
||||
from bumble.device import Device, Peer
|
||||
from bumble.transport import open_transport
|
||||
from bumble.profiles.heart_rate_service import HeartRateServiceProxy
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main():
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: heart_rate_client.py <transport-spec> <bluetooth-address>')
|
||||
print('example: heart_rate_client.py usb:0 E1:CA:72:48:C4:E8')
|
||||
return
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport(sys.argv[1]) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Create and start a device
|
||||
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
|
||||
await device.power_on()
|
||||
|
||||
# Connect to the peer
|
||||
target_address = sys.argv[2]
|
||||
print(f'=== Connecting to {target_address}...')
|
||||
connection = await device.connect(target_address)
|
||||
print(f'=== Connected to {connection}')
|
||||
|
||||
# Discover the Heart Rate Service
|
||||
peer = Peer(connection)
|
||||
print('=== Discovering Heart Rate Service')
|
||||
heart_rate_service = await peer.discover_service_and_create_proxy(HeartRateServiceProxy)
|
||||
|
||||
# Check that the service was found
|
||||
if not heart_rate_service:
|
||||
print('!!! Service not found')
|
||||
return
|
||||
|
||||
# Read the body sensor location
|
||||
if heart_rate_service.body_sensor_location:
|
||||
location = await heart_rate_service.body_sensor_location.read_value()
|
||||
print(color('Sensor Location:', 'green'), location)
|
||||
|
||||
# Subscribe to the heart rate measurement
|
||||
if heart_rate_service.heart_rate_measurement:
|
||||
await heart_rate_service.heart_rate_measurement.subscribe(
|
||||
lambda value: print(f'{color("Heart Rate Measurement:", "green")} {value}')
|
||||
)
|
||||
|
||||
await hci_source.wait_for_termination()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import sys
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import struct
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.profiles.device_information_service import DeviceInformationService
|
||||
from bumble.profiles.heart_rate_service import HeartRateService
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main():
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: python heart_rate_server.py <device-config> <transport-spec>')
|
||||
print('example: python heart_rate_server.py device1.json usb:0')
|
||||
return
|
||||
|
||||
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||
|
||||
# Keep track of accumulated expended energy
|
||||
energy_start_time = time.time()
|
||||
|
||||
def reset_energy_expended():
|
||||
nonlocal energy_start_time
|
||||
energy_start_time = time.time()
|
||||
|
||||
# Add a Device Information Service and Heart Rate Service to the GATT sever
|
||||
device_information_service = DeviceInformationService(
|
||||
manufacturer_name = 'ACME',
|
||||
model_number = 'HR-102',
|
||||
serial_number = '7654321',
|
||||
hardware_revision = '1.1.3',
|
||||
software_revision = '2.5.6',
|
||||
system_id = (0x123456, 0x8877665544)
|
||||
)
|
||||
|
||||
heart_rate_service = HeartRateService(
|
||||
read_heart_rate_measurement = lambda _: HeartRateService.HeartRateMeasurement(
|
||||
heart_rate = 100 + int(50 * math.sin(time.time() * math.pi / 60)),
|
||||
sensor_contact_detected = random.choice((True, False, None)),
|
||||
energy_expended = random.choice((int((time.time() - energy_start_time) * 100), None)),
|
||||
rr_intervals = random.choice(((random.randint(900, 1100) / 1000, random.randint(900, 1100) / 1000), None))
|
||||
),
|
||||
body_sensor_location=HeartRateService.BodySensorLocation.WRIST,
|
||||
reset_energy_expended=lambda _: reset_energy_expended()
|
||||
)
|
||||
|
||||
device.add_services([device_information_service, heart_rate_service])
|
||||
|
||||
# Set the advertising data
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData([
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Heart', 'utf-8')),
|
||||
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, bytes(heart_rate_service.uuid)),
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
|
||||
])
|
||||
)
|
||||
|
||||
# Go!
|
||||
await device.power_on()
|
||||
await device.start_advertising(auto_restart=True)
|
||||
|
||||
# Notify every 3 seconds
|
||||
while True:
|
||||
await asyncio.sleep(3.0)
|
||||
await device.notify_subscribers(heart_rate_service.heart_rate_measurement_characteristic)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
|
||||
asyncio.run(main())
|
||||
+27
-15
@@ -16,6 +16,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import pytest
|
||||
@@ -30,7 +31,8 @@ from bumble.smp import (
|
||||
PairingConfig,
|
||||
PairingDelegate,
|
||||
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
||||
SMP_CONFIRM_VALUE_FAILED_ERROR
|
||||
SMP_CONFIRM_VALUE_FAILED_ERROR,
|
||||
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||
)
|
||||
from bumble.core import ProtocolError
|
||||
|
||||
@@ -196,11 +198,28 @@ async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
IO_CAP = [
|
||||
PairingDelegate.NO_OUTPUT_NO_INPUT,
|
||||
PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||
PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||
PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
||||
PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
||||
]
|
||||
SC = [False, True]
|
||||
MITM = [False, True]
|
||||
# Key distribution is a 4-bit bitmask
|
||||
# IdKey is necessary for current SMP structure
|
||||
KEY_DIST = [i for i in range(16) if (i & SMP_ID_KEY_DISTRIBUTION_FLAG)]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_smp():
|
||||
@pytest.mark.parametrize('io_cap, sc, mitm, key_dist',
|
||||
itertools.product(IO_CAP, SC, MITM, KEY_DIST)
|
||||
)
|
||||
async def test_self_smp(io_cap, sc, mitm, key_dist):
|
||||
class Delegate(PairingDelegate):
|
||||
def __init__(self, name, io_capability):
|
||||
super().__init__(io_capability)
|
||||
def __init__(self, name, io_capability, local_initiator_key_distribution, local_responder_key_distribution):
|
||||
super().__init__(io_capability, local_initiator_key_distribution,
|
||||
local_responder_key_distribution)
|
||||
self.name = name
|
||||
self.reset()
|
||||
|
||||
@@ -240,17 +259,8 @@ async def test_self_smp():
|
||||
|
||||
pairing_config_sets = [('Initiator', [None]), ('Responder', [None])]
|
||||
for pairing_config_set in pairing_config_sets:
|
||||
for io_capability in [
|
||||
PairingDelegate.NO_OUTPUT_NO_INPUT,
|
||||
PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||
PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||
PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
||||
PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
||||
]:
|
||||
for sc in [False, True]:
|
||||
for mitm in [False, True]:
|
||||
delegate = Delegate(pairing_config_set[0], io_capability)
|
||||
pairing_config_set[1].append(PairingConfig(sc, mitm, True, delegate))
|
||||
delegate = Delegate(pairing_config_set[0], io_cap, key_dist, key_dist)
|
||||
pairing_config_set[1].append(PairingConfig(sc, mitm, True, delegate))
|
||||
|
||||
for pairing_config1 in pairing_config_sets[0][1]:
|
||||
for pairing_config2 in pairing_config_sets[1][1]:
|
||||
@@ -262,7 +272,9 @@ async def test_self_smp():
|
||||
if pairing_config1 and pairing_config2:
|
||||
pairing_config1.delegate.peer_delegate = pairing_config2.delegate
|
||||
pairing_config2.delegate.peer_delegate = pairing_config1.delegate
|
||||
|
||||
await _test_self_smp_with_configs(pairing_config1, pairing_config2)
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -176,6 +176,20 @@ def test_g2():
|
||||
assert(value == 0x2f9ed5ba)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_h6():
|
||||
KEY = bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||
KEY_ID = bytes.fromhex('6c656272')
|
||||
assert(h6(KEY, KEY_ID) == bytes.fromhex('2d9ae102 e76dc91c e8d3a9e2 80b16399'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_h7():
|
||||
KEY = bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')
|
||||
SALT = bytes.fromhex('00000000 00000000 00000000 746D7031')
|
||||
assert(h7(SALT, KEY) == bytes.fromhex('fb173597 c6a3c0ec d2998c2a 75a57011'))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_ah():
|
||||
irk = bytes(reversed(bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b')))
|
||||
@@ -195,4 +209,6 @@ if __name__ == '__main__':
|
||||
test_f5()
|
||||
test_f6()
|
||||
test_g2()
|
||||
test_h6()
|
||||
test_h7()
|
||||
test_ah()
|
||||
|
||||
Reference in New Issue
Block a user