IanHarvey / bluepy

Python interface to Bluetooth LE on Linux
Other
1.6k stars 491 forks source link

Pairing always require user input #450

Open robino16 opened 3 years ago

robino16 commented 3 years ago

When pairing, a dialogue box always appears, asking the end user to accept. If using bluetoothctl from a terminal with a NoInputNoOutput agent, the prompt appear in the command line instead, where the user have to write "yes" in order to accept the pairing.

I am using a custom made BLE device which uses Just Works pairing.

Python code:

from bluepy.btle import Peripheral
peripheral = Peripheral('aa:bb:cc:dd:ee:ff')
peripheral.connect()
# Bonding is manually enabled on the device.
peripheral.pair()
# Dialogue box or command line prompt appears...

If I enable Simple Secure Pairing on the Raspberry Pi using:

sudo hciconfig hci0 piscan
sudo hciconfig hci0 sspmode 1

the peripheral.pair() method raises an exception with "Authentication Failed" (error code5). If using bluetoothctl in command line (or using subprocess in Python) like:

sudo bluetoothctl
agent off
agent NoInputNoOutput
default-agent

the same prompt instead occur inside the terminal, once the code is executed. The user has to type "yes" in the terminal in order to approve it.

robino16 commented 3 years ago

So I've found a solution. And it may not be an issue with bluepy. I had to run a custom bluez Python agent in a separate thread. Once my custom agent runs, I can issue pair() without any prompt showing up.

abhishek-v-pandey commented 3 years ago

Hello @robino16 Could you explain how did you use the custom bluez Python agent. I am also facing the same issue. I need to connect to a sensor that requires a passkey to see its services. I am using bluepy

robino16 commented 3 years ago

I'll try my best @abhishek-v-pandey .

I basically run my bluepy Peripheral in one thread and my custom agent in another thread. I based my agent on ukBaz's example agent as shown in this thread. Then I modified it to not request user authorization, and simply run it in it's own thread once my application start, like this:

from threading import Thread
# ...
agent = Agent(bus, AGENT_PATH)
manager = dbus.Interface(bus.get_object(BUS_NAME, AGNT_MNGR_PATH), ANGT_MNGR_IFACE)
manager.RegisterAgent(AGENT_PATH, CAPABILITY)
manager.RequestDefaultAgent(AGENT_PATH)
adapter = Adapter()
main_loop = GLib.MainLoop()

# start the thread
def run_gbus():
    main_loop.run()
t1 = Thread(run_gbus)
t1.start()

# after this, I connect and pair with my Peripheral
peripheral = Peripheral('aa:bb:cc:dd:ee:ff', 'random')
peripheral.connect()
# ...
peripheral.pair()

I hope this answers your question. I'm not sure how this works with Passkey authentication.

Edit: The run_gbus() function should not catch the KeyboardInterrupt event. It's better to stop the main_loop using main_loop.stop() before exiting the program.

abhishek-v-pandey commented 3 years ago

Thanks @robino16 for the code. I tried it . But no progess, I still could not connect to my device. It is a BLE sensor which requires a passkey to autenticate.

Below is my code.

from threading import Thread
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib
import sys
import time
from bluepy import btle

BUS_NAME = 'org.bluez'
ADAPTER_IFACE = 'org.bluez.Adapter1'
ADAPTER_ROOT = '/org/bluez/hci'
AGENT_IFACE = 'org.bluez.Agent1'
AGNT_MNGR_IFACE = 'org.bluez.AgentManager1'
AGENT_PATH = '/my/app/agent'
AGNT_MNGR_PATH = '/org/bluez'
CAPABILITY = 'KeyboardDisplay'
DEVICE_IFACE = 'org.bluez.Device1'
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()

def set_trusted(path):
    props = dbus.Interface(bus.get_object(BUS_NAME, path), dbus.PROPERTIES_IFACE)
    props.Set(DEVICE_IFACE, "Trusted", True)

class Agent(dbus.service.Object):

    @dbus.service.method(AGENT_IFACE,
                         in_signature="", out_signature="")
    def Release(self):
        print("Release")

    @dbus.service.method(AGENT_IFACE,
                         in_signature='o', out_signature='s')
    def RequestPinCode(self, device):
        print(f'RequestPinCode {device}')
        return '0000'

    @dbus.service.method(AGENT_IFACE,
                         in_signature="ou", out_signature="")
    def RequestConfirmation(self, device, passkey):
        print("RequestConfirmation (%s, %06d)" % (device, passkey))
        set_trusted(device)
        return

    @dbus.service.method(AGENT_IFACE,
                         in_signature="o", out_signature="")
    def RequestAuthorization(self, device):
        print("RequestAuthorization (%s)" % (device))
        auth = input("Authorize? (yes/no): ")
        if (auth == "yes"):
            return
        raise Rejected("Pairing rejected")

    @dbus.service.method(AGENT_IFACE,
                         in_signature="o", out_signature="u")
    def RequestPasskey(self, device):
        print("RequestPasskey (%s)" % (device))
        set_trusted(device)
        passkey = input("Enter passkey: ")
        return dbus.UInt32(passkey)

    @dbus.service.method(AGENT_IFACE,
                         in_signature="ouq", out_signature="")
    def DisplayPasskey(self, device, passkey, entered):
        print("DisplayPasskey (%s, %06u entered %u)" %
              (device, passkey, entered))

    @dbus.service.method(AGENT_IFACE,
                         in_signature="os", out_signature="")
    def DisplayPinCode(self, device, pincode):
        print("DisplayPinCode (%s, %s)" % (device, pincode))

class Adapter:
    def __init__(self, idx=0):
        bus = dbus.SystemBus()
        self.path = f'{ADAPTER_ROOT}{idx}'
        self.adapter_object = bus.get_object(BUS_NAME, self.path)
        self.adapter_props = dbus.Interface(self.adapter_object,
                                            dbus.PROPERTIES_IFACE)
        self.adapter_props.Set(ADAPTER_IFACE,
                               'DiscoverableTimeout', dbus.UInt32(0))
        self.adapter_props.Set(ADAPTER_IFACE,
                               'Discoverable', True)
        self.adapter_props.Set(ADAPTER_IFACE,
                               'PairableTimeout', dbus.UInt32(0))
        self.adapter_props.Set(ADAPTER_IFACE,
                               'Pairable', True)

agent = Agent(bus, AGENT_PATH)
manager = dbus.Interface(bus.get_object(BUS_NAME, AGNT_MNGR_PATH), AGNT_MNGR_IFACE)
manager.RegisterAgent(AGENT_PATH, CAPABILITY)
manager.RequestDefaultAgent(AGENT_PATH)
adapter = Adapter()

def run_gbus():
    main_loop = GLib.MainLoop()
    try:
        main_loop.run()
    except KeyboardInterrupt:
        manager.UnregisterAgent(AGENT_PATH)
        main_loop.quit()

t1 = Thread(target = run_gbus)
t1.start()

# after this, I connect and pair with my Peripheral
peripheral = btle.Peripheral('00:1e:ae:3d:d8:1c')
peripheral.connect()
peripheral_A = peripheral.getServiceByUUID("65c4f2e0-b19e-11e2-9e96-0800200c9a66")
print(peripheral_A)

Below is the error: bluepy.btle.BTLEDisconnectError: Failed to connect to peripheral 00:1e:ae:3d:d8:1c, addr type: public

Please help and tell where I am wrong and what changes should i make.

Thanks

robino16 commented 3 years ago

Hi, @abhishek-v-pandey.

I'm sorry, but I have no idea what the problem is. I don't think it's the agents fault. It seems you fail to establish the connection with the device. In my app i use addr type random. I have read other places that people first use pair(), and then connect(). But I have far too little knowledge about bluepy to guess why you get the BTLEDisconnectError. I usually get this error when my peripheral device is not advertising/switched off.

My device does not require a PassKey to pair. I pair using JustWorks with no security. After this, the peripheral whitelists the central device, so that no other device may connect. I have not had any issues with connecting to my device.

Edit: JustWorks is a pairing mechanism.

abhishek-v-pandey commented 3 years ago

Hi @robino16 , I am using Raspberry Pi 4 to connect with the sensor. With nRF connect app I am able to connect to the sensor, it ask for the passkey but with the Pi its shows the above error.

Do you think this makes a difference by the way it connects.? How it connection works with you using bluepy.? Any special code we should write for working with security like JustWorks?

Thanks

robino16 commented 3 years ago

Hi, @abhishek-v-pandey.

I wrote a bit wrong. JustWorks is a pairing mechanism. It simply means that no PassKey or user input should be required during the pairing process. My Issue was that I always was asked for user authorization when I tried to pair. I believe the peripheral.connect() does not relate to JustWorks. But it still seems like your peripheral refuses to connect. Perhaps you need to make modifications to the firmware.

When I use peripheral.connect(), I do not get a BTLEDisconnectError. My only annoyance was that a prompt showed up every time I called peripheral.pair().

abhishek-v-pandey commented 3 years ago

Hi @robino16 ,

I studied about pairing mechanism. The sensor i have is not designed by me. It uses passkey to authorize it. Firmware change is not possible. I need to make the R.Pi to connect with it.

Today I tried to pair it with using blueman and hcitool command. I can trust my device using blueman. When I enter the command sudo hcitool lecc "mac_address" . i get a prompt to enter the passkey and then afterwards its get bonded. I can then see it trusted and bonded in the blueman but when i run the python scripts it throws an error. I have tried to disable blueman and try as well.

I am using Bluez v5.50. do you think this is a version problem or some issues related to the bluez stack that the sensor is refusing the connection. With android device , it connects and android does not use the bluez.

Any other method you could advise that I can try to connect to my sensor. I can see the sensor advertisement packet while scanning using bluepy?

Thanks for the help !