add classic pairing io delegation

This commit is contained in:
Gilles Boccon-Gibod
2022-06-11 01:33:51 -07:00
parent 1af61e8af3
commit b4e2f21d2a
6 changed files with 281 additions and 76 deletions

View File

@@ -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))
# -----------------------------------------------------------------------------

View File

@@ -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):

View File

@@ -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),

View File

@@ -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')

View File

@@ -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

View File

@@ -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())