diff --git a/apps/pair.py b/apps/pair.py index 8c14ddcc..73d71c8f 100644 --- a/apps/pair.py +++ b/apps/pair.py @@ -44,7 +44,7 @@ from bumble.att import ( # ----------------------------------------------------------------------------- class Delegate(PairingDelegate): - def __init__(self, connection, capability_string, prompt): + def __init__(self, mode, connection, capability_string, prompt): super().__init__({ 'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY, 'display': PairingDelegate.DISPLAY_OUTPUT_ONLY, @@ -53,6 +53,7 @@ class Delegate(PairingDelegate): 'none': PairingDelegate.NO_OUTPUT_NO_INPUT }[capability_string.lower()]) + self.mode = mode self.peer = Peer(connection) self.peer_name = None self.prompt = prompt @@ -62,12 +63,16 @@ class Delegate(PairingDelegate): # We already asked the peer return - # Try to get the peer's name - if self.peer: - peer_name = await get_peer_name(self.peer) - self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]' + # For classic, just use the address + if self.mode == 'classic': + self.peer_name = str(self.peer.connection.peer_address) else: - self.peer_name = '[?]' + # Try to get the peer's name + if self.peer: + peer_name = await get_peer_name(self.peer) + self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]' + else: + self.peer_name = '[?]' async def accept(self): if self.prompt: @@ -91,7 +96,7 @@ class Delegate(PairingDelegate): # Accept silently return True - async def compare_numbers(self, number): + async def compare_numbers(self, number, digits): await self.update_peer_name() # Wait a bit to allow some of the log lines to print before we prompt @@ -102,7 +107,7 @@ class Delegate(PairingDelegate): print(color(f'### Pairing with {self.peer_name}', 'yellow')) print(color('###-----------------------------------', 'yellow')) while True: - response = await aioconsole.ainput(color(f'>>> Does the other device display {number:06}? ', 'yellow')) + response = await aioconsole.ainput(color(f'>>> Does the other device display {number:{digits}}? ', 'yellow')) response = response.lower().strip() if response == 'yes': return True @@ -125,7 +130,7 @@ class Delegate(PairingDelegate): except ValueError: pass - async def display_number(self, number): + async def display_number(self, number, digits): await self.update_peer_name() # Wait a bit to allow some of the log lines to print before we prompt @@ -134,7 +139,7 @@ class Delegate(PairingDelegate): # Display a PIN code print(color('###-----------------------------------', 'yellow')) print(color(f'### Pairing with {self.peer_name}', 'yellow')) - print(color(f'### PIN: {number:06}', 'yellow')) + print(color(f'### PIN: {number:0{digits}}', 'yellow')) print(color('###-----------------------------------', 'yellow')) @@ -224,9 +229,22 @@ def on_pairing_failure(reason): # ----------------------------------------------------------------------------- -async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name): +async def pair( + mode, + sc, + mitm, + bond, + io, + prompt, + request, + print_keys, + keystore_file, + device_config, + hci_transport, + address_or_name +): print('<<< connecting to HCI...') - async with await open_transport_or_link(transport) as (hci_source, hci_sink): + async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink): print('<<< connected') # Create a device to manage the host @@ -245,19 +263,25 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d # Expose a GATT characteristic that can be used to trigger pairing by # responding with an authentication error when read - device.add_service( - Service( - '50DB505C-8AC4-4738-8448-3B1D9CC09CC5', - [ - Characteristic( - '552957FB-CF1F-4A31-9535-E78847E1A714', - Characteristic.READ | Characteristic.WRITE, - Characteristic.READABLE | Characteristic.WRITEABLE, - CharacteristicValue(read=read_with_error, write=write_with_error) - ) - ] + if mode == 'le': + device.add_service( + Service( + '50DB505C-8AC4-4738-8448-3B1D9CC09CC5', + [ + Characteristic( + '552957FB-CF1F-4A31-9535-E78847E1A714', + Characteristic.READ | Characteristic.WRITE, + Characteristic.READABLE | Characteristic.WRITEABLE, + CharacteristicValue(read=read_with_error, write=write_with_error) + ) + ] + ) ) - ) + + # Select LE or Classic + if mode == 'classic': + device.classic_enabled = True + device.le_enabled = False # Get things going await device.power_on() @@ -267,7 +291,7 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d sc, mitm, bond, - Delegate(connection, io, prompt) + Delegate(mode, connection, io, prompt) ) # Connect to a peer or wait for a connection @@ -278,10 +302,14 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d if not request: try: - await connection.pair() + if mode == 'le': + await connection.pair() + else: + await connection.authenticate() + return + except ProtocolError as error: + print(color(f'Pairing failed: {error}', 'red')) return - except ProtocolError: - pass else: # Advertise so that peers can find us and connect await device.start_advertising(auto_restart=True) @@ -291,6 +319,7 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d # ----------------------------------------------------------------------------- @click.command() +@click.option('--mode', type=click.Choice(['le', 'classic']), default='le', show_default=True) @click.option('--sc', type=bool, default=True, help='Use the Secure Connections protocol', show_default=True) @click.option('--mitm', type=bool, default=True, help='Request MITM protection', show_default=True) @click.option('--bond', type=bool, default=True, help='Enable bonding', show_default=True) @@ -300,11 +329,11 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d @click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing') @click.option('--keystore-file', help='File in which to store the pairing keys') @click.argument('device-config') -@click.argument('transport') +@click.argument('hci_transport') @click.argument('address-or-name', required=False) -def main(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name): +def main(mode, sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, hci_transport, address_or_name): logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) - asyncio.run(pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name)) + asyncio.run(pair(mode, sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, hci_transport, address_or_name)) # ----------------------------------------------------------------------------- diff --git a/bumble/device.py b/bumble/device.py index 4dc71b05..df5cffd9 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -292,7 +292,18 @@ def with_connection_from_handle(function): if (connection := self.lookup_connection(connection_handle)) is None: logger.warn(f'no connection found for handle 0x{connection_handle:04X}') return - function(self, connection, *args, **kwargs) + return function(self, connection, *args, **kwargs) + return wrapper + + +# Decorator that converts the first argument from a bluetooth address to a connection +def with_connection_from_address(function): + @functools.wraps(function) + def wrapper(self, address, *args, **kwargs): + for connection in self.connections.values(): + if connection.peer_address == address: + return function(self, connection, *args, **kwargs) + logger.warn(f'no connection found for address {address}') return wrapper @@ -453,6 +464,11 @@ class Device(CompositeEventEmitter): if connection := self.connections.get(connection_handle): return connection + def find_connection_by_bd_addr(self, bd_addr): + for connection in self.connections.values(): + if connection.peer_address == bd_addr: + return connection + def register_l2cap_server(self, psm, server): self.l2cap_channel_manager.register_server(psm, server) @@ -936,13 +952,13 @@ class Device(CompositeEventEmitter): # Set up event handlers pending_authentication = asyncio.get_running_loop().create_future() - def on_authentication_complete(): + def on_authentication(): pending_authentication.set_result(None) - def on_authentication_failure(error): - pending_authentication.set_exception(error) + def on_authentication_failure(error_code): + pending_authentication.set_exception(HCI_Error(error_code)) - connection.on('connection_authentication_complete', on_authentication_complete) + connection.on('connection_authentication', on_authentication) connection.on('connection_authentication_failure', on_authentication_failure) # Request the authentication @@ -957,7 +973,7 @@ class Device(CompositeEventEmitter): # Wait for the authentication to complete await pending_authentication finally: - connection.remove_listener('connection_authentication_complete', on_authentication_complete) + connection.remove_listener('connection_authentication', on_authentication) connection.remove_listener('connection_authentication_failure', on_authentication_failure) async def encrypt(self, connection): @@ -1141,10 +1157,10 @@ class Device(CompositeEventEmitter): @host_event_handler @with_connection_from_handle - def on_connection_authentication_complete(self, connection): - logger.debug(f'*** Connection Authentication Complete: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}') + def on_connection_authentication(self, connection): + logger.debug(f'*** Connection Authentication: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}') connection.authenticated = True - connection.emit('connection_authentication_complete') + connection.emit('connection_authentication') @host_event_handler @with_connection_from_handle @@ -1152,6 +1168,110 @@ class Device(CompositeEventEmitter): logger.debug(f'*** Connection Authentication Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}') connection.emit('connection_authentication_failure', error) + # [Classic only] + @host_event_handler + @with_connection_from_address + def on_authentication_io_capability_request(self, connection): + # Ask what the pairing config should be for this connection + pairing_config = self.pairing_config_factory(connection) + + # Map the SMP IO capability to a Classic IO capability + if (io_capability := { + smp.SMP_DISPLAY_ONLY_IO_CAPABILITY: HCI_DISPLAY_ONLY_IO_CAPABILITY, + smp.SMP_DISPLAY_YES_NO_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY, + smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY: HCI_KEYBOARD_ONLY_IO_CAPABILITY, + smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY, + smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY + }.get(pairing_config.delegate.io_capability)) is None: + logger.warning(f'cannot map IO capability ({pairing_config.delegate.io_capability}') + io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY + + # Compute the authentication requirements + authentication_requirements = ( + ( + HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS, + HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS + ), + ( + HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS, + HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS + ) + )[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0] + + # Respond + self.host.send_command_sync( + HCI_IO_Capability_Request_Reply_Command( + bd_addr = connection.peer_address, + io_capability = io_capability, + oob_data_present = 0x00, + authentication_requirements = authentication_requirements + ) + ) + + # [Classic only] + @host_event_handler + @with_connection_from_address + def on_authentication_user_confirmation_request(self, connection, code): + # Ask what the pairing config should be for this connection + pairing_config = self.pairing_config_factory(connection) + + can_confirm = pairing_config.delegate.io_capability not in { + smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY, + smp.SMP_DISPLAY_ONLY_IO_CAPABILITY + } + + # Respond + if can_confirm and pairing_config.delegate: + async def compare_numbers(): + numbers_match = await pairing_config.delegate.compare_numbers(code, digits=6) + if numbers_match: + self.host.send_command_sync( + HCI_User_Confirmation_Request_Reply_Command(bd_addr=connection.peer_address) + ) + else: + self.host.send_command_sync( + HCI_User_Confirmation_Request_Negative_Reply_Command(bd_addr=connection.peer_address) + ) + + asyncio.create_task(compare_numbers()) + else: + self.host.send_command_sync( + HCI_User_Confirmation_Request_Reply_Command(bd_addr=connection.peer_address) + ) + + # [Classic only] + @host_event_handler + @with_connection_from_address + def on_authentication_user_passkey_request(self, connection): + # Ask what the pairing config should be for this connection + pairing_config = self.pairing_config_factory(connection) + + can_input = pairing_config.delegate.io_capability in { + smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY, + smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY + } + + # Respond + if can_input and pairing_config.delegate: + async def get_number(): + number = await pairing_config.delegate.get_number() + if number is not None: + self.host.send_command_sync( + HCI_User_Passkey_Request_Reply_Command( + bd_addr = connection.peer_address, + numeric_value = number) + ) + else: + self.host.send_command_sync( + HCI_User_Passkey_Request_Negative_Reply_Command(bd_addr=connection.peer_address) + ) + + asyncio.create_task(get_number()) + else: + self.host.send_command_sync( + HCI_User_Passkey_Request_Negative_Reply_Command(bd_addr=connection.peer_address) + ) + @host_event_handler @with_connection_from_handle def on_connection_encryption_change(self, connection, encryption): diff --git a/bumble/hci.py b/bumble/hci.py index a5af6479..057e4b9b 100644 --- a/bumble/hci.py +++ b/bumble/hci.py @@ -258,6 +258,9 @@ HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND = hci_command_ HCI_READ_CLOCK_OFFSET_COMMAND = hci_command_op_code(0x01, 0x001F) HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002B) HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002C) +HCI_USER_CONFIRMATION_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x01, 0x002D) +HCI_USER_PASSKEY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002E) +HCI_USER_PASSKEY_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x01, 0x002F) HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND = hci_command_op_code(0x01, 0x003D) HCI_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0003) HCI_EXIT_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0004) @@ -407,6 +410,9 @@ HCI_COMMAND_NAMES = { HCI_READ_CLOCK_OFFSET_COMMAND: 'HCI_READ_CLOCK_OFFSET_COMMAND', HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND: 'HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND', HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND: 'HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND', + HCI_USER_CONFIRMATION_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_USER_CONFIRMATION_REQUEST_NEGATIVE_REPLY_COMMAND', + HCI_USER_PASSKEY_REQUEST_REPLY_COMMAND: 'HCI_USER_PASSKEY_REQUEST_REPLY_COMMAND', + HCI_USER_PASSKEY_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_USER_PASSKEY_REQUEST_NEGATIVE_REPLY_COMMAND', HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND: 'HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND', HCI_SNIFF_MODE_COMMAND: 'HCI_SNIFF_MODE_COMMAND', HCI_EXIT_SNIFF_MODE_COMMAND: 'HCI_EXIT_SNIFF_MODE_COMMAND', @@ -683,20 +689,20 @@ HCI_IO_CAPABILITY_NAMES = { } # Authentication Requirements -HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x00 -HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x01 -HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x02 -HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x03 -HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x04 -HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x05 +HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS = 0x00 +HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS = 0x01 +HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS = 0x02 +HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS = 0x03 +HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS = 0x04 +HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS = 0x05 HCI_AUTHENTICATION_REQUIREMENTS_NAMES = { - HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS', - HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS', - HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS', - HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS', - HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS', - HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS' + HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS' } # Link Key Types @@ -1449,6 +1455,55 @@ class HCI_User_Confirmation_Request_Reply_Command(HCI_Command): ''' +# ----------------------------------------------------------------------------- +@HCI_Command.command( + fields=[ + ('bd_addr', Address.parse_address) + ], + return_parameters_fields=[ + ('status', STATUS_SPEC), + ('bd_addr', Address.parse_address) + ] +) +class HCI_User_Confirmation_Request_Negative_Reply_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.1.31 User Confirmation Request Negative Reply Command + ''' + + +# ----------------------------------------------------------------------------- +@HCI_Command.command( + fields=[ + ('bd_addr', Address.parse_address), + ('numeric_value', 4) + ], + return_parameters_fields=[ + ('status', STATUS_SPEC), + ('bd_addr', Address.parse_address) + ] +) +class HCI_User_Passkey_Request_Reply_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.1.32 User Passkey Request Reply Command + ''' + + +# ----------------------------------------------------------------------------- +@HCI_Command.command( + fields=[ + ('bd_addr', Address.parse_address) + ], + return_parameters_fields=[ + ('status', STATUS_SPEC), + ('bd_addr', Address.parse_address) + ] +) +class HCI_User_Passkey_Request_Negative_Reply_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.1.33 User Passkey Request Negative Reply Command + ''' + + # ----------------------------------------------------------------------------- @HCI_Command.command([ ('connection_handle', 2), @@ -3367,6 +3422,16 @@ class HCI_User_Confirmation_Request_Event(HCI_Event): ''' +# ----------------------------------------------------------------------------- +@HCI_Event.event([ + ('bd_addr', Address.parse_address) +]) +class HCI_User_Passkey_Request_Event(HCI_Event): + ''' + See Bluetooth spec @ 7.7.43 User Passkey Request Event + ''' + + # ----------------------------------------------------------------------------- @HCI_Event.event([ ('status', STATUS_SPEC), diff --git a/bumble/host.py b/bumble/host.py index 68859ca8..db58dc6d 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -85,6 +85,7 @@ class Host(EventEmitter): self.command_semaphore = asyncio.Semaphore(1) self.long_term_key_provider = None self.link_key_provider = None + self.pairing_io_capability_provider = None # Classic only # Connect to the source and sink if specified if controller_source: @@ -495,7 +496,7 @@ class Host(EventEmitter): def on_hci_authentication_complete_event(self, event): # Notify the client if event.status == HCI_SUCCESS: - self.emit('connection_authentication_complete', event.connection_handle) + self.emit('connection_authentication', event.connection_handle) else: self.emit('connection_authentication_failure', event.connection_handle, event.status) @@ -560,26 +561,16 @@ class Host(EventEmitter): asyncio.create_task(send_link_key()) def on_hci_io_capability_request_event(self, event): - # For now, just return NoInputNoOutput and no MITM - # TODO: delegate the decision - self.send_command_sync( - HCI_IO_Capability_Request_Reply_Command( - bd_addr = event.bd_addr, - io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY, - oob_data_present = 0x00, - authentication_requirements = 0x00 # 0x02 # FIXME: testing only - ) - ) + self.emit('authentication_io_capability_request', event.bd_addr) def on_hci_io_capability_response_event(self, event): pass def on_hci_user_confirmation_request_event(self, event): - # For now, just confirm everything - # TODO: delegate the decision - self.send_command_sync( - HCI_User_Confirmation_Request_Reply_Command(bd_addr = event.bd_addr) - ) + self.emit('authentication_user_confirmation_request', event.bd_addr, event.numeric_value) + + def on_hci_user_passkey_request_event(self, event): + self.emit('authentication_user_passkey_request', event.bd_addr) def on_hci_inquiry_complete_event(self, event): self.emit('inquiry_complete') diff --git a/bumble/smp.py b/bumble/smp.py index 7efb8f59..c25e0146 100644 --- a/bumble/smp.py +++ b/bumble/smp.py @@ -464,13 +464,13 @@ class PairingDelegate: async def accept(self): return True - async def compare_numbers(self, number): + async def compare_numbers(self, number, digits=6): return True async def get_number(self): return 0 - async def display_number(self, number): + async def display_number(self, number, digits=6): pass @@ -699,7 +699,7 @@ class Session: async def prompt(): logger.debug(f'verification code: {code}') try: - response = await self.pairing_config.delegate.compare_numbers(code) + response = await self.pairing_config.delegate.compare_numbers(code, digits=6) if response: next_steps() return @@ -733,7 +733,7 @@ class Session: self.tk = self.passkey.to_bytes(16, byteorder='little') logger.debug(f'TK from passkey = {self.tk.hex()}') - asyncio.create_task(self.pairing_config.delegate.display_number(self.passkey)) + asyncio.create_task(self.pairing_config.delegate.display_number(self.passkey, digits=6)) def input_passkey(self, next_steps=None): # Prompt the user for the passkey displayed on the peer diff --git a/tests/self_test.py b/tests/self_test.py index 9f956135..0b3762a0 100644 --- a/tests/self_test.py +++ b/tests/self_test.py @@ -208,11 +208,11 @@ async def test_self_smp(): self.peer_delegate = None self.number = asyncio.get_running_loop().create_future() - async def compare_numbers(self, number): + async def compare_numbers(self, number, digits): if self.peer_delegate is None: logger.warn(f'[{self.name}] no peer delegate') return False - await self.display_number(number) + await self.display_number(number, digits=6) logger.debug(f'[{self.name}] waiting for peer number') peer_number = await self.peer_delegate.number logger.debug(f'[{self.name}] comparing numbers: {number} and {peer_number}') @@ -231,7 +231,7 @@ async def test_self_smp(): logger.debug(f'[{self.name}] returning number: {peer_number}') return peer_number - async def display_number(self, number): + async def display_number(self, number, digits): logger.debug(f'[{self.name}] displaying number: {number}') self.number.set_result(number) @@ -293,7 +293,7 @@ async def test_self_smp_wrong_pin(): def __init__(self): super().__init__(PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT) - async def compare_numbers(self, number): + async def compare_numbers(self, number, digits): return False wrong_pin_pairing_config = PairingConfig(delegate = WrongPinDelegate())