BLE pairing has three phases — feature exchange, key generation, and key distribution — producing an encrypted link

Every GATT operation on a Bluetooth LE link — reading a heart rate characteristic, writing a configuration value, subscribing to notifications — travels over an unencrypted connection by default. Any device in radio range that has captured the pairing exchange or hijacked the link can read or modify that traffic. Pairing is what closes this gap.

Pairing is the process by which two BLE devices establish a shared secret key and use it to encrypt (and optionally authenticate) the link. It is not the same as connecting. Connecting creates a physical link. Pairing decides whether that link will carry plaintext or ciphertext — and whether the remote device can prove it is who it claims to be.

The Bluetooth Framework exposes the entire pairing workflow through the wclBluetoothManager class. Applications configure their IO capabilities and MITM requirements, provide PINs and passkeys when requested, and receive a single completion event that carries the final result.


Pairing vs bonding

The two terms are often used interchangeably, but they describe different things.

Pairing is the act of establishing a shared encryption key. The key exists for the duration of the connection and is discarded when the link drops — unless bonding is enabled.

Bonding is the act of saving the keys in non-volatile storage on both sides, so that the next time the devices meet they can skip the pairing exchange and go straight to an encrypted link using the stored keys.

The practical difference matters at every layer of the API. In the Bluetooth Framework, a device that has been paired but not bonded will report GetRemotePaired as true only while the link is active. A bonded device reports true even before a connection is established, because the bond record persists on the host and on the peripheral.

Bonding is controlled by the MITM protection flags described later in this article — specifically, the mitmProtectionNotRequiredBonding and mitmProtectionRequiredBonding variants request bonding in addition to plain pairing.


The three phases of pairing

The Bluetooth Core Specification defines three phases that every pairing procedure goes through.

Phase 1 — Pairing Feature Exchange. The two devices exchange their IO capabilities, their authentication requirements (MITM or not), and any flags indicating whether bonding, secure connections, and OOB are supported. The result of this phase determines which pairing method will be used.

Phase 2 — Key Generation. Depending on the negotiated method, the devices either exchange a Temporary Key derived from a 6-digit PIN (legacy pairing), or perform an Elliptic-Curve Diffie-Hellman exchange (LE Secure Connections, available in Bluetooth 4.2 and above). The output is a Short Term Key used to encrypt the link.

Phase 3 — Key Distribution. The encrypted link is now used to distribute the long-term keys: the Long Term Key (LTK) for future reconnections, the Identity Resolving Key (IRK) for resolving random private addresses, and the Connection Signature Resolving Key (CSRK) for signed writes. If bonding is enabled, these keys are stored on both sides.

The Bluetooth Framework handles all three phases transparently. Applications observe only the events that Phase 1 requires user interaction for: IO capability requests, MITM requests, and the specific method event (Just Works confirmation, PIN, passkey, or numeric comparison).


Pairing methods

Four pairing methods exist. Which one is selected depends entirely on the IO capabilities exchanged in Phase 1.

Just Works

Both devices have no display and no keyboard (or they are configured to behave that way). The pairing proceeds without any user interaction beyond an optional confirmation. The link is encrypted, but it is not protected against a Man-in-the-Middle attacker who relays the pairing exchange between the two legitimate devices.

Just Works is common for low-power sensors, fitness trackers, and any device where a display would be too costly. It is acceptable when the sensitivity of the data is low, and unacceptable for anything that carries credentials, biometrics, or control over physical infrastructure.

In the Bluetooth Framework, Just Works pairing fires the OnConfirm event. The application either accepts or rejects the pairing. The GattClient sample accepts unconditionally:

procedure TfmMain.wclBluetoothManagerConfirm(Sender: TObject;
  const Radio: TwclBluetoothRadio; const Address: Int64;
  out Confirm: Boolean);
begin
  // Accept any pairing.
  Confirm := True;
  TraceEvent(Address, 'Just works pairing', 'Accept', 'True');
end;
void __fastcall TfmMain::wclBluetoothManagerConfirm(TObject *Sender,
      const TwclBluetoothRadio *Radio, const __int64 Address, bool& Confirm)
{
  // Accept any pairing.
  Confirm = true;
  TraceEvent(Address, "Just works pairing", "Accept", "True");
}
void Manager_OnConfirm(Object Sender, wclBluetoothRadio Radio, Int64 Address,
    out Boolean Confirm)
{
    // Accept any pairing.
    Confirm = true;
    TraceEvent(Address, "Just works pairing", "Accept", "True");
}
Private Sub Manager_OnConfirm(Sender As Object, Radio As wclBluetooth.wclBluetoothRadio,
        Address As Long, ByRef Confirm As Boolean) Handles Manager.OnConfirm
    ' Accept any pairing.
    Confirm = True
    TraceEvent(Address, "Just works pairing", "Accept", "True")
End Sub
void CGattClientDlg::wclBluetoothManagerConfirm(void* Sender, CwclBluetoothRadio* const Radio,
    const __int64 Address, bool& Confirm)
{
  UNREFERENCED_PARAMETER(Radio);
  UNREFERENCED_PARAMETER(Sender);

  // Accept any pairing.
  Confirm = true;
  TraceEvent(Address, _T("Just works pairing"), _T("Accept"), _T("True"));
}

Passkey Entry

One device displays a 6-digit passkey, and the user types it into the other device. Both devices contribute to the final key in a way that proves the user was present on both sides — so MITM protection is provided.

Two events are involved:

  • OnPasskeyNotification fires on the device that displays the passkey. The application should show it to the user.
  • OnPasskeyRequest fires on the device that inputs the passkey. The application should ask the user for it and return the value.

The sample implements both:

procedure TfmMain.wclBluetoothManagerPasskeyNotification(Sender: TObject;
  const Radio: TwclBluetoothRadio; const Address: Int64;
  const Passkey: Cardinal);
begin
  TraceEvent(Address, 'Passkey notification', 'Passkey', IntToStr(Passkey));
end;

procedure TfmMain.wclBluetoothManagerPasskeyRequest(Sender: TObject;
  const Radio: TwclBluetoothRadio; const Address: Int64;
  out Passkey: Cardinal);
begin
  // Use 123456 as passkey.
  Passkey := 123456;
  TraceEvent(Address, 'Passkey request', 'Passkey', IntToStr(Passkey));
end;
void __fastcall TfmMain::wclBluetoothManagerPasskeyNotification(
      TObject *Sender, const TwclBluetoothRadio *Radio,
      const __int64 Address, const DWORD Passkey)
{
  TraceEvent(Address, "Passkey notification", "Passkey", IntToStr((int)Passkey));
}

void __fastcall TfmMain::wclBluetoothManagerPasskeyRequest(TObject *Sender,
      const TwclBluetoothRadio *Radio, const __int64 Address,
      DWORD& Passkey)
{
  // Use 123456 as passkey.
  Passkey = 123456;
  TraceEvent(Address, "Passkey request", "Passkey", IntToStr((int)Passkey));
}
void Manager_OnPasskeyNotification(Object Sender, wclBluetoothRadio Radio,
    Int64 Address, UInt32 Passkey)
{
    TraceEvent(Address, "Passkey notification", "Passkey", Passkey.ToString());
}

void Manager_OnPasskeyRequest(Object Sender, wclBluetoothRadio Radio,
    Int64 Address, out UInt32 Passkey)
{
    // Use 123456 as passkey.
    Passkey = 123456;
    TraceEvent(Address, "Passkey request", "Passkey", Passkey.ToString());
}
Private Sub Manager_OnPasskeyNotification(ByVal Sender As Object,
        ByVal Radio As wclBluetooth.wclBluetoothRadio, ByVal Address As Long,
        ByVal Passkey As UInteger) Handles Manager.OnPasskeyNotification
    TraceEvent(Address, "Passkey notification", "Passkey", Passkey.ToString())
End Sub

Private Sub Manager_OnPasskeyRequest(ByVal Sender As Object,
        ByVal Radio As wclBluetooth.wclBluetoothRadio, ByVal Address As Long,
        ByRef Passkey As UInteger) Handles Manager.OnPasskeyRequest
    ' Use 123456 as passkey.
    Passkey = 123456
    TraceEvent(Address, "Passkey request", "Passkey", Passkey.ToString())
End Sub
void CGattClientDlg::wclBluetoothManagerPasskeyNotification(void* Sender,
    CwclBluetoothRadio* const Radio, const __int64 Address, const unsigned long Passkey)
{
  UNREFERENCED_PARAMETER(Sender);
  UNREFERENCED_PARAMETER(Radio);

  TraceEvent(Address, _T("Passkey notification"), _T("Passkey"), IntToStr((int)Passkey));
}

void CGattClientDlg::wclBluetoothManagerPasskeyRequest(void* Sender,
    CwclBluetoothRadio* const Radio, const __int64 Address, unsigned long& Passkey)
{
  UNREFERENCED_PARAMETER(Sender);
  UNREFERENCED_PARAMETER(Radio);

  // Use 123456 as passkey.
  Passkey = 123456;
  TraceEvent(Address, _T("Passkey request"), _T("Passkey"), IntToStr((int)Passkey));
}

In a real application, OnPasskeyNotification would display a modal dialog, and OnPasskeyRequest would show an input prompt. The sample uses a hardcoded value to keep the demonstration deterministic.

Numeric Comparison

Both devices have a display capable of showing a 6-digit number, and at least one has a yes/no confirmation button. Both devices compute the same number from their shared secret and display it. The user compares the two numbers and confirms that they match. If they do not, an attacker is present.

The Bluetooth Framework exposes this through OnNumericComparison. The Number parameter contains the value both devices must show:

procedure TfmMain.wclBluetoothManagerNumericComparison(Sender: TObject;
  const Radio: TwclBluetoothRadio; const Address: Int64;
  const Number: Cardinal; out Confirm: Boolean);
begin
  // Accept any pairing.
  Confirm := True;
  TraceEvent(Address, 'Numeric comparison', 'Number', IntToStr(Number));
end;
void __fastcall TfmMain::wclBluetoothManagerNumericComparison(
      TObject *Sender, const TwclBluetoothRadio *Radio,
      const __int64 Address, const DWORD Number, bool& Confirm)
{
  // Accept any pairing.
  Confirm = true;
  TraceEvent(Address, "Numeric comparison", "Number", IntToStr((int)Number));
}
void Manager_OnNumericComparison(Object Sender, wclBluetoothRadio Radio,
    Int64 Address, UInt32 Number, out Boolean Confirm)
{
    // Accept any pairing.
    Confirm = true;
    TraceEvent(Address, "Numeric comparison", "Number", Number.ToString());
}
Private Sub Manager_OnNumericComparison(ByVal Sender As Object,
        ByVal Radio As wclBluetooth.wclBluetoothRadio, ByVal Address As Long,
        ByVal Number As UInteger, ByRef Confirm As Boolean) Handles Manager.OnNumericComparison
    ' Accept any pairing.
    Confirm = True
    TraceEvent(Address, "Numeric comparison", "Number", Number.ToString())
End Sub
void CGattClientDlg::wclBluetoothManagerNumericComparison(void* Sender,
    CwclBluetoothRadio* const Radio, const __int64 Address,
    const unsigned long Number, bool& Confirm)
{
  UNREFERENCED_PARAMETER(Sender);
  UNREFERENCED_PARAMETER(Radio);

  // Accept any pairing.
  Confirm = true;
  TraceEvent(Address, _T("Numeric comparison"), _T("Number"), IntToStr((int)Number));
}

Numeric Comparison is the strongest method that does not require OOB hardware. It is used by modern devices with displays: smartphones, laptops, and appliances with LCD panels.

Out of Band

The pairing key is transferred through a channel that is not the Bluetooth LE link — typically an NFC tap, a QR code, or a wired connection. Because the attacker cannot see the OOB channel, MITM is impossible.

The OnIoCapabilityRequest event includes an OobPresent output parameter. When the application sets it to true, the framework attempts to use OOB data. The GattClient sample always sets it to false because no OOB hardware is assumed. Applications that support OOB would set it to true in the appropriate circumstances.


IO capabilities and MITM protection

The four pairing methods are selected by a matrix of IO capabilities. The Bluetooth Framework models these as two enumerations, both set through the OnIoCapabilityRequest event.

wclBluetoothIoCapability describes what the local device can do:

ValueMeaning
iocapDisplayOnlyHas a display, cannot enter data
iocapDisplayYesNoHas a display and a yes/no button
iocapKeyboardOnlyHas a keyboard, no display
iocapNoInputNoOutputNo display, no keyboard
iocapDisplayKeyboardBoth display and keyboard
iocapNotDefinedLet the stack decide

wclBluetoothMitmProtection describes the level of protection the application requires:

ValueMeaning
mitmProtectionNotRequiredEncrypt the link, no authentication
mitmProtectionRequiredAuthenticate the link
mitmProtectionNotRequiredBondingEncrypt, save keys
mitmProtectionRequiredBondingAuthenticate, save keys
mitmProtectionNotRequiredGeneralBondingEncrypt, save keys including CSRK
mitmProtectionRequiredGeneralBondingAuthenticate, save keys including CSRK
mitmProtectionNotDefinedLet the stack decide

The OnIoCapabilityRequest event carries both. The application sets the values and returns:

procedure TfmMain.wclBluetoothManagerIoCapabilityRequest(Sender: TObject;
  const Radio: TwclBluetoothRadio; const Address: Int64;
  out Mitm: TwclBluetoothMitmProtection;
  out IoCapability: TwclBluetoothIoCapability; out OobPresent: Boolean);
begin
  case cbMitmProtection.ItemIndex of
    0: Mitm := mitmProtectionNotRequired;
    1: Mitm := mitmProtectionRequired;
    2: Mitm := mitmProtectionNotRequiredBonding;
    3: Mitm := mitmProtectionRequiredBonding;
    4: Mitm := mitmProtectionNotRequiredGeneralBonding;
    5: Mitm := mitmProtectionRequiredGeneralBonding;
    else Mitm := mitmProtectionNotDefined;
  end;

  case cbIoCap.ItemIndex of
    0: IoCapability := iocapDisplayOnly;
    1: IoCapability := iocapDisplayYesNo;
    2: IoCapability := iocapKeyboardOnly;
    3: IoCapability := iocapNoInputNoOutput;
    4: IoCapability := iocapDisplayKeyboard;
    else IoCapability := iocapNotDefined;
  end;

  OobPresent := False;
end;
void __fastcall TfmMain::wclBluetoothManagerIoCapabilityRequest(
      TObject *Sender, const TwclBluetoothRadio *Radio,
      const __int64 Address, TwclBluetoothMitmProtection& Mitm,
      TwclBluetoothIoCapability& IoCapability, bool& OobPresent)
{
  switch (cbMitmProtection->ItemIndex)
  {
    case 0:
      Mitm = mitmProtectionNotRequired;
      break;
    case 1:
      Mitm = mitmProtectionRequired;
      break;
    case 2:
      Mitm = mitmProtectionNotRequiredBonding;
      break;
    case 3:
      Mitm = mitmProtectionRequiredBonding;
      break;
    case 4:
      Mitm = mitmProtectionNotRequiredGeneralBonding;
      break;
    case 5:
      Mitm = mitmProtectionRequiredGeneralBonding;
      break;
    default:
      Mitm = mitmProtectionNotDefined;
      break;
  }

  switch (cbIoCap->ItemIndex)
  {
    case 0:
      IoCapability = iocapDisplayOnly;
      break;
    case 1:
      IoCapability = iocapDisplayYesNo;
      break;
    case 2:
      IoCapability = iocapKeyboardOnly;
      break;
    case 3:
      IoCapability = iocapNoInputNoOutput;
      break;
    case 4:
      IoCapability = iocapDisplayKeyboard;
      break;
    default:
      IoCapability = iocapNotDefined;
      break;
  }

  OobPresent = false;
}
void Manager_OnIoCapabilityRequest(Object Sender, wclBluetoothRadio Radio, Int64 Address,
    out wclBluetoothMitmProtection Mitm, out wclBluetoothIoCapability IoCapability,
    out Boolean OobPresent)
{
    switch (cbMitmProtection.SelectedIndex)
    {
        case 0:
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotRequired;
            break;
        case 1:
            Mitm = wclBluetoothMitmProtection.mitmProtectionRequired;
            break;
        case 2:
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotRequiredBonding;
            break;
        case 3:
            Mitm = wclBluetoothMitmProtection.mitmProtectionRequiredBonding;
            break;
        case 4:
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotRequiredGeneralBonding;
            break;
        case 5:
            Mitm = wclBluetoothMitmProtection.mitmProtectionRequiredGeneralBonding;
            break;
        default:
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotDefined;
            break;
    }

    switch (cbIoCap.SelectedIndex)
    {
        case 0:
            IoCapability = wclBluetoothIoCapability.iocapDisplayOnly;
            break;
        case 1:
            IoCapability = wclBluetoothIoCapability.iocapDisplayYesNo;
            break;
        case 2:
            IoCapability = wclBluetoothIoCapability.iocapKeyboardOnly;
            break;
        case 3:
            IoCapability = wclBluetoothIoCapability.iocapNoInputNoOutput;
            break;
        case 4:
            IoCapability = wclBluetoothIoCapability.iocapDisplayKeyboard;
            break;
        default:
            IoCapability = wclBluetoothIoCapability.iocapNotDefined;
            break;
    }

    OobPresent = false;
}
Private Sub Manager_OnIoCapabilityRequest(Sender As Object,
        Radio As wclBluetooth.wclBluetoothRadio, Address As Long,
        ByRef Mitm As wclBluetooth.wclBluetoothMitmProtection,
        ByRef IoCapability As wclBluetooth.wclBluetoothIoCapability,
        ByRef OobPresent As Boolean) Handles Manager.OnIoCapabilityRequest
    Select cbMitmProtection.SelectedIndex
        Case 0
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotRequired
        Case 1
            Mitm = wclBluetoothMitmProtection.mitmProtectionRequired
        Case 2
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotRequiredBonding
        Case 3
            Mitm = wclBluetoothMitmProtection.mitmProtectionRequiredBonding
        Case 4
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotRequiredGeneralBonding
        Case 5
            Mitm = wclBluetoothMitmProtection.mitmProtectionRequiredGeneralBonding
        Case Else
            Mitm = wclBluetoothMitmProtection.mitmProtectionNotDefined
    End Select

    Select Case cbIoCap.SelectedIndex
        Case 0
            IoCapability = wclBluetoothIoCapability.iocapDisplayOnly
        Case 1
            IoCapability = wclBluetoothIoCapability.iocapDisplayYesNo
        Case 2
            IoCapability = wclBluetoothIoCapability.iocapKeyboardOnly
        Case 3
            IoCapability = wclBluetoothIoCapability.iocapNoInputNoOutput
        Case 4
            IoCapability = wclBluetoothIoCapability.iocapDisplayKeyboard
        Case Else
            IoCapability = wclBluetoothIoCapability.iocapNotDefined
    End Select

    OobPresent = False
End Sub
void CGattClientDlg::wclBluetoothManagerIoCapabilityRequest(void* Sender,
    CwclBluetoothRadio* const Radio, const __int64 Address, wclBluetoothMitmProtection& Mitm,
    wclBluetoothIoCapability& IoCapability, bool& OobPresent)
{
  UNREFERENCED_PARAMETER(Sender);
  UNREFERENCED_PARAMETER(Radio);
  UNREFERENCED_PARAMETER(Address);

  switch (cbMitm.GetCurSel())
  {
    case 0:
      Mitm = mitmProtectionNotRequired;
      break;
    case 1:
      Mitm = mitmProtectionRequired;
      break;
    case 2:
      Mitm = mitmProtectionNotRequiredBonding;
      break;
    case 3:
      Mitm = mitmProtectionRequiredBonding;
      break;
    case 4:
      Mitm = mitmProtectionNotRequiredGeneralBonding;
      break;
    case 5:
      Mitm = mitmProtectionRequiredGeneralBonding;
      break;
    default:
      Mitm = mitmProtectionNotDefined;
      break;
  }

  switch (cbIoCap.GetCurSel())
  {
    case 0:
      IoCapability = iocapDisplayOnly;
      break;
    case 1:
      IoCapability = iocapDisplayYesNo;
      break;
    case 2:
      IoCapability = iocapKeyboardOnly;
      break;
    case 3:
      IoCapability = iocapNoInputNoOutput;
      break;
    case 4:
      IoCapability = iocapDisplayKeyboard;
      break;
    default:
      IoCapability = iocapNotDefined;
      break;
  }

  OobPresent = false;
}

The framework combines the two values with the peer's capabilities to select the pairing method automatically. Applications do not choose the method directly.


Security levels

Once pairing is complete, the link is protected according to the negotiated level. The Bluetooth Framework exposes two distinct enumerations for this.

wclBluetoothLeProtectionLevel is used during pairing itself, negotiated through OnProtectionLevelRequest. It controls how the pairing exchange proceeds:

ValueMeaning
pplNoneNo protection — the link stays unencrypted
pplDefaultUse the driver default
pplEncryptionEncrypt the link, but do not authenticate
pplEncryptionAndAuthenticationEncrypt and authenticate — full MITM protection
procedure TfmMain.wclBluetoothManagerProtectionLevelRequest(
  Sender: TObject; const Radio: TwclBluetoothRadio; const Address: Int64;
  out Protection: TwclBluetoothLeProtectionLevel);
begin
  case cbProtection.ItemIndex of
    0: Protection := pplNone;
    1: Protection := pplDefault;
    2: Protection := pplEncryption;
    3: Protection := pplEncryptionAndAuthentication
  end;
end;
void __fastcall TfmMain::wclBluetoothManagerProtectionLevelRequest(
      TObject *Sender, const TwclBluetoothRadio *Radio,
      const __int64 Address, TwclBluetoothLeProtectionLevel& Protection)
{
  switch (cbProtection->ItemIndex)
  {
    case 0:
      Protection = pplNone;
      break;
    case 1:
      Protection = pplDefault;
      break;
    case 2:
      Protection = pplEncryption;
      break;
    case 3:
      Protection = pplEncryptionAndAuthentication;
      break;
    default:
      Protection = pplDefault;
      break;
  }
}
void Manager_OnProtectionLevelRequest(Object Sender, wclBluetoothRadio Radio, Int64 Address,
    out wclBluetoothLeProtectionLevel Protection)
{
    switch (cbProtection.SelectedIndex)
    {
        case 0:
            Protection = wclBluetoothLeProtectionLevel.pplNone;
            break;
        case 1:
            Protection = wclBluetoothLeProtectionLevel.pplDefault;
            break;
        case 2:
            Protection = wclBluetoothLeProtectionLevel.pplEncryption;
            break;
        case 3:
            Protection = wclBluetoothLeProtectionLevel.pplEncryptionAndAuthentication;
            break;
        default:
            Protection = wclBluetoothLeProtectionLevel.pplDefault;
            break;
    }
}
Private Sub Manager_OnProtectionLevelRequest(Sender As Object,
        Radio As wclBluetooth.wclBluetoothRadio, Address As Int64,
        ByRef Protection As wclBluetooth.wclBluetoothLeProtectionLevel) Handles Manager.OnProtectionLevelRequest
    Select Case cbProtection.SelectedIndex
        Case 0
            Protection = wclBluetoothLeProtectionLevel.pplNone
        Case 1
            Protection = wclBluetoothLeProtectionLevel.pplDefault
        Case 2
            Protection = wclBluetoothLeProtectionLevel.pplEncryption
        Case 3
            Protection = wclBluetoothLeProtectionLevel.pplEncryptionAndAuthentication
        Case Else
            Protection = wclBluetoothLeProtectionLevel.pplDefault
    End Select
End Sub
void CGattClientDlg::wclBluetoothManagerProtectionLevelRequest(void* Sender,
    CwclBluetoothRadio* const Radio, const __int64 Address,
    wclBluetoothLeProtectionLevel& Protection)
{
  UNREFERENCED_PARAMETER(Sender);
  UNREFERENCED_PARAMETER(Radio);
  UNREFERENCED_PARAMETER(Address);

  switch (cbProtection.GetCurSel())
  {
    case 0:
      Protection = pplNone;
      break;
    case 1:
      Protection = pplDefault;
      break;
    case 2:
      Protection = pplEncryption;
      break;
    case 3:
      Protection = pplEncryptionAndAuthentication;
      break;
    default:
      Protection = pplDefault;
      break;
  }
}

wclGattProtectionLevel is used at the GATT operation level, passed to every read, write, and descriptor operation. It tells the framework what level of protection the individual operation requires:

ValueMeaning
plNoneNo protection required
plAuthenticationAuthentication required
plEncryptionEncryption required
plEncryptionAndAuthenticationBoth required
function TfmMain.Protection: TwclGattProtectionLevel;
begin
  case cbProtection.ItemIndex of
    0: Result := plNone;
    1: Result := plAuthentication;
    2: Result := plEncryption;
    3: Result := plEncryptionAndAuthentication;
  else
    Result := plNone;
  end;
end;
TwclGattProtectionLevel TfmMain::Protection()
{
  switch (cbProtection->ItemIndex)
  {
    case 0:
      return plNone;
    case 1:
      return plAuthentication;
    case 2:
      return plEncryption;
    case 3:
      return plEncryptionAndAuthentication;
    default:
      return plNone;
  }
}
private wclGattProtectionLevel Protection()
{
    switch (cbProtection.SelectedIndex)
    {
        case 0:
            return wclGattProtectionLevel.plNone;
        case 1:
            return wclGattProtectionLevel.plAuthentication;
        case 2:
            return wclGattProtectionLevel.plEncryption;
        case 3:
            return wclGattProtectionLevel.plEncryptionAndAuthentication;
        default:
            return wclGattProtectionLevel.plNone;
    }
}
Private Function Protection() As wclGattProtectionLevel
    Select Case cbProtection.SelectedIndex
        Case 0
            Return wclGattProtectionLevel.plNone
        Case 1
            Return wclGattProtectionLevel.plAuthentication
        Case 2
            Return wclGattProtectionLevel.plEncryption
        Case 3
            Return wclGattProtectionLevel.plEncryptionAndAuthentication
        Case Else
            Return wclGattProtectionLevel.plNone
    End Select
End Function
wclGattProtectionLevel CGattClientDlg::Protection() const
{
  switch (cbProtection.GetCurSel())
  {
    case 0:
      return plNone;
    case 1:
      return plAuthentication;
    case 2:
      return plEncryption;
    case 3:
      return plEncryptionAndAuthentication;
    default:
      return plNone;
  }
}

The two levels work together. If the pairing negotiated pplEncryptionAndAuthentication, then any GATT operation requesting plAuthentication or plEncryptionAndAuthentication will succeed. If the pairing only achieved pplEncryption, then a GATT operation requesting plAuthentication will fail with WCL_E_BLUETOOTH_LE_AUTH_ACCESS_DENIED.


Pairing in the Bluetooth Framework

Protection level

The protection level is set through the same combo box in the sample, and read back at the moment of each GATT operation. The combo contains four entries (None, Authentication, Encryption, Encryption And Authentication) and the selection drives both the pairing request and the GATT operation requests.

IO capabilities and MITM

Both values are set through OnIoCapabilityRequest as shown above. They are stored on the wclBluetoothManager class for the lifetime of the pairing exchange. The combo boxes cbMitmProtection and cbIoCap are populated during form initialization:

cbMitmProtection.ItemIndex := 0;
cbIoCap.ItemIndex := 0;
cbMitm.AddString("mitmProtectionNotRequired");
cbMitm.AddString("mitmProtectionRequired");
cbMitm.AddString("mitmProtectionNotRequiredBonding");
cbMitm.AddString("mitmProtectionRequiredBonding");
cbMitm.AddString("mitmProtectionNotRequiredGeneralBonding");
cbMitm.AddString("mitmProtectionRequiredGeneralBonding");
cbMitm.AddString("mitmProtectionNotDefined");
cbMitm.SetCurSel(0);

cbIoCap.AddString("iocapDisplayOnly");
cbIoCap.AddString("iocapDisplayYesNo");
cbIoCap.AddString("iocapKeyboardOnly");
cbIoCap.AddString("iocapNoInputNoOutput");
cbIoCap.AddString("iocapDisplayKeyboard");
cbIoCap.AddString("iocapNotDefined");
cbIoCap.SetCurSel(0);
cbMitmProtection.SelectedIndex = 0;
cbIoCap.SelectedIndex = 0;
cbMitmProtection.SelectedIndex = 0
cbIoCap.SelectedIndex = 0
cbMitm.AddString(_T("mitmProtectionNotRequired"));
cbMitm.AddString(_T("mitmProtectionRequired"));
cbMitm.AddString(_T("mitmProtectionNotRequiredBonding"));
cbMitm.AddString(_T("mitmProtectionRequiredBonding"));
cbMitm.AddString(_T("mitmProtectionNotRequiredGeneralBonding"));
cbMitm.AddString(_T("mitmProtectionRequiredGeneralBonding"));
cbMitm.AddString(_T("mitmProtectionNotDefined"));
cbMitm.SetCurSel(0);

cbIoCap.AddString(_T("iocapDisplayOnly"));
cbIoCap.AddString(_T("iocapDisplayYesNo"));
cbIoCap.AddString(_T("iocapKeyboardOnly"));
cbIoCap.AddString(_T("iocapNoInputNoOutput"));
cbIoCap.AddString(_T("iocapDisplayKeyboard"));
cbIoCap.AddString(_T("iocapNotDefined"));
cbIoCap.SetCurSel(0);

Pairing events

Eight events make up the complete pairing workflow:

EventPurpose
OnIoCapabilityRequestNegotiate IO capabilities and MITM requirements
OnProtectionLevelRequestChoose the target security level
OnConfirmAnswer Just Works pairing
OnPasskeyNotificationDisplay a passkey to the user
OnPasskeyRequestAsk the user for a passkey
OnNumericComparisonConfirm a 6-digit number
OnPinRequestProvide a legacy PIN
OnAuthenticationCompletedReceive the final result

Only OnAuthenticationCompleted is guaranteed to fire on every pairing attempt. The others fire depending on the method selected:

procedure TfmMain.wclBluetoothManagerAuthenticationCompleted(
  Sender: TObject; const Radio: TwclBluetoothRadio; const Address: Int64;
  const Error: Integer);
begin
  TraceEvent(Address, 'Authentication completed', 'Error', IntToHex(Error, 8));
end;
void __fastcall TfmMain::wclBluetoothManagerAuthenticationCompleted(
      TObject *Sender, const TwclBluetoothRadio *Radio,
      const __int64 Address, const int Error)
{
  TraceEvent(Address, "Authentication completed", "Error", IntToHex(Error, 9));
}
void Manager_OnAuthenticationCompleted(Object Sender, wclBluetoothRadio Radio,
    Int64 Address, Int32 Error)
{
    TraceEvent(Address, "Authentication completed", "Error", Error.ToString("X8"));
}
Private Sub Manager_OnAuthenticationCompleted(Sender As Object,
        Radio As wclBluetooth.wclBluetoothRadio, Address As Long,
        [Error] As Integer) Handles Manager.OnAuthenticationCompleted
    TraceEvent(Address, "Authentication completed", "Error", [Error].ToString("X8"))
End Sub
void CGattClientDlg::wclBluetoothManagerAuthenticationCompleted(void* Sender,
    CwclBluetoothRadio* const Radio, const __int64 Address, const int Error)
{
  UNREFERENCED_PARAMETER(Radio);
  UNREFERENCED_PARAMETER(Sender);

  TraceEvent(Address, _T("Authentication completed"), _T("Error"), IntToHex(Error, 9));
}

The Error parameter is WCL_E_SUCCESS on success or one of the pairing error codes on failure. WCL_E_BLUETOOTH_LE_ALREADY_PAIRED indicates the device was already bonded — this is not a failure, and the application should treat it as success. WCL_E_BLUETOOTH_PAIRED_BY_OTHER indicates the device is bonded to a different host and the local pair request was rejected.

Manual pairing

By default, pairing happens automatically as a side effect of the first protected GATT operation. The framework triggers pairing when a read, write, or subscription requires a level of protection that the current link does not provide.

Some applications need to pair explicitly, before any GATT operation. The RemotePair method on wclBluetoothRadio does exactly that:

procedure TfmMain.btPairClick(Sender: TObject);
var
  Res: Integer;
  Item: TListItem;
begin
  if lvDevices.Selected = nil then
    MessageDlg('Select device', mtWarning, [mbOK], 0)

  else begin
    Item := lvDevices.Selected;
    Res := TwclBluetoothRadio(Item.Data).RemotePair(StrToInt64('$' +
      Item.Caption), pmLe);
    if Res <> WCL_E_SUCCESS then
      MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
  end;
end;
void __fastcall TfmMain::btPairClick(TObject *Sender)
{
  if (lvDevices->Selected == NULL)
    MessageDlg("Select device", mtWarning, TMsgDlgButtons() << mbOK, 0);

  else
  {
    TListItem* Item = lvDevices->Selected;
    int Res = ((TwclBluetoothRadio*)Item->Data)->RemotePair(StrToInt64("$" + Item->Caption),
      pmLe);
    if (Res != WCL_E_SUCCESS)
    {
      MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError,
        TMsgDlgButtons() << mbOK, 0);
    }
  }
}
private void btPair_Click(Object sender, EventArgs e)
{
    if (lvDevices.SelectedItems.Count == 0)
        MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    else
    {
        ListViewItem Item = lvDevices.SelectedItems[0];
        Int32 Res = ((wclBluetoothRadio)Item.Tag).RemotePair(Convert.ToInt64(Item.Text, 16),
            wclBluetoothPairingMethod.pmLe);
        if (Res != wclErrors.WCL_E_SUCCESS)
        {
            MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error",
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}
Private Sub btPair_Click(sender As System.Object, e As System.EventArgs) Handles btPair.Click
    If lvDevices.SelectedItems.Count = 0 Then
        MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    Else
        Dim Item As ListViewItem = lvDevices.SelectedItems(0)
        Dim Res As Integer = (CType(Item.Tag, wclBluetoothRadio)).RemotePair(Convert.ToInt64(Item.Text, 16),
                                                                             wclBluetoothPairingMethod.pmLe)
        If Res <> wclErrors.WCL_E_SUCCESS Then
            MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End If
    End If
End Sub
void CGattClientDlg::OnBnClickedButtonPair()
{
  POSITION Pos = lvDevices.GetFirstSelectedItemPosition();
  if (Pos == NULL)
    AfxMessageBox(_T("Select device"));
  else
  {
    int Item = lvDevices.GetNextSelectedItem(Pos);
    int Res = ((CwclBluetoothRadio*)lvDevices.GetItemData(Item))->RemotePair(StrToInt64(lvDevices.GetItemText(Item, 0)),
      pmLe);
    if (Res != WCL_E_SUCCESS)
      AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
  }
}

The second parameter is the pairing method. pmLe forces BLE pairing; pmClassic forces classic Bluetooth; pmAuto lets the framework choose based on the device type. Explicit BLE pairing is useful when the device supports both classic and BLE, or when the pairing must happen before any connection attempt.


Unpairing and bond management

Removing a bond is done through RemoteUnpair. The device must be selected from the device list, and the pairing method must match the one used during the initial pair:

procedure TfmMain.btUnpairClick(Sender: TObject);
var
  Res: Integer;
  Item: TListItem;
begin
  if lvDevices.Selected = nil then
    MessageDlg('Select device', mtWarning, [mbOK], 0)

  else begin
    Item := lvDevices.Selected;
    Res := TwclBluetoothRadio(Item.Data).RemoteUnpair(StrToInt64('$' +
      Item.Caption), pmLe);
    if Res <> WCL_E_SUCCESS then
      MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
  end;
end;
void __fastcall TfmMain::btUnpairClick(TObject *Sender)
{
  if (lvDevices->Selected == NULL)
    MessageDlg("Select device", mtWarning, TMsgDlgButtons() << mbOK, 0);
  else
  {
    TListItem* Item = lvDevices->Selected;
    int Res = ((TwclBluetoothRadio*)Item->Data)->RemoteUnpair(StrToInt64("$" +
      Item->Caption), pmLe);
    if (Res != WCL_E_SUCCESS)
      MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
  }
}
private void btUnpair_Click(Object sender, EventArgs e)
{
    if (lvDevices.SelectedItems.Count == 0)
        MessageBox.Show("Select device");
    else
    {
        ListViewItem Item = lvDevices.SelectedItems[0];
        Int32 Res = ((wclBluetoothRadio)Item.Tag).RemoteUnpair(Convert.ToInt64(Item.Text, 16),
            wclBluetoothPairingMethod.pmLe);
        if (Res != wclErrors.WCL_E_SUCCESS)
            MessageBox.Show("Error: 0x" + Res.ToString("X8"));
    }
}
Private Sub btUnpair_Click(sender As System.Object, e As System.EventArgs) Handles btUnpair.Click
    If lvDevices.SelectedItems.Count = 0 Then
        MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    Else
        Dim Item As ListViewItem = lvDevices.SelectedItems(0)
        Dim Res As Integer = (CType(Item.Tag, wclBluetoothRadio)).RemoteUnpair(Convert.ToInt64(Item.Text, 16),
                                                                               wclBluetoothPairingMethod.pmLe)
        If Res <> wclErrors.WCL_E_SUCCESS Then
            MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End If
    End If
End Sub
void CGattClientDlg::OnBnClickedButtonUnpair()
{
  POSITION Pos = lvDevices.GetFirstSelectedItemPosition();
  if (Pos == NULL)
    AfxMessageBox(_T("Select device"));
  else
  {
    int Item = lvDevices.GetNextSelectedItem(Pos);
    int Res = ((CwclBluetoothRadio*)lvDevices.GetItemData(Item))->RemoteUnpair(StrToInt64(lvDevices.GetItemText(Item, 0)),
      pmLe);
    if (Res != WCL_E_SUCCESS)
      AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
  }
}

Enumerating all currently bonded devices is done through EnumPairedDevices. The Kind parameter filters by device type — dkBle, dkClassic, or dkAll:

procedure TfmMain.btEnumPairedClick(Sender: TObject);
var
  Res: Integer;
  Radio: TwclBluetoothRadio;
  Devices: TwclBluetoothAddresses;
  i: Integer;
  Item: TListItem;
  Name: string;
  DevType: TwclBluetoothDeviceType;
  Paired: Boolean;
begin
  Radio := GetRadio;
  if Radio <> nil then begin
    lvDevices.Items.Clear;

    Res := Radio.EnumPairedDevices(dkBle, Devices);
    if Res <> WCL_E_SUCCESS then
      ShowMessage('Enum paired failed: 0x' + IntToHex(Res, 8))

    else begin
      if Length(Devices) > 0 then begin
        for i := 0 to Length(Devices) - 1 do begin
          Item := lvDevices.Items.Add;
          Item.Data := Radio;

          Item.Caption := IntToHex(Devices[i], 12);

          Res := Radio.GetRemoteName(Devices[i], Name);
          if Res = WCL_E_SUCCESS then
            Item.SubItems.Add(Name)
          else
            Item.SubItems.Add('Error: 0x' + IntToHex(Res, 8));

          Res := Radio.GetRemoteDeviceType(Devices[i], DevType);
          if Res <> WCL_E_SUCCESS then
            Item.SubItems.Add('Error: 0x' + IntToHex(Res, 8))
          else begin
            case DevType of
              dtClassic: Item.SubItems.Add('Classic');
              dtBle: Item.SubItems.Add('BLE');
              dtMixed: Item.SubItems.Add('Mixed');
              else Item.SubItems.Add('Unknown');
            end;
          end;

          Res := Radio.GetRemotePaired(Devices[i], Paired);
          if Res <> WCL_E_SUCCESS then
            Item.SubItems.Add('Error: 0x' + IntToHex(Res, 8))
          else begin
            if Paired then
              Item.SubItems.Add('True')
            else
              Item.SubItems.Add('False');
          end;
        end;
      end;
    end;
  end;
end;
void __fastcall TfmMain::btEnumPairedClick(TObject *Sender)
{
  TwclBluetoothRadio* Radio = GetRadio();
  if (Radio != NULL)
  {
    lvDevices->Items->Clear();

    TwclBluetoothAddresses Devices;
    int Res = Radio->EnumPairedDevices(dkBle, Devices);
    if (Res != WCL_E_SUCCESS)
      ShowMessage("Enum paired failed: 0x" + IntToHex(Res, 8));
    else
    {
      if (Devices.Length > 0)
      {
        for (int i = 0; i < Devices.Length; i++)
        {
          TListItem* Item = lvDevices->Items->Add();
          Item->Data = Radio;

          Item->Caption = IntToHex(Devices[i], 12);

          String Name = "";
          Res = Radio->GetRemoteName(Devices[i], Name);
          if (Res == WCL_E_SUCCESS)
            Item->SubItems->Add(Name);
          else
            Item->SubItems->Add("Error: 0x" + IntToHex(Res, 8));

          TwclBluetoothDeviceType DevType = dtMixed;
          Res = Radio->GetRemoteDeviceType(Devices[i], DevType);
          if (Res != WCL_E_SUCCESS)
            Item->SubItems->Add("Error: 0x" + IntToHex(Res, 8));
          else
          {
            switch (DevType)
            {
              case dtClassic:
                Item->SubItems->Add("Classic");
                break;
              case dtBle:
                Item->SubItems->Add("BLE");
                break;
              case dtMixed:
                Item->SubItems->Add("Mixed");
                break;
              default:
                Item->SubItems->Add("Unknown");
                break;
            }
          }

          bool Paired = false;
          Res = Radio->GetRemotePaired(Devices[i], Paired);
          if (Res != WCL_E_SUCCESS)
            Item->SubItems->Add("Error: 0x" + IntToHex(Res, 8));
          else
          {
            if (Paired)
              Item->SubItems->Add("True");
            else
              Item->SubItems->Add("False");
          }
        }
      }
    }
  }
}
private void btEnumPaired_Click(object sender, EventArgs e)
{
    wclBluetoothRadio Radio = GetRadio();
    if (Radio != null)
    {
        lvDevices.Items.Clear();

        Int64[] Devices;
        Int32 Res = Radio.EnumPairedDevices(wclBluetoothDiscoverKind.dkBle, out Devices);
        if (Res != wclErrors.WCL_E_SUCCESS)
            MessageBox.Show("Enum paired failed: 0x" + Res.ToString("X8"));
        else
        {
            if (Devices != null && Devices.Length > 0)
            {
                foreach (Int64 Device in Devices)
                {
                    ListViewItem Item = lvDevices.Items.Add(Device.ToString("X12"));
                    Item.Tag = Radio;

                    String Name;
                    Res = Radio.GetRemoteName(Device, out Name);
                    if (Res == wclErrors.WCL_E_SUCCESS)
                        Item.SubItems.Add(Name);
                    else
                        Item.SubItems.Add("Error: 0x" + Res.ToString("X8"));

                    wclBluetoothDeviceType DevType;
                    Res = Radio.GetRemoteDeviceType(Device, out DevType);
                    if (Res != wclErrors.WCL_E_SUCCESS)
                        Item.SubItems.Add("Error: 0x" + Res.ToString("X8"));
                    else
                    {
                        switch (DevType)
                        {
                            case wclBluetoothDeviceType.dtClassic:
                                Item.SubItems.Add("Classic");
                                break;
                            case wclBluetoothDeviceType.dtBle:
                                Item.SubItems.Add("BLE");
                                break;
                            case wclBluetoothDeviceType.dtMixed:
                                Item.SubItems.Add("Mixed");
                                break;
                            default:
                                Item.SubItems.Add("Unknown");
                                break;
                        }
                    }

                    Boolean Paired;
                    Res = Radio.GetRemotePaired(Device, out Paired);
                    if (Res != wclErrors.WCL_E_SUCCESS)
                        Item.SubItems.Add("Error: 0x" + Res.ToString("X8"));
                    else
                    {
                        if (Paired)
                            Item.SubItems.Add("True");
                        else
                            Item.SubItems.Add("False");
                    }
                }
            }
        }
    }
}
Private Sub btEnumPaired_Click(sender As System.Object, e As System.EventArgs) Handles btEnumPaired.Click
    Dim Radio As wclBluetoothRadio = GetRadio()
    If Radio IsNot Nothing Then
        lvDevices.Items.Clear()

        Dim Devices() As Int64 = Nothing
        Dim Res As Integer = Radio.EnumPairedDevices(wclBluetoothDiscoverKind.dkBle, Devices)
        If Res <> wclErrors.WCL_E_SUCCESS Then
            MessageBox.Show("Enum paired failed: 0x" + Res.ToString("X8"))
        Else
            If Devices IsNot Nothing AndAlso Devices.Length > 0 Then
                For Each Device As Int64 In Devices
                    Dim Item As ListViewItem = lvDevices.Items.Add(Device.ToString("X12"))
                    Item.Tag = Radio

                    Dim Name As String = ""
                    Res = Radio.GetRemoteName(Device, Name)
                    If Res = wclErrors.WCL_E_SUCCESS Then
                        Item.SubItems.Add(Name)
                    Else
                        Item.SubItems.Add("Error: 0x" + Res.ToString("X8"))
                    End If

                    Dim DevType As wclBluetoothDeviceType = wclBluetoothDeviceType.dtMixed
                    Res = Radio.GetRemoteDeviceType(Device, DevType)
                    If Res <> wclErrors.WCL_E_SUCCESS Then
                        Item.SubItems.Add("Error: 0x" + Res.ToString("X8"))
                    Else
                        Select Case DevType
                            Case wclBluetoothDeviceType.dtClassic
                                Item.SubItems.Add("Classic")
                            Case wclBluetoothDeviceType.dtBle
                                Item.SubItems.Add("BLE")
                            Case wclBluetoothDeviceType.dtMixed
                                Item.SubItems.Add("Mixed")
                            Case Else
                                Item.SubItems.Add("Unknown")
                        End Select
                    End If

                    Dim Paired As Boolean = False
                    Res = Radio.GetRemotePaired(Device, Paired)
                    If Res <> wclErrors.WCL_E_SUCCESS Then
                        Item.SubItems.Add("Error: 0x" + Res.ToString("X8"))
                    Else
                        If Paired Then
                            Item.SubItems.Add("True")
                        Else
                            Item.SubItems.Add("False")
                        End If
                    End If
                Next
            End If
        End If
    End If
End Sub
void CGattClientDlg::OnBnClickedButtonEnumPaired()
{
  CwclBluetoothRadio* Radio = GetRadio();
  if (Radio != NULL)
  {
    lvDevices.DeleteAllItems();

    wclBluetoothAddresses Devices;
    int Res = Radio->EnumPairedDevices(dkBle, Devices);
    if (Res != WCL_E_SUCCESS)
      AfxMessageBox(_T("Enum paired failed: 0x:") + IntToHex(Res));
    else
    {
      if (Devices.size() > 0)
      {
        for (wclBluetoothAddresses::iterator Device = Devices.begin(); Device != Devices.end(); Device++)
        {
          int Item = lvDevices.GetItemCount();
          lvDevices.InsertItem(Item, IntToHex(*Device));
          lvDevices.SetItemData(Item, (DWORD_PTR)Radio);

          tstring Name;
          Res = Radio->GetRemoteName(*Device, Name);
          if (Res == WCL_E_SUCCESS)
            lvDevices.SetItemText(Item, 1, Name.c_str());
          else
            lvDevices.SetItemText(Item, 1, _T("Error: 0x") + IntToHex(Res));

          wclBluetoothDeviceType DevType = dtMixed;
          Res = Radio->GetRemoteDeviceType(*Device, DevType);
          if (Res != WCL_E_SUCCESS)
            lvDevices.SetItemText(Item, 2, _T("Error: 0x") + IntToHex(Res));
          else
          {
            switch (DevType)
            {
              case dtClassic:
                lvDevices.SetItemText(Item, 2, _T("Classic"));
                break;
              case dtBle:
                lvDevices.SetItemText(Item, 2, _T("BLE"));
                break;
              case dtMixed:
                lvDevices.SetItemText(Item, 2, _T("Mixed"));
                break;
              default:
                lvDevices.SetItemText(Item, 2, _T("Unknown"));
            }
          }

          bool Paired = false;
          Res = Radio->GetRemotePaired(*Device, Paired);
          if (Res != WCL_E_SUCCESS)
            lvDevices.SetItemText(Item, 3, _T("Error: 0x") + IntToHex(Res));
          else
          {
            if (Paired)
              lvDevices.SetItemText(Item, 3, _T("Paired"));
            else
              lvDevices.SetItemText(Item, 3, _T("Unpaired"));
          }
        }
      }
    }
  }
}

Checking whether a specific device is currently bonded is a single call. The GattClient sample uses it when populating the device list after discovery:

Res := Radio.GetRemotePaired(Address, Paired);
if Res <> WCL_E_SUCCESS then
  Item.SubItems.Add('Error: 0x' + IntToHex(Res, 8))
else begin
  if Paired then
    Item.SubItems.Add('True')
  else
    Item.SubItems.Add('False');
end;
bool Paired = false;
Res = ((TwclBluetoothRadio*)Radio)->GetRemotePaired(Address, Paired);
if (Res != WCL_E_SUCCESS)
  Item->SubItems->Add("Error: 0x" + IntToHex(Res, 8));
else
{
  if (Paired)
    Item->SubItems->Add("True");
  else
    Item->SubItems->Add("False");
}
Boolean Paired;
Res = Radio.GetRemotePaired(Address, out Paired);
if (Res != wclErrors.WCL_E_SUCCESS)
    Item.SubItems.Add("Error: 0x" + Res.ToString("X8"));
else
{
    if (Paired)
        Item.SubItems.Add("True");
    else
        Item.SubItems.Add("False");
}
Dim Paired As Boolean = False
Res = Radio.GetRemotePaired(Address, Paired)
If Res <> wclErrors.WCL_E_SUCCESS Then
    Item.SubItems.Add("Error: 0x" + Res.ToString("X8"))
Else
    If Paired Then
        Item.SubItems.Add("True")
    Else
        Item.SubItems.Add("False")
    End If
End If
bool Paired = false;
Res = Radio->GetRemotePaired(Address, Paired);
if (Res != WCL_E_SUCCESS)
  lvDevices.SetItemText(Item, 3, _T("Error: 0x") + IntToHex(Res));
else
{
  if (Paired)
    lvDevices.SetItemText(Item, 3, _T("True"));
  else
    lvDevices.SetItemText(Item, 3, _T("False"));
}

The paired status is displayed in the fourth column of the device list. It reflects the OS-level bond record — the same record that Windows stores in its device manager.


Troubleshooting

WCL_E_BLUETOOTH_LE_ALREADY_PAIRED from RemotePair. The device is already bonded. This is not an error — the bond exists and can be used directly. Applications that need to re-pair should call RemoteUnpair first.

WCL_E_BLUETOOTH_PAIRED_BY_OTHER from RemotePair. The device is bonded to a different host, or the peripheral side has a stale bond record that does not match the current host. RemoteUnpair followed by RemotePair usually resolves it.

WCL_E_BLUETOOTH_LE_AUTH_ACCESS_DENIED from a GATT operation. The link is encrypted but not authenticated, and the characteristic requires authentication. The application should request pairing with pplEncryptionAndAuthentication and try again.

WCL_E_BLUETOOTH_LE_MANUAL_PAIRING_REQUIRED. On Windows 11, some devices cannot be paired programmatically and require the user to go through the Windows Settings dialog. This is a platform limitation, not a framework limitation.

Bluno boards and other devices without a Client Configuration Descriptor. Some low-cost peripherals have incomplete GATT databases and require ForceNotifications = true on the wclGattClient instance. See DFRobot Bluno Boards for the full story.

Pairing on Windows 10 1607. This version has a bug in the Microsoft Bluetooth stack that prevents certain pairing flows. Devices that cannot be paired through the standard path may pair successfully with the BLED112 dongle, which bypasses the Microsoft stack entirely.


Frequently asked questions

What is the difference between pairing and bonding?
Pairing establishes a shared encryption key for the current connection. Bonding saves that key so future connections can skip the pairing exchange. In the Bluetooth Framework, bonded devices report GetRemotePaired = true even when disconnected.
Why does my BLE device require a PIN instead of using Just Works?
Just Works is only selected when both devices have no input and no output capability. If your device's IO capabilities are configured as anything that can display or input, the framework may choose a different method. Check OnIoCapabilityRequest — the values you return there determine the outcome.
Can pairing happen without user interaction?
Yes — Just Works performs the entire pairing without any user prompt. But it does not provide MITM protection. For devices that handle sensitive data, use pplEncryptionAndAuthentication and an IO capability that enables Passkey Entry or Numeric Comparison.
How do I force pairing to happen before any GATT operation?
Call RemotePair with pmLe on the target radio. This initiates pairing immediately, without waiting for a GATT operation to trigger it.
How do I remove a bond?
Call RemoteUnpair with the same pairing method that was used to establish the bond. On Windows, the bond is removed from the system device manager, and the peripheral side discards the shared keys.