Skip to content
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

No Pairing Confirmation When Peripheral Does Not Use Secure Connections #592

Open
hkpeprah opened this issue Nov 15, 2024 · 9 comments
Open

Comments

@hkpeprah
Copy link
Contributor

Summary

I'm working with two peripherals; one that is running on BLE 5, and one that uses BLE 4. I have bumble set up as the Central. On the BLE 5 peripheral, Secure Connections are enabled. On the BLE 4 peripheral they are not. When running a pairing test, the PairingDelegate gets prompted for user confirmation in on_smp_pairing_random_command_secure_connections(): https://github.com/google/bumble/blob/main/bumble/smp.py#L1794. This does not happen in on_smp_pairing_random_command_legacy(): https://github.com/google/bumble/blob/main/bumble/smp.py#L1653.

When either peripheral is used with a mobile device (iOS or Android), the mobile device will always prompt the user for confirmation, regardless of if the software initiated pairing (Android).

Request

Would it be possible to have on_smp_pairing_random_command_legacy() also prompt for confirmation of pairing when the pairing method is JUST_WORKS? This would be closer to how pairing is handled on mobile devices where user confirmation is always requested.

@barbibulle
Copy link
Collaborator

There are two levels of prompting that a pairing delegate can participate in:

  1. A catch-all acceptance for pairing requests, which will be checked for any pairing request, regardless of the eventual pairing method (which is decided later on, based on the I/O capabilities of both participants). This is the delegate's accept method that a delegate can implement.
  2. An optional pairing-method-specific confirmation, which depends on the pairing method (the decision matrix for which confirmation type is applicable is defined in the BT specs):
    * confirm is for yes/no based pairing confirmations
    * compare_numbers is for pairing confirmations where a user can read a PIN displayed on the peer device and on the local device and compare them
    * get_number and get_string are for cases where there's no display but the local user can still type something
    * display_number is for when the local device can display something but has neither a yes/no button nor a keyboard.

So it seems that just implementing accept in your delegate should do exactly what you're looking for.

@hkpeprah
Copy link
Contributor Author

hkpeprah commented Nov 15, 2024

In the case that the I call .pair(), I do not get a SMP_PAIRING_REQUEST_COMMAND from the peripheral, and so .accept() is not called. Instead I get:

SMP_PAIRING_RESPONSE_COMMAND
SMP_PAIRING_CONFIRM_COMMAND
SMP_PAIRING_RANDOM_COMMAND
SMP_ENCRYPTION_INFORMATION_COMMAND
SMP_MASTER_IDENTIFICATION_COMMAND
SMP_IDENTITY_INFORMATION_COMMAND
SMP_IDENTITY_ADDRESS_INFORMATION_COMMAND

For SMP_PAIRING_RANDOM_COMMAND, that checks if secure connection is enabled, and calls on_smp_pairing_random_command_secure_connections if so, otherwise calls on_smp_pairing_random_command_legacy:

    def on_smp_pairing_random_command(
        self, command: SMP_Pairing_Random_Command
    ) -> None:
        self.peer_random_value = command.random_value
        if self.sc:
            self.on_smp_pairing_random_command_secure_connections(command)
        else:
            self.on_smp_pairing_random_command_legacy(command)

https://github.com/google/bumble/blob/main/bumble/smp.py#L1800-L1807

on_smp_pairing_random_command_secure_connections will invoke the delegate's .confirm() by calling prompt_user_for_confirmation(): https://github.com/google/bumble/blob/main/bumble/smp.py#L1794; whereas on_smp_pairing_random_command_legacy() does not: https://github.com/google/bumble/blob/main/bumble/smp.py#L1653. The .accept() method is only called if the other device sends the SMP_PAIRING_REQUEST_COMMAND: https://github.com/google/bumble/blob/main/bumble/smp.py#L1442. Since I'm sending the request, I only get the response, but the behaviour differs depending on whether the peripheral supports secure connections or not.

@hkpeprah
Copy link
Contributor Author

This is the SMP_PAIRING_RESPONSE which results in no confirmation:

  io_capability:               SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
  oob_data_flag:               0
  auth_req:                    bonding_flags=1, MITM=0, sc=0, keypress=0, ct2=0
  maximum_encryption_key_size: 16
  initiator_key_distribution:  ENC,ID
  responder_key_distribution:  ENC,ID

This is the one that results in confirmation:

  io_capability:               SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
  oob_data_flag:               0
  auth_req:                    bonding_flags=1, MITM=0, sc=1, keypress=0, ct2=0
  maximum_encryption_key_size: 16
  initiator_key_distribution:  ENC,ID
  responder_key_distribution:  ENC,ID

@barbibulle
Copy link
Collaborator

It is normal that the .accept method of the delegate is only invoked when there's an incoming pairing request: when you initiate pairing, you are the one triggering the request, so you don't need to confirm it. If you want to reuse the same implementation of some sort of confirmation step for both initiated and received pairing requests, you should call that implementation prior to calling pair() (either directly, or by calling the .accept method of your delegate).

@hkpeprah
Copy link
Contributor Author

hkpeprah commented Nov 19, 2024

I'm currently seeing confirmations for initiated pairings when the peripheral has secure connections set (sc=1); it's only when secure connections is not set (sc=0) that there is no confirmation. I see the following cases:

  1. Peripheral Initiated and Secure Connections is 0 + JUST_WORKS -> .accept() is called.
  2. Peripheral Initiated and Secure Connection is 1 + JUST_WORKS -> .accept() is called.
  3. Central Initiated and Secure Connections is 0 + JUST_WORKS -> .confirm() is not called.
  4. Central Initiated and Secure Connections is 1 + JUST_WORKS-> .confirm() is called.

Shouldn't 3 and 4 behave the same regardless of if sc=1 or sc=0? On an Android mobile device, Central initiated pairing requires user confirmation independent of legacy pairing.

@barbibulle
Copy link
Collaborator

3 and 4 should behave the same, both without confirmation (that's what JUST_WORKS does). Are you sure the pairing method inferred from the two peer's IO capabilities ended up in JUST_WORKS in case 4? I've just run a quick test on my system, pairing with secure connections and empty IO capabilities, and I don't see a confirmation request, as expected.
If you can run your use case with BUMBLE_LOGCONFIG=debug, we can double check what pairing method ended up being selected.

NOTE: on an Android device, you'll see a pairing acceptance dialog in all cases (whether initiated by the phone or not), because that's a security measure to ensure that you don't have an app-triggered pairing without the user knowing it. That initial confirmation happens before the pairing protocol runs, regardless of the details of the pairing process that happens after (assuming the user accepts). That's the equivalent of the .accept method of the delegate with Bumble.
For pairing methods that require a confirmation step (PIN code comparison, or PIN entry, or other), you will get a second step. For JUST_WORKS, there's no second step. With Bumble, that method-specific second step is one of the delegate methods (.compare_numbers, .confirm, .display_number, ...)

@hkpeprah
Copy link
Contributor Author

hkpeprah commented Nov 21, 2024

Thanks for digging into this @barbibulle . I was able to root cause the issue. The peripheral with Secure Connections set is sending a SMP_SECURITY_REQUEST_COMMAND, whereas the one without is not. Since I'm trying to replicate the mobile device behaviour, it seems like I'll have to figure out a workaround to get the peripheral to get a rejection during the pairing process. Is there an API I can call to force a pairing rejection? I'm essentially trying to test the cases where pairing is rejected by the end user, and where pairing times out (SMP timeout is reached). With the .confirm() call in the first case, I'm able to simulate both, but without it, I can't.

@barbibulle
Copy link
Collaborator

Ok, this makes sense. I can see now that the "security request" support isn't quite complete enough and needs improvement.
In the current implementation, when a security request comes in, an event is generated, but there's no default handler for that event. This should be improved. I need to think of a good way to handle this without burdening the API users too much. I'll submit a PR for that shortly.

@barbibulle
Copy link
Collaborator

One more quick note: for your specific use case, you probably don't need to wait for this enhancement. You can simply handle the security_request event (emitted by the connection object, so you'd call something like: connection.on("security_request", ...)). If you don't listen for the event, or listen for it but don't trigger pairing in the listener, you'll be able to observe what happens when there's a pairing timeout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants