Skip to content

Commit

Permalink
Merge pull request #35 from zxzxwu/ctkd
Browse files Browse the repository at this point in the history
Support CTKD over BR/EDR
  • Loading branch information
barbibulle committed Aug 30, 2022
2 parents 84d70ad + 99cba19 commit d188041
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 11 deletions.
2 changes: 2 additions & 0 deletions bumble/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ def __init__(self, name = None, address = None, config = None, host = None, gene
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)
Expand Down
7 changes: 4 additions & 3 deletions bumble/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@

# -----------------------------------------------------------------------------
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)
Expand Down Expand Up @@ -364,7 +365,7 @@ def on_hci_le_connection_complete_event(self, event):

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
Expand Down Expand Up @@ -399,7 +400,7 @@ def on_hci_connection_complete_event(self, event):

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
Expand Down
44 changes: 36 additions & 8 deletions bumble/smp.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
# Constants
# -----------------------------------------------------------------------------
SMP_CID = 0x06
SMP_BR_CID = 0x07

SMP_PAIRING_REQUEST_COMMAND = 0x01
SMP_PAIRING_RESPONSE_COMMAND = 0x02
Expand Down Expand Up @@ -152,6 +153,7 @@

# Crypto salt
SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031')
SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032')

# -----------------------------------------------------------------------------
# Utils
Expand Down Expand Up @@ -598,6 +600,7 @@ def __init__(self, manager, connection, pairing_config):
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)
Expand Down Expand Up @@ -876,11 +879,22 @@ def start_encryption(self, key):
)
)
)

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))
Expand Down Expand Up @@ -909,8 +923,11 @@ def distribute_keys(self):
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))
Expand Down Expand Up @@ -940,7 +957,7 @@ def distribute_keys(self):
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)
Expand Down Expand Up @@ -968,7 +985,7 @@ def check_key_distribution(self, command_class):
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)
Expand Down Expand Up @@ -999,7 +1016,7 @@ def on_connection_encryption_key_refresh(self):
# 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:
Expand All @@ -1016,11 +1033,16 @@ def on_pairing(self):
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
Expand Down Expand Up @@ -1059,7 +1081,6 @@ def on_pairing(self):
value = self.link_key,
authenticated = authenticated
)

self.manager.on_pairing(self, peer_address, keys)

def on_pairing_failure(self, reason):
Expand Down Expand Up @@ -1137,6 +1158,12 @@ async def on_smp_pairing_request_command_async(self, command):
# 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'))
Expand Down Expand Up @@ -1462,7 +1489,8 @@ def __init__(self, device, address):

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
Expand Down

0 comments on commit d188041

Please sign in to comment.