diff --git a/bumble/device.py b/bumble/device.py index cb04e7cf..0b7a1ef6 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -497,6 +497,8 @@ class Device(CompositeEventEmitter): self.smp_manager = smp.Manager(self, self.random_address) self.l2cap_channel_manager.register_fixed_channel( smp.SMP_CID, self.on_smp_pdu) + self.l2cap_channel_manager.register_fixed_channel( + smp.SMP_BR_CID, self.on_smp_pdu) # Register the SDP server with the L2CAP Channel Manager self.sdp_server.register(self.l2cap_channel_manager) diff --git a/bumble/host.py b/bumble/host.py index 7175412a..70a3f999 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -44,12 +44,13 @@ HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1 # ----------------------------------------------------------------------------- class Connection: - def __init__(self, host, handle, role, peer_address): + def __init__(self, host, handle, role, peer_address, transport): self.host = host self.handle = handle self.role = role self.peer_address = peer_address self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu) + self.transport = transport def on_hci_acl_data_packet(self, packet): self.assembler.feed_packet(packet) @@ -350,7 +351,7 @@ class Host(EventEmitter): connection = self.connections.get(event.connection_handle) if connection is None: - connection = Connection(self, event.connection_handle, event.role, event.peer_address) + connection = Connection(self, event.connection_handle, event.role, event.peer_address, BT_LE_TRANSPORT) self.connections[event.connection_handle] = connection # Notify the client @@ -385,7 +386,7 @@ class Host(EventEmitter): connection = self.connections.get(event.connection_handle) if connection is None: - connection = Connection(self, event.connection_handle, BT_CENTRAL_ROLE, event.bd_addr) + connection = Connection(self, event.connection_handle, BT_CENTRAL_ROLE, event.bd_addr, BT_BR_EDR_TRANSPORT) self.connections[event.connection_handle] = connection # Notify the client diff --git a/bumble/smp.py b/bumble/smp.py index 7d978b26..17ca6edf 100644 --- a/bumble/smp.py +++ b/bumble/smp.py @@ -44,6 +44,7 @@ logger = logging.getLogger(__name__) # Constants # ----------------------------------------------------------------------------- SMP_CID = 0x06 +SMP_BR_CID = 0x07 SMP_PAIRING_REQUEST_COMMAND = 0x01 SMP_PAIRING_RESPONSE_COMMAND = 0x02 @@ -152,6 +153,7 @@ SMP_CT2_AUTHREQ = 0b00100000 # Crypto salt SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031') +SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032') # ----------------------------------------------------------------------------- # Utils @@ -598,6 +600,7 @@ class Session: self.pairing_config = pairing_config self.wait_before_continuing = None self.completed = False + self.ctkd_task = None # Decide if we're the initiator or the responder self.is_initiator = (connection.role == BT_CENTRAL_ROLE) @@ -876,11 +879,22 @@ class Session: ) ) ) + + async def derive_ltk(self): + link_key = await self.manager.device.get_link_key(self.connection.peer_address) + assert link_key is not None + ilk = crypto.h7( + salt=SMP_CTKD_H7_BRLE_SALT, + w=link_key) if self.ct2 else crypto.h6(link_key, b'tmp2') + self.ltk = crypto.h6(ilk, b'brle') def distribute_keys(self): # Distribute the keys as required if self.is_initiator: - if not self.sc: + # CTKD: Derive LTK from LinkKey + if self.connection.transport == BT_BR_EDR_TRANSPORT and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG: + self.ctkd_task = asyncio.create_task(self.derive_ltk()) + elif not self.sc: # Distribute the LTK, EDIV and RAND if self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG: self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk)) @@ -909,8 +923,11 @@ class Session: self.link_key = crypto.h6(ilk, b'lebr') else: + # CTKD: Derive LTK from LinkKey + if self.connection.transport == BT_BR_EDR_TRANSPORT and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG: + self.ctkd_task = asyncio.create_task(self.derive_ltk()) # Distribute the LTK, EDIV and RAND - if not self.sc: + elif 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)) @@ -940,7 +957,7 @@ class Session: def compute_peer_expected_distributions(self, key_distribution_flags): # Set our expectations for what to wait for in the key distribution phase self.peer_expected_distributions = [] - if not self.sc: + if not self.sc and self.connection.transport == BT_LE_TRANSPORT: if (key_distribution_flags & SMP_ENC_KEY_DISTRIBUTION_FLAG != 0): self.peer_expected_distributions.append(SMP_Encryption_Information_Command) self.peer_expected_distributions.append(SMP_Master_Identification_Command) @@ -968,7 +985,7 @@ class Session: self.distribute_keys() # Nothing left to expect, we're done - self.on_pairing() + asyncio.create_task(self.on_pairing()) else: logger.warn(color(f'!!! unexpected key distribution command: {command_class.__name__}', 'red')) self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR) @@ -999,7 +1016,7 @@ class Session: # Do as if the connection had just been encrypted self.on_connection_encryption_change() - def on_pairing(self): + async def on_pairing(self): logger.debug('pairing complete') if self.completed: @@ -1016,11 +1033,16 @@ class Session: else: peer_address = self.connection.peer_address + # Wait for link key fetch and key derivation + if self.ctkd_task is not None: + await self.ctkd_task + self.ctkd_task = None + # Create an object to hold the keys keys = PairingKeys() keys.address_type = peer_address.address_type authenticated = self.pairing_method != self.JUST_WORKS - if self.sc: + if self.sc or self.connection.transport == BT_BR_EDR_TRANSPORT: keys.ltk = PairingKeys.Key( value = self.ltk, authenticated = authenticated @@ -1059,7 +1081,6 @@ class Session: value = self.link_key, authenticated = authenticated ) - self.manager.on_pairing(self, peer_address, keys) def on_pairing_failure(self, reason): @@ -1137,6 +1158,12 @@ class Session: # Respond self.send_pairing_response_command() + # Vol 3, Part C, 5.2.2.1.3 + # CTKD over BR/EDR should happen after the connection has been encrypted, + # so when receiving pairing requests, responder should start distributing keys + if self.connection.transport == BT_BR_EDR_TRANSPORT and self.connection.is_encrypted and self.is_responder and accepted: + self.distribute_keys() + def on_smp_pairing_response_command(self, command): if self.is_responder: logger.warn(color('received pairing response as a responder', 'red')) @@ -1462,7 +1489,8 @@ class Manager(EventEmitter): def send_command(self, connection, command): logger.debug(f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}') - connection.send_l2cap_pdu(SMP_CID, command.to_bytes()) + cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID + connection.send_l2cap_pdu(cid, command.to_bytes()) def on_smp_pdu(self, connection, pdu): # Look for a session with this connection, and create one if none exists