forked from auracaster/bumble_mirror
Merge pull request #4 from google/gbg/classic-pairing-io
classic pairing io
This commit is contained in:
97
apps/pair.py
97
apps/pair.py
@@ -44,7 +44,7 @@ from bumble.att import (
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Delegate(PairingDelegate):
|
class Delegate(PairingDelegate):
|
||||||
def __init__(self, connection, capability_string, prompt):
|
def __init__(self, mode, connection, capability_string, prompt):
|
||||||
super().__init__({
|
super().__init__({
|
||||||
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY,
|
||||||
'display': PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
'display': PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
||||||
@@ -53,6 +53,7 @@ class Delegate(PairingDelegate):
|
|||||||
'none': PairingDelegate.NO_OUTPUT_NO_INPUT
|
'none': PairingDelegate.NO_OUTPUT_NO_INPUT
|
||||||
}[capability_string.lower()])
|
}[capability_string.lower()])
|
||||||
|
|
||||||
|
self.mode = mode
|
||||||
self.peer = Peer(connection)
|
self.peer = Peer(connection)
|
||||||
self.peer_name = None
|
self.peer_name = None
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
@@ -64,7 +65,7 @@ class Delegate(PairingDelegate):
|
|||||||
|
|
||||||
# Try to get the peer's name
|
# Try to get the peer's name
|
||||||
if self.peer:
|
if self.peer:
|
||||||
peer_name = await get_peer_name(self.peer)
|
peer_name = await get_peer_name(self.peer, self.mode)
|
||||||
self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]'
|
self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]'
|
||||||
else:
|
else:
|
||||||
self.peer_name = '[?]'
|
self.peer_name = '[?]'
|
||||||
@@ -91,7 +92,7 @@ class Delegate(PairingDelegate):
|
|||||||
# Accept silently
|
# Accept silently
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def compare_numbers(self, number):
|
async def compare_numbers(self, number, digits):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
# Wait a bit to allow some of the log lines to print before we prompt
|
||||||
@@ -102,7 +103,7 @@ class Delegate(PairingDelegate):
|
|||||||
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
print(color(f'### Pairing with {self.peer_name}', 'yellow'))
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
print(color('###-----------------------------------', 'yellow'))
|
||||||
while True:
|
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:0{digits}}? ', 'yellow'))
|
||||||
response = response.lower().strip()
|
response = response.lower().strip()
|
||||||
if response == 'yes':
|
if response == 'yes':
|
||||||
return True
|
return True
|
||||||
@@ -125,7 +126,7 @@ class Delegate(PairingDelegate):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def display_number(self, number):
|
async def display_number(self, number, digits):
|
||||||
await self.update_peer_name()
|
await self.update_peer_name()
|
||||||
|
|
||||||
# Wait a bit to allow some of the log lines to print before we prompt
|
# Wait a bit to allow some of the log lines to print before we prompt
|
||||||
@@ -134,19 +135,23 @@ class Delegate(PairingDelegate):
|
|||||||
# Display a PIN code
|
# Display a PIN code
|
||||||
print(color('###-----------------------------------', 'yellow'))
|
print(color('###-----------------------------------', 'yellow'))
|
||||||
print(color(f'### Pairing with {self.peer_name}', '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'))
|
print(color('###-----------------------------------', 'yellow'))
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def get_peer_name(peer):
|
async def get_peer_name(peer, mode):
|
||||||
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
if mode == 'classic':
|
||||||
if not services:
|
return await peer.request_name()
|
||||||
return None
|
else:
|
||||||
|
# Try to get the peer name from GATT
|
||||||
|
services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE)
|
||||||
|
if not services:
|
||||||
|
return None
|
||||||
|
|
||||||
values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0])
|
values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0])
|
||||||
if values:
|
if values:
|
||||||
return values[0].decode('utf-8')
|
return values[0].decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -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...')
|
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')
|
print('<<< connected')
|
||||||
|
|
||||||
# Create a device to manage the host
|
# 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
|
# Expose a GATT characteristic that can be used to trigger pairing by
|
||||||
# responding with an authentication error when read
|
# responding with an authentication error when read
|
||||||
device.add_service(
|
if mode == 'le':
|
||||||
Service(
|
device.add_service(
|
||||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
Service(
|
||||||
[
|
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
||||||
Characteristic(
|
[
|
||||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
Characteristic(
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READ | Characteristic.WRITE,
|
||||||
CharacteristicValue(read=read_with_error, write=write_with_error)
|
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
|
# Get things going
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
@@ -267,7 +291,7 @@ async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, d
|
|||||||
sc,
|
sc,
|
||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
Delegate(connection, io, prompt)
|
Delegate(mode, connection, io, prompt)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Connect to a peer or wait for a connection
|
# 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:
|
if not request:
|
||||||
try:
|
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
|
return
|
||||||
except ProtocolError:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
# Advertise so that peers can find us and connect
|
# Advertise so that peers can find us and connect
|
||||||
await device.start_advertising(auto_restart=True)
|
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.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('--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('--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)
|
@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('--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.option('--keystore-file', help='File in which to store the pairing keys')
|
||||||
@click.argument('device-config')
|
@click.argument('device-config')
|
||||||
@click.argument('transport')
|
@click.argument('hci_transport')
|
||||||
@click.argument('address-or-name', required=False)
|
@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())
|
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))
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
206
bumble/device.py
206
bumble/device.py
@@ -137,6 +137,10 @@ class Peer:
|
|||||||
def get_characteristics_by_uuid(self, uuid, service = None):
|
def get_characteristics_by_uuid(self, uuid, service = None):
|
||||||
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
async def request_name(self):
|
||||||
|
return await self.connection.request_remote_name()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.connection.peer_address} as {self.connection.role_name}'
|
return f'{self.connection.peer_address} as {self.connection.role_name}'
|
||||||
|
|
||||||
@@ -176,6 +180,7 @@ class Connection(CompositeEventEmitter):
|
|||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.peer_address = peer_address
|
self.peer_address = peer_address
|
||||||
self.peer_resolvable_address = peer_resolvable_address
|
self.peer_resolvable_address = peer_resolvable_address
|
||||||
|
self.peer_name = None # Classic only
|
||||||
self.role = role
|
self.role = role
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
self.encryption = 0
|
self.encryption = 0
|
||||||
@@ -231,6 +236,10 @@ class Connection(CompositeEventEmitter):
|
|||||||
supervision_timeout
|
supervision_timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
async def request_remote_name(self):
|
||||||
|
return await self.device.request_remote_name(self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})'
|
||||||
|
|
||||||
@@ -290,9 +299,19 @@ def with_connection_from_handle(function):
|
|||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
def wrapper(self, connection_handle, *args, **kwargs):
|
def wrapper(self, connection_handle, *args, **kwargs):
|
||||||
if (connection := self.lookup_connection(connection_handle)) is None:
|
if (connection := self.lookup_connection(connection_handle)) is None:
|
||||||
logger.warn(f'no connection found for handle 0x{connection_handle:04X}')
|
raise ValueError('no connection for handle')
|
||||||
return
|
return function(self, connection, *args, **kwargs)
|
||||||
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)
|
||||||
|
raise ValueError('no connection for address')
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@@ -453,6 +472,11 @@ class Device(CompositeEventEmitter):
|
|||||||
if connection := self.connections.get(connection_handle):
|
if connection := self.connections.get(connection_handle):
|
||||||
return connection
|
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):
|
def register_l2cap_server(self, psm, server):
|
||||||
self.l2cap_channel_manager.register_server(psm, server)
|
self.l2cap_channel_manager.register_server(psm, server)
|
||||||
|
|
||||||
@@ -936,13 +960,13 @@ class Device(CompositeEventEmitter):
|
|||||||
# Set up event handlers
|
# Set up event handlers
|
||||||
pending_authentication = asyncio.get_running_loop().create_future()
|
pending_authentication = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
def on_authentication_complete():
|
def on_authentication():
|
||||||
pending_authentication.set_result(None)
|
pending_authentication.set_result(None)
|
||||||
|
|
||||||
def on_authentication_failure(error):
|
def on_authentication_failure(error_code):
|
||||||
pending_authentication.set_exception(error)
|
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)
|
connection.on('connection_authentication_failure', on_authentication_failure)
|
||||||
|
|
||||||
# Request the authentication
|
# Request the authentication
|
||||||
@@ -957,7 +981,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# Wait for the authentication to complete
|
# Wait for the authentication to complete
|
||||||
await pending_authentication
|
await pending_authentication
|
||||||
finally:
|
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)
|
connection.remove_listener('connection_authentication_failure', on_authentication_failure)
|
||||||
|
|
||||||
async def encrypt(self, connection):
|
async def encrypt(self, connection):
|
||||||
@@ -1028,6 +1052,40 @@ class Device(CompositeEventEmitter):
|
|||||||
connection.remove_listener('connection_encryption_change', on_encryption_change)
|
connection.remove_listener('connection_encryption_change', on_encryption_change)
|
||||||
connection.remove_listener('connection_encryption_failure', on_encryption_failure)
|
connection.remove_listener('connection_encryption_failure', on_encryption_failure)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
async def request_remote_name(self, connection):
|
||||||
|
# Set up event handlers
|
||||||
|
pending_name = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
|
def on_remote_name():
|
||||||
|
pending_name.set_result(connection.peer_name)
|
||||||
|
|
||||||
|
def on_remote_name_failure(error_code):
|
||||||
|
pending_name.set_exception(HCI_Error(error_code))
|
||||||
|
|
||||||
|
connection.on('remote_name', on_remote_name)
|
||||||
|
connection.on('remote_name_failure', on_remote_name_failure)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.send_command(
|
||||||
|
HCI_Remote_Name_Request_Command(
|
||||||
|
bd_addr = connection.peer_address,
|
||||||
|
page_scan_repetition_mode = HCI_Remote_Name_Request_Command.R0, # TODO investigate other options
|
||||||
|
reserved = 0,
|
||||||
|
clock_offset = 0 # TODO investigate non-0 values
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
|
logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}')
|
||||||
|
raise HCI_Error(result.status)
|
||||||
|
|
||||||
|
# Wait for the result
|
||||||
|
return await pending_name
|
||||||
|
finally:
|
||||||
|
connection.remove_listener('remote_name', on_remote_name)
|
||||||
|
connection.remove_listener('remote_name_failure', on_remote_name_failure)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
def on_link_key(self, bd_addr, link_key, key_type):
|
def on_link_key(self, bd_addr, link_key, key_type):
|
||||||
@@ -1141,10 +1199,10 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_authentication_complete(self, connection):
|
def on_connection_authentication(self, connection):
|
||||||
logger.debug(f'*** Connection Authentication Complete: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}')
|
logger.debug(f'*** Connection Authentication: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}')
|
||||||
connection.authenticated = True
|
connection.authenticated = True
|
||||||
connection.emit('connection_authentication_complete')
|
connection.emit('connection_authentication')
|
||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
@@ -1152,6 +1210,132 @@ class Device(CompositeEventEmitter):
|
|||||||
logger.debug(f'*** Connection Authentication Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}')
|
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)
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
if 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 = (
|
||||||
|
# No Bonding
|
||||||
|
(
|
||||||
|
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
|
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS
|
||||||
|
),
|
||||||
|
# General Bonding
|
||||||
|
(
|
||||||
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
|
HCI_MITM_REQUIRED_GENERAL_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, # Not present
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_remote_name(self, connection, remote_name):
|
||||||
|
# Try to decode the name
|
||||||
|
try:
|
||||||
|
connection.peer_name = remote_name.decode('utf-8')
|
||||||
|
connection.emit('remote_name')
|
||||||
|
except UnicodeDecodeError as error:
|
||||||
|
logger.warning('peer name is not valid UTF-8')
|
||||||
|
connection.emit('remote_name_failure', error)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_remote_name_failure(self, connection, error):
|
||||||
|
connection.emit('remote_name_failure', error)
|
||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_connection_encryption_change(self, connection, encryption):
|
def on_connection_encryption_change(self, connection, encryption):
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ HCI_VERSION_BLUETOOTH_CORE_4_2 = 8
|
|||||||
HCI_VERSION_BLUETOOTH_CORE_5_0 = 9
|
HCI_VERSION_BLUETOOTH_CORE_5_0 = 9
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_1 = 10
|
HCI_VERSION_BLUETOOTH_CORE_5_1 = 10
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_2 = 11
|
HCI_VERSION_BLUETOOTH_CORE_5_2 = 11
|
||||||
|
HCI_VERSION_BLUETOOTH_CORE_5_3 = 12
|
||||||
|
|
||||||
# HCI Packet types
|
# HCI Packet types
|
||||||
HCI_COMMAND_PACKET = 0x01
|
HCI_COMMAND_PACKET = 0x01
|
||||||
@@ -258,6 +259,9 @@ HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND = hci_command_
|
|||||||
HCI_READ_CLOCK_OFFSET_COMMAND = hci_command_op_code(0x01, 0x001F)
|
HCI_READ_CLOCK_OFFSET_COMMAND = hci_command_op_code(0x01, 0x001F)
|
||||||
HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002B)
|
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_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_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND = hci_command_op_code(0x01, 0x003D)
|
||||||
HCI_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0003)
|
HCI_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0003)
|
||||||
HCI_EXIT_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0004)
|
HCI_EXIT_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0004)
|
||||||
@@ -407,6 +411,9 @@ HCI_COMMAND_NAMES = {
|
|||||||
HCI_READ_CLOCK_OFFSET_COMMAND: 'HCI_READ_CLOCK_OFFSET_COMMAND',
|
HCI_READ_CLOCK_OFFSET_COMMAND: 'HCI_READ_CLOCK_OFFSET_COMMAND',
|
||||||
HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND: 'HCI_IO_CAPABILITY_REQUEST_REPLY_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_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_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND: 'HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND',
|
||||||
HCI_SNIFF_MODE_COMMAND: 'HCI_SNIFF_MODE_COMMAND',
|
HCI_SNIFF_MODE_COMMAND: 'HCI_SNIFF_MODE_COMMAND',
|
||||||
HCI_EXIT_SNIFF_MODE_COMMAND: 'HCI_EXIT_SNIFF_MODE_COMMAND',
|
HCI_EXIT_SNIFF_MODE_COMMAND: 'HCI_EXIT_SNIFF_MODE_COMMAND',
|
||||||
@@ -683,20 +690,20 @@ HCI_IO_CAPABILITY_NAMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Authentication Requirements
|
# Authentication Requirements
|
||||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x00
|
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS = 0x00
|
||||||
HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x01
|
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS = 0x01
|
||||||
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x02
|
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS = 0x02
|
||||||
HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x03
|
HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS = 0x03
|
||||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x04
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS = 0x04
|
||||||
HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x05
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS = 0x05
|
||||||
|
|
||||||
HCI_AUTHENTICATION_REQUIREMENTS_NAMES = {
|
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_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||||
HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS',
|
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||||
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS',
|
HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||||
HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS',
|
HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS',
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS',
|
||||||
HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS'
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Link Key Types
|
# Link Key Types
|
||||||
@@ -723,7 +730,7 @@ HCI_LINK_TYPE_NAMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Address types
|
# Address types
|
||||||
HCI_PUBLIC_DEVICE_ADDRESS_TYPE = 0x0
|
HCI_PUBLIC_DEVICE_ADDRESS_TYPE = 0x00
|
||||||
HCI_RANDOM_DEVICE_ADDRESS_TYPE = 0x01
|
HCI_RANDOM_DEVICE_ADDRESS_TYPE = 0x01
|
||||||
HCI_PUBLIC_IDENTITY_ADDRESS_TYPE = 0x02
|
HCI_PUBLIC_IDENTITY_ADDRESS_TYPE = 0x02
|
||||||
HCI_RANDOM_IDENTITY_ADDRESS_TYPE = 0x03
|
HCI_RANDOM_IDENTITY_ADDRESS_TYPE = 0x03
|
||||||
@@ -1371,6 +1378,9 @@ class HCI_Remote_Name_Request_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
See Bluetooth spec @ 7.1.19 Remote Name Request Command
|
See Bluetooth spec @ 7.1.19 Remote Name Request Command
|
||||||
'''
|
'''
|
||||||
|
R0 = 0x00
|
||||||
|
R1 = 0x01
|
||||||
|
R2 = 0x02
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1449,6 +1459,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([
|
@HCI_Command.command([
|
||||||
('connection_handle', 2),
|
('connection_handle', 2),
|
||||||
@@ -3367,6 +3426,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([
|
@HCI_Event.event([
|
||||||
('status', STATUS_SPEC),
|
('status', STATUS_SPEC),
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class Host(EventEmitter):
|
|||||||
self.command_semaphore = asyncio.Semaphore(1)
|
self.command_semaphore = asyncio.Semaphore(1)
|
||||||
self.long_term_key_provider = None
|
self.long_term_key_provider = None
|
||||||
self.link_key_provider = None
|
self.link_key_provider = None
|
||||||
|
self.pairing_io_capability_provider = None # Classic only
|
||||||
|
|
||||||
# Connect to the source and sink if specified
|
# Connect to the source and sink if specified
|
||||||
if controller_source:
|
if controller_source:
|
||||||
@@ -495,7 +496,7 @@ class Host(EventEmitter):
|
|||||||
def on_hci_authentication_complete_event(self, event):
|
def on_hci_authentication_complete_event(self, event):
|
||||||
# Notify the client
|
# Notify the client
|
||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
self.emit('connection_authentication_complete', event.connection_handle)
|
self.emit('connection_authentication', event.connection_handle)
|
||||||
else:
|
else:
|
||||||
self.emit('connection_authentication_failure', event.connection_handle, event.status)
|
self.emit('connection_authentication_failure', event.connection_handle, event.status)
|
||||||
|
|
||||||
@@ -560,26 +561,16 @@ class Host(EventEmitter):
|
|||||||
asyncio.create_task(send_link_key())
|
asyncio.create_task(send_link_key())
|
||||||
|
|
||||||
def on_hci_io_capability_request_event(self, event):
|
def on_hci_io_capability_request_event(self, event):
|
||||||
# For now, just return NoInputNoOutput and no MITM
|
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||||
# 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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_hci_io_capability_response_event(self, event):
|
def on_hci_io_capability_response_event(self, event):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_hci_user_confirmation_request_event(self, event):
|
def on_hci_user_confirmation_request_event(self, event):
|
||||||
# For now, just confirm everything
|
self.emit('authentication_user_confirmation_request', event.bd_addr, event.numeric_value)
|
||||||
# TODO: delegate the decision
|
|
||||||
self.send_command_sync(
|
def on_hci_user_passkey_request_event(self, event):
|
||||||
HCI_User_Confirmation_Request_Reply_Command(bd_addr = event.bd_addr)
|
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||||
)
|
|
||||||
|
|
||||||
def on_hci_inquiry_complete_event(self, event):
|
def on_hci_inquiry_complete_event(self, event):
|
||||||
self.emit('inquiry_complete')
|
self.emit('inquiry_complete')
|
||||||
@@ -602,3 +593,9 @@ class Host(EventEmitter):
|
|||||||
event.extended_inquiry_response,
|
event.extended_inquiry_response,
|
||||||
event.rssi
|
event.rssi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def on_hci_remote_name_request_complete_event(self, event):
|
||||||
|
if event.status != HCI_SUCCESS:
|
||||||
|
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||||
|
else:
|
||||||
|
self.emit('remote_name', event.bd_addr, event.remote_name)
|
||||||
|
|||||||
@@ -464,13 +464,13 @@ class PairingDelegate:
|
|||||||
async def accept(self):
|
async def accept(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def compare_numbers(self, number):
|
async def compare_numbers(self, number, digits=6):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_number(self):
|
async def get_number(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def display_number(self, number):
|
async def display_number(self, number, digits=6):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ class Session:
|
|||||||
async def prompt():
|
async def prompt():
|
||||||
logger.debug(f'verification code: {code}')
|
logger.debug(f'verification code: {code}')
|
||||||
try:
|
try:
|
||||||
response = await self.pairing_config.delegate.compare_numbers(code)
|
response = await self.pairing_config.delegate.compare_numbers(code, digits=6)
|
||||||
if response:
|
if response:
|
||||||
next_steps()
|
next_steps()
|
||||||
return
|
return
|
||||||
@@ -733,7 +733,7 @@ class Session:
|
|||||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
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):
|
def input_passkey(self, next_steps=None):
|
||||||
# Prompt the user for the passkey displayed on the peer
|
# Prompt the user for the passkey displayed on the peer
|
||||||
|
|||||||
2
docs/mkdocs/src/apps_and_tools/gatt_dump.md
Normal file
2
docs/mkdocs/src/apps_and_tools/gatt_dump.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
GATT DUMP TOOL
|
||||||
|
==============
|
||||||
@@ -5,6 +5,8 @@ Included in the project are a few apps and tools, built on top of the core libra
|
|||||||
These include:
|
These include:
|
||||||
|
|
||||||
* [Console](console.md) - an interactive text-based console
|
* [Console](console.md) - an interactive text-based console
|
||||||
|
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
||||||
|
* [Unbond](unbond.md) - Remove a previously established bond
|
||||||
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
||||||
* [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool"
|
* [Golden Gate Bridge](gg_bridge.md) - a bridge between GATT and UDP to use with the Golden Gate "stack tool"
|
||||||
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
|
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form
|
||||||
|
|||||||
2
docs/mkdocs/src/apps_and_tools/pair.md
Normal file
2
docs/mkdocs/src/apps_and_tools/pair.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
PAIR TOOL
|
||||||
|
=========
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
SHOW TOOL
|
||||||
|
=========
|
||||||
|
|||||||
2
docs/mkdocs/src/apps_and_tools/unbond.md
Normal file
2
docs/mkdocs/src/apps_and_tools/unbond.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
UNBOND TOOL
|
||||||
|
===========
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
junit_logging = all
|
junit_logging = all
|
||||||
|
asyncio_mode = auto
|
||||||
@@ -208,11 +208,11 @@ async def test_self_smp():
|
|||||||
self.peer_delegate = None
|
self.peer_delegate = None
|
||||||
self.number = asyncio.get_running_loop().create_future()
|
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:
|
if self.peer_delegate is None:
|
||||||
logger.warn(f'[{self.name}] no peer delegate')
|
logger.warn(f'[{self.name}] no peer delegate')
|
||||||
return False
|
return False
|
||||||
await self.display_number(number)
|
await self.display_number(number, digits=6)
|
||||||
logger.debug(f'[{self.name}] waiting for peer number')
|
logger.debug(f'[{self.name}] waiting for peer number')
|
||||||
peer_number = await self.peer_delegate.number
|
peer_number = await self.peer_delegate.number
|
||||||
logger.debug(f'[{self.name}] comparing numbers: {number} and {peer_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}')
|
logger.debug(f'[{self.name}] returning number: {peer_number}')
|
||||||
return 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}')
|
logger.debug(f'[{self.name}] displaying number: {number}')
|
||||||
self.number.set_result(number)
|
self.number.set_result(number)
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ async def test_self_smp_wrong_pin():
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT)
|
super().__init__(PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT)
|
||||||
|
|
||||||
async def compare_numbers(self, number):
|
async def compare_numbers(self, number, digits):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
wrong_pin_pairing_config = PairingConfig(delegate = WrongPinDelegate())
|
wrong_pin_pairing_config = PairingConfig(delegate = WrongPinDelegate())
|
||||||
|
|||||||
Reference in New Issue
Block a user