40

Does anyone have a complete working example of how to programmatically pair with a BLE (not Bluetooth Classic) device that uses passkey entry (i.e. a 6-digit PIN) or Numeric Comparison on Android 4.4 or later? By 'programmatically' I mean I tell Android the PIN - the user isn't prompted.

There are many similar questions about this on SO but they are either a) about Bluetooth Classic, b) old (before setPin() and createBond() were public), or c) unanswered.

My understanding is as follows.

  1. You connect to the device and discover its services.
  2. You try to read a 'protected' characteristic.
  3. The device returns an authentication error.
  4. Android somehow initiates pairing and you tell it the PIN.
  5. You can now read the characteristic.

I have created a device using mBed running on the nRF51-DK and given it a single characteristic.

I set up the security parameters like so:

ble.securityManager().init(
    true, // Enable bonding (though I don't really need this)
    true, // Require MitM protection. I assume you don't get a PIN prompt without this, though I'm not 100% sure.
    SecurityManager::IO_CAPS_DISPLAY_ONLY, // This makes it us the Passkey Entry (PIN) pairing method.
    "123456"); // Static PIN

And then in the characteristic I used

requireSecurity(SecurityManager::SECURITY_MODE_ENCRYPTION_WITH_MITM);

Now when I try to read it with the Nordic Master Control Panel, I get a pairing request notification like this:

pairing request

passkey entry

And I can put this PIN in, and then MCP says I'm bonded, and can read the characteristic.

However, in my app I would like to avoid having the user enter the PIN, since I know it already. Does anyone have a complete recent example of how to do this?

Edit: By the way this is the most relevant question I found on SO, but the answer there doesn't seem to work.

Community
  • 1
  • 1
Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • Does the Android SDK allow pairing without the user being notified / asked for confirmation? It does sound like you might not be able to... – Florian Castellane Jul 07 '16 at 09:45
  • 1
    Yes it does. It has a `setPin()` method specifically for this, and I've got it to work except the "Pairing request" notification is still shown. – Timmmm Jul 07 '16 at 15:43
  • I am not sure if this gone help, but it is worth reading it http://stackoverflow.com/questions/17971834/android-prevent-bluetooth-pairing-dialog – Maytham Fahmi Jul 11 '16 at 20:32
  • Yeah I've seen that. No answer there either. :-/ – Timmmm Jul 12 '16 at 08:07
  • Can you answer my question ? in https://security.stackexchange.com/questions/179298/how-to-enforce-android-bluetooth-protocol-stack-to-work-only-in-secure-modes – ofskyMohsen Feb 24 '18 at 14:35
  • @Timmmm can you shed some light on [my question](https://stackoverflow.com/q/54529441/6880611) – Tejas Pandya Feb 05 '19 at 07:48

2 Answers2

40

I almost have it working. It pairs programmatically but I can't get rid of the "Pairing request" notification. Some answers to this question claim to be able to hide it just after it is shown using the hidden method cancelPairingUserInput() but that doesn't seem to work for me.

Edit: Success!

I eventually resorted to reading the source code of BluetoothPairingRequest and the code that sends the pairing request broadcast and realised I should be intercepting the ACTION_PAIRING_REQUEST. Fortunately it is an ordered intent broadcast so you can intercept it before the system does.

Here's the procedure.

  1. Register to receive BluetoothDevice.ACTION_PAIRING_REQUEST changed broadcast intents. Use a high priority!
  2. Connect to the device.
  3. Discover services.
  4. If you have disconnected by now, it's probably because the bond information is incorrect (e.g. the peripheral purged it). In that case, delete the bond information using a hidden method (seriously Google), and reconnect.
  5. Try to read a characteristic that requires encryption MitM protection.
  6. In the ACTION_PAIRING_REQUEST broadcast receiver, check that the pairing type is BluetoothDevice.PAIRING_VARIANT_PIN and if so, call setPin() and abortBroadcast(). Otherwise you can just let the system handle it, or show an error or whatever.

Here is the code.

/* This implements the BLE connection logic. Things to watch out for:

1. If the bond information is wrong (e.g. it has been deleted on the peripheral) then
   discoverServices() will cause a disconnect. You need to delete the bonding information and reconnect.

2. If the user ignores the PIN request, you get the undocumented GATT_AUTH_FAILED code.

 */
public class ConnectActivityLogic extends Fragment
{
    // The connection to the device, if we are connected.
    private BluetoothGatt mGatt;

    // This is used to allow GUI fragments to subscribe to state change notifications.
    public static class StateObservable extends Observable
    {
        private void notifyChanged() {
            setChanged();
            notifyObservers();
        }
    };

    // When the logic state changes, State.notifyObservers(this) is called.
    public final StateObservable State = new StateObservable();

    public ConnectActivityLogic()
    {
    }

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // Tell the framework to try to keep this fragment around
        // during a configuration change.
        setRetainInstance(true);

        // Actually set it in response to ACTION_PAIRING_REQUEST.
        final IntentFilter pairingRequestFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
        pairingRequestFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1);
        getActivity().getApplicationContext().registerReceiver(mPairingRequestRecevier, pairingRequestFilter);

        // Update the UI.
        State.notifyChanged();

        // Note that we don't actually need to request permission - all apps get BLUETOOTH and BLUETOOTH_ADMIN permissions.
        // LOCATION_COARSE is only used for scanning which I don't need (MAC is hard-coded).

        // Connect to the device.
        connectGatt();
    }

    @Override
    public void onDestroy()
    {
        super.onDestroy();

        // Disconnect from the device if we're still connected.
        disconnectGatt();

        // Unregister the broadcast receiver.
        getActivity().getApplicationContext().unregisterReceiver(mPairingRequestRecevier);
    }

    // The state used by the UI to show connection progress.
    public ConnectionState getConnectionState()
    {
        return mState;
    }

    // Internal state machine.
    public enum ConnectionState
    {
        IDLE,
        CONNECT_GATT,
        DISCOVER_SERVICES,
        READ_CHARACTERISTIC,
        FAILED,
        SUCCEEDED,
    }
    private ConnectionState mState = ConnectionState.IDLE;

    // When this fragment is created it is given the MAC address and PIN to connect to.
    public byte[] macAddress()
    {
        return getArguments().getByteArray("mac");
    }
    public int pinCode()
    {
        return getArguments().getInt("pin", -1);
    }

    // Start the connection process.
    private void connectGatt()
    {
        // Disconnect if we are already connected.
        disconnectGatt();

        // Update state.
        mState = ConnectionState.CONNECT_GATT;
        State.notifyChanged();

        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress());

        // Connect!
        mGatt = device.connectGatt(getActivity(), false, mBleCallback);
    }

    private void disconnectGatt()
    {
        if (mGatt != null)
        {
            mGatt.disconnect();
            mGatt.close();
            mGatt = null;
        }
    }

    // See https://android.googlesource.com/platform/external/bluetooth/bluedroid/+/master/stack/include/gatt_api.h
    private static final int GATT_ERROR = 0x85;
    private static final int GATT_AUTH_FAIL = 0x89;

    private android.bluetooth.BluetoothGattCallback mBleCallback = new BluetoothGattCallback()
    {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
        {
            super.onConnectionStateChange(gatt, status, newState);
            switch (newState)
            {
            case BluetoothProfile.STATE_CONNECTED:
                // Connected to the device. Try to discover services.
                if (gatt.discoverServices())
                {
                    // Update state.
                    mState = ConnectionState.DISCOVER_SERVICES;
                    State.notifyChanged();
                }
                else
                {
                    // Couldn't discover services for some reason. Fail.
                    disconnectGatt();
                    mState = ConnectionState.FAILED;
                    State.notifyChanged();
                }
                break;
            case BluetoothProfile.STATE_DISCONNECTED:
                // If we try to discover services while bonded it seems to disconnect.
                // We need to debond and rebond...

                switch (mState)
                {
                    case IDLE:
                        // Do nothing in this case.
                        break;
                    case CONNECT_GATT:
                        // This can happen if the bond information is incorrect. Delete it and reconnect.
                        deleteBondInformation(gatt.getDevice());
                        connectGatt();
                        break;
                    case DISCOVER_SERVICES:
                        // This can also happen if the bond information is incorrect. Delete it and reconnect.
                        deleteBondInformation(gatt.getDevice());
                        connectGatt();
                        break;
                    case READ_CHARACTERISTIC:
                        // Disconnected while reading the characteristic. Probably just a link failure.
                        gatt.close();
                        mState = ConnectionState.FAILED;
                        State.notifyChanged();
                        break;
                    case FAILED:
                    case SUCCEEDED:
                        // Normal disconnection.
                        break;
                }
                break;
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status)
        {
            super.onServicesDiscovered(gatt, status);

            // Services have been discovered. Now I try to read a characteristic that requires MitM protection.
            // This triggers pairing and bonding.

            BluetoothGattService nameService = gatt.getService(UUIDs.NAME_SERVICE);
            if (nameService == null)
            {
                // Service not found.
                disconnectGatt();
                mState = ConnectionState.FAILED;
                State.notifyChanged();
                return;
            }
            BluetoothGattCharacteristic characteristic = nameService.getCharacteristic(UUIDs.NAME_CHARACTERISTIC);
            if (characteristic == null)
            {
                // Characteristic not found.
                disconnectGatt();
                mState = ConnectionState.FAILED;
                State.notifyChanged();
                return;
            }

            // Read the characteristic.
            gatt.readCharacteristic(characteristic);
            mState = ConnectionState.READ_CHARACTERISTIC;
            State.notifyChanged();
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
        {
            super.onCharacteristicRead(gatt, characteristic, status);

            if (status == BluetoothGatt.GATT_SUCCESS)
            {
                // Characteristic read. Check it is the right one.
                if (!UUIDs.NAME_CHARACTERISTIC.equals(characteristic.getUuid()))
                {
                    // Read the wrong characteristic. This shouldn't happen.
                    disconnectGatt();
                    mState = ConnectionState.FAILED;
                    State.notifyChanged();
                    return;
                }

                // Get the name (the characteristic I am reading just contains the device name).
                byte[] value = characteristic.getValue();
                if (value == null)
                {
                    // Hmm...
                }

                disconnectGatt();
                mState = ConnectionState.SUCCEEDED;
                State.notifyChanged();

                // Success! Save it to the database or whatever...
            }
            else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION)
            {
                // This is where the tricky part comes
                if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_NONE)
                {
                    // Bonding required.
                    // The broadcast receiver should be called.
                }
                else
                {
                    // ?
                }
            }
            else if (status == GATT_AUTH_FAIL)
            {
                // This can happen because the user ignored the pairing request notification for too long.
                // Or presumably if they put the wrong PIN in.
                disconnectGatt();
                mState = ConnectionState.FAILED;
                State.notifyChanged();
            }
            else if (status == GATT_ERROR)
            {
                // I thought this happened if the bond information was wrong, but now I'm not sure.
                disconnectGatt();
                mState = ConnectionState.FAILED;
                State.notifyChanged();
            }
            else
            {
                // That's weird.
                disconnectGatt();
                mState = ConnectionState.FAILED;
                State.notifyChanged();
            }
        }
    };


    private final BroadcastReceiver mPairingRequestRecevier = new BroadcastReceiver()
    {
        @Override
        public void onReceive(Context context, Intent intent)
        {
            if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction()))
            {
                final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);

                if (type == BluetoothDevice.PAIRING_VARIANT_PIN)
                {
                    device.setPin(Util.IntToPasskey(pinCode()));
                    abortBroadcast();
                }
                else
                {
                    L.w("Unexpected pairing type: " + type);
                }
            }
        }
    };

    public static void deleteBondInformation(BluetoothDevice device)
    {
        try
        {
            // FFS Google, just unhide the method.
            Method m = device.getClass().getMethod("removeBond", (Class[]) null);
            m.invoke(device, (Object[]) null);
        }
        catch (Exception e)
        {
            L.e(e.getMessage());
        }
    }
}
Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • Hi! Could you tell where the MITM protection requirements come from: "Try to read a characteristic that requires encryption MitM protection" Is it a GATT profile spec, the phone, your product? – Eirik M Jul 13 '16 at 10:06
  • It's set on the product. Each GATT characteristic can be configured to be one of [these values](https://github.com/ARMmbed/ble/blob/master/ble/SecurityManager.h#L27). I'm not exactly sure how that corresponds to the [Bluetooth Core Spec](https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=286439&_ga=1.199106534.853042725.1464016791) but there's some info in Vol 3 Part G Section 8, and Vol 3 Part C Section 5.2.2. – Timmmm Jul 13 '16 at 14:51
  • OK, I see it from the code now. Do you really need MITM protection? If the PIN is hard-coded the protection is not very strong. Disabling the MITM protection should result in Just Works pairing, and then there should be no PIN dialog. – Eirik M Jul 13 '16 at 15:08
  • Yes, I need it to prevent casual attackers (i.e. your typical neighbours). It is strong enough for that. – Timmmm Jul 15 '16 at 10:44
  • Works like a charm – Sarweshkumar C R Dec 14 '17 at 05:36
  • What function would you use to send string data to the BLE? something likes sendData("data"); – Billyjoker Jan 12 '18 at 08:29
  • What happens if we don't set high priority for BluetoothDevice.ACTION_PAIRING_REQUEST? – anticafe Jan 31 '18 at 08:50
  • I think it's because it is an [ordered broadcast](https://android-developers.googleblog.com/2011/01/processing-ordered-broadcasts.html) that goes to the highest priority broadcast listener first. They can abort the broadcast, so if there is some other app (or the system) with a higher priority than your app, you might not get the broadcast. Now that I think about it, there should probably be some code to check that the pairing request is actually one that you are responsible for - otherwise when the user tries to pair with a totally unrelated device it might open your app! No idea how though. – Timmmm Jan 31 '18 at 11:05
  • what is `UUIDs` here? – Bishwajyoti Roy Feb 02 '18 at 11:34
  • Just a class full of static members that list the UUIDs of the BLE services and characteristics you are interested in. – Timmmm Feb 02 '18 at 12:24
  • Where is the code for util class and what does the inToPassKey() method do there, i already did a app by using some google sample i am able to read data from peripheral device , but i want to know how to do pairing and bonding when i initiate connection with the devices can you please tell me how to do that alone ? – Jeyaseelan Mar 02 '18 at 05:03
  • It just converts an `int` to a decimal string that `setPin()` expects. Sorry I don't have this code any more but it's a simple function. I vaguely recall that the string is an array of 0-based digits rather than an ASCII string but I don't remember exactly and obviously [the documentation](https://developer.android.com/reference/android/bluetooth/BluetoothDevice.html#setPin(byte[])) doesn't say - this *is* Android documentation after all! You might have to read the source code. – Timmmm Mar 02 '18 at 18:44
  • Worked for me to get rid of user confirmation for pairing (no pin)! I didn't know about the 'abortBroadcast()'. – Brian Reinhold Mar 16 '18 at 09:59
  • @Billyjoker did you get your answer ? i'm looking for something like that – Tejas Pandya Feb 02 '19 at 06:52
  • 1
    @Timmmm i cant figure out what you have placed inside `UUIDs` i'm initialising static variable with null . but it's causing error . please can you show me a way – Tejas Pandya Feb 02 '19 at 08:38
  • Like [this](https://github.com/NordicSemiconductor/Android-BLE-Library/blob/12441cb4fe0074c3e2267500bd28621d288cb398/ble/src/main/java/no/nordicsemi/android/ble/BleManager.java#L118) but it will depend on which service and characteristic you are accessing. This is standard BLE stuff - I suggest you try and find an Android BLE tutorial. – Timmmm Feb 02 '19 at 17:05
  • @Timmmm thanks for your reply . for setting pin in broadcastreceiver . Im getting type 2 which means `PAIRING_VARIANT_PASSKEY_CONFIRMATION` . so its stil showing confirmation dialog . any idea ? i'm testing in android pie – Tejas Pandya Feb 04 '19 at 07:52
  • @Tejas Pandya you should have the bluetoothdescriptor to do something like - descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); this.bluetoothGatt.writeDescriptor(descriptor); – Billyjoker Feb 04 '19 at 09:49
  • @Billyjoker but im passing data from android to arduio (HC-05) module . so would be the same approach for both ? – Tejas Pandya Feb 04 '19 at 10:15
  • @Tejas Pandya in the Arduino side you have to read bytes with bluetooth.read(); – Billyjoker Feb 04 '19 at 11:48
  • This solution works for me to connect the BLE Device but the weird part is as soon as it connects the BLE Device after 30 seconds or so, android disconnects the ble device automatically. I am still figuring out why it disconnects the device? I've post my detailed question here : https://stackoverflow.com/questions/57892013/bluetooth-connection-drops-automatically-with-anrdoid-application – MMJ Sep 16 '19 at 15:23
  • Are you able to connect to device? – Amit Gandole Oct 04 '19 at 09:56
  • @Timmmm UUIDs isn't resolved. Can you please help? – Nguyen Minh Binh Feb 14 '21 at 15:00
  • @NguyenMinhBinh: It's just a class full of static members that list the UUIDs of the BLE services and characteristics you are interested in. – Timmmm Feb 14 '21 at 21:15
6

I also faced the same problem and after all the research, I figured out the below solution to pair to a BLE without any manual intervention.

(Tested and working!!!)

I am basically looking for a particular Bluetooth device (I know MAC address) and pair with it once found. The first thing to do is to create pair request using a broadcast receiver and handle the request as below.

IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
                intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
                registerReceiver(broadCastReceiver,intentFilter);

You need to write the broadcastReceiver and handle it as below.

String BLE_PIN = "1234"
private BroadcastReceiver broadCastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(BluetoothDevice.ACTION_PAIRING_REQUEST.equals(action))
        {
            BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            bluetoothDevice.setPin(BLE_PIN.getBytes());
            Log.e(TAG,"Auto-entering pin: " + BLE_PIN);
            bluetoothDevice.createBond();
            Log.e(TAG,"pin entered and request sent...");
        }
    }
};

Voila! You should be able to pair to Bluetooth device without ANY MANUAL INTERVENTION.

Hope this helps :-) Please make it right answer if it works for you.

Varun A M
  • 1,103
  • 2
  • 14
  • 29