-
Notifications
You must be signed in to change notification settings - Fork 300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pairing agent #1100
base: develop
Are you sure you want to change the base?
Pairing agent #1100
Conversation
Using the # REVISIT: implement passing capability if needed
# "DisplayOnly", "DisplayYesNo", "KeyboardOnly", "NoInputNoOutput", "KeyboardDisplay"
capability = "" print("pairing...")
callbacks = AgentCallbacks()
async with BleakClient(device) as client:
try:
await client.pair(callbacks)
print("success")
except BleakPairingCancelledError:
print("paring was canceled")
except BleakPairingFailedError:
print("pairing failed (bad pin?)") connection to my test device fails with (error from Silabs BLE stack):
When the original code is modified, so that the pairing agent has proper capabilities # REVISIT: implement passing capability if needed
# "DisplayOnly", "DisplayYesNo", "KeyboardOnly", "NoInputNoOutput", "KeyboardDisplay"
capability = "KeyboardDisplay" and the agent is created before connection print("pairing...")
callbacks = AgentCallbacks()
client = BleakClient(device)
# Bus needs to be connected for RegisterAgent in bluez_agent() to succeed
await client._backend._bus.connect()
async with bluez_agent(bus=client._backend._bus, callbacks=callbacks):
async with client:
try:
await client.pair() # Do not pass callbacks to prevent registering another agent
print("success")
except BleakPairingCancelledError:
print("paring was canceled")
except BleakPairingFailedError:
print("pairing failed (bad pin?)") the example works. I took this concept and changed API a bit - my proposed solution is here pairing-agent...bojanpotocnik:bleak:pairing |
Thanks for trying this out.
BlueZ is supposed to use "KeyboardDisplay" as a fallback when given an empty string 1. Which BlueZ version are you using? Can you share Wireshark logs of the different behavior between "" and "KeyboardDisplay"?
Hmm... if this is needed for this device, then the Windows implementation doesn't seem likely to be able to work with this device. Can you Wireshark logs showing the difference between registering the agent before connecting vs. registering the agent after connecting? |
You're correct about IO capabilities - the behaviour in both cases is the same (that is, the pairing works with my fork even with I've attached bleak_pr1100_wireshark_captures.zip containing Interestingly, in both cases Master (my PC) sends
Shortly after, Slave requests pairing with
to which Master responds with
in
in Looks like BlueZ thinks bonding is not supported unless I register my own agent. Note that I'm testing this on Lenovo Thinkpad using Ubuntu 20.04, BlueZ 5.62 with system pairing agent present (I would understand such behaviour on a headless RPi). |
Thanks for the analysis. I had not considered the case where the remote device initiates pairing. Given that Windows only support custom pairing when the central initiates the pairing, it would be nice if we could only support that case to keep things consistent across platforms. Would it work to do something like this? client = BleakClient(device, ...)
await client.pair(...)
try:
...
finally:
await client.disconnect() There are probably some modifications needed to get the services but in BlueZ pairing will result in connecting the device. I'll have to look to see how it would work on Windows. |
Technically, pairing is still initiated by the central device (Pairing Request), the peripheral just requests the higher security level (the thing which triggers pairing in in Core Bluetooth). The only difference here is that usually, peripheral devices allow service discovery without pairing, but send Security Request when central tries to read the value of certain protected characteristic. We usually know that such device will require pairing to read the value, so we initiate pairing before reading the value (after service discovery), so the Security Request is never received. I am not sure if I understand what is different in solution proposed in your last comment. However, I just tested with print("pairing...")
callbacks = AgentCallbacks()
async with BleakClient(device, pairing_callbacks=callbacks) as client:
await asyncio.sleep(10) and class AgentCallbacks(BaseBleakAgentCallbacks):
async def request_pin(self, device: BLEDevice) -> str: was still invoked, so effectively you're correct about "the remote device initiates pairing". I understand your argument about keeping things consistent across platforms, however I am fond of the idea of supporting wider range of devices, if it only cost changing API in a way that pairing callbacks are provided to the |
This seems to go against the requirement of the Bluetooth specification. For example, in the Core spec v5.3, Part G, Section 3.1, it states:
There is similar language for other attributes related to service discovery.
While I generally agree with this sentiment, I'm not fond of trying to support devices that don't follow the specifications.
In the case of BlueZ, it means that the agent is registered as long as the device is connected instead of just during the pairing operation. It also complicates managing the lifetime of the agent, such as ensuring that the agent is unregistered when the device is disconnected. These sorts of issues are a common source of bugs and cost quite a bit of my time, hence my pushing back against the proposed API changes. Since this only works with the BlueZ backend and is only needed for devices like this that don't follow the specifications, I would suggest using your original workaround using the current proposed API: callbacks = AgentCallbacks()
client = BleakClient(device)
await client._backend_bus.connect()
async with bluez_agent(client._backend._bus, callbacks), client:
... |
This adds a close() method to the BleakClient class and backends. This is used to explicitly close an resource, like the D-Bus socket in the BlueZ backend. In the BlueZ backend, the D-Bus socket is no longer closed on disconnect. This is needed to avoid problems with D-Bus methods failing with EOFError() or hanging forever because the socket was disconnect.
This adds a callbacks arg to programmatically responding to pairing requests instead of allowing the OS to prompt the user. This just adds the parameter but does not provide an implementation yet.
I double checked the peripheral device and done a bit more testing. There are 2 separate things this peripheral device is using to ensure sufficient security level, both of them are per BT spec. You are correct that Attribute Permissions do not require authentication or authorization, but reading the value does - this peripheral device has Authenticated + Bonded + Encrypted selected for all enabled entries in table shows under Set up Attribute Permissions in GATT. So although it is indeed impossible to protect reading attribute access properties and permission levels, everything else can be protected (and most central devices try to automatically read GATT characteristic descriptors). Additionally, there is another feature, described in Core Spec 5.2 | Vol 3, Part H, 2.4.6 Slave Security Request:
This device utilizes Increase Security Level feature immediately after connection, to ensure that unauthorized central cannot be connected to this peripheral - otherwise malicious central device could connect without trying to read any GATT value (therefore not initiating pairing) and stay connected, effectively blocking other central devices to connect. I tested with Windows 10 yesterday and I was greeted with Windows notification that the device would like to pair (system pairing agent), while Bleak was waiting for pairing process to be completed. I will continue testing in the evening - I suspect adding |
manager = await get_global_bluez_manager() | ||
props = manager.get_device_props(device_path) | ||
return BLEDevice( | ||
props["Address"], props["Alias"], {"path": device_path, "props": props} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
props["Address"], props["Alias"], {"path": device_path, "props": props} | |
props["Address"], props["Alias"], {"path": device_path, "props": props}, props.get("RSSI", -127) |
because of bcae937#diff-d647d8e3f316fd939334bd8ed60d248095c8a6f69bc73dd0b369867609dc00a6R21
During testing on Windows, I discovered that my problem can be solved with completely different and much simpler approach, as described in #1133 |
I haven't had time to get back to any of this yet. I will have to take another good look at everything again to decide the best way to move forward. But that commit does seem useful on it's own. |
Though I've not been able to closely follow all the updates and improvements made to |
Could it be an option to try to move this forward in smaller increments? For example, if Windows is still problematic, an initial version could support only bluez. Also, additional pairing methods could be added later. |
this seems to work for me on linux, mostly. but on ubuntu 22 (bluez 5.64), pairing of a device im working with results in immediate disconnect before pin code handler is able to run. log:
removing As an aside- I think it would be great to be able to provide a pin in Also: If pairing fails, maybe unset trusted, assuming it was not already trusted prior to bleak attempt. |
This is a work-in-progress attempt at implementing to implement programmatic pairing on Linux and Windows (alternative to the stalled #640).
I'm fairly happy with the Linux/BlueZ implementation at this point. But the Windows implementation is quite flaky need work.
Fixes #827
Issues to be resolved before merging:
BaseBleakAgentCallbacks.request_pin()
.pair_async()
method to return whenaccept()
is not called. The pair method seems to block forever unless theaccept()
method of the DevicePairingRequestedEventArgs is called. But the user might not want to accept the pairing, so how do we handle this case? Also, if a pin is requested and you reply with a non-numeric string, thepair_async()
method never returns instead of the expected invalid pin error.