07 - BLE Security Modes

Author: Tony Fu
Date: 2025/4/13
Device: nRF52840 Dongle
Toolchain: nRF Connect SDK v2.8.0

Common BLE Security Threats

Here are some common threats to BLE security:

Identity Tracking (ID)

Tracking a device over time using its Bluetooth address. Prevented by using resolvable private addresses with the IRK (Identity Resolving Key) to obscure the device identity.

Passive Eavesdropping (PE)

An attacker silently listens to data over the air. Prevented by encrypting the connection.

Man-in-the-Middle (MITM)

An attacker impersonates both peers, relaying and potentially modifying messages. Prevented by authenticated pairing methods that confirm peer identity (e.g., Passkey Entry, Numeric Comparison).


BLE Security Levels

Security Level Description Protects Against
Level 1 No security: no encryption, no authentication. None
Level 2 Encrypted link using unauthenticated pairing (e.g., Just Works). PE
Level 3 Encrypted link with authenticated pairing (e.g., Passkey, OOB with Legacy). PE, basic MITM
Level 4 Encrypted link with LE Secure Connections + authentication. PE, MITM, ID (with privacy)

Notes:

  • All connections start at Level 1, then upgrade during pairing.
  • Just Works leads to Level 2 — encrypted but not authenticated.
  • Passkey Entry or OOB leads to Level 3.
  • Level 4 requires both devices to support LE Secure Connections and use authenticated pairing.
  • Privacy (ID protection) is separate from these levels but essential to combat identity tracking.

Connecting vs Pairing vs Bonding

Before diving into BLE security modes, it's important to understand three foundational terms: connecting, pairing, and bonding. These steps form the basis of how two Bluetooth devices establish trust. While connecting simply creates a communication link, pairing is the beginning of any real security—it’s when encryption keys are generated and exchanged. Bonding builds on pairing by storing that trust for future sessions.

  • Connecting
  • Establishes a temporary link between two BLE devices.
  • No security by default.

  • Pairing

  • Establishes trusted relationship for the current session.
  • Negotiates and exchanges encryption keys.
  • Enables secure data transfer (encryption, authentication).

  • Bonding

  • Saves pairing information (keys) to non-volatile memory.
  • Allows devices to reconnect securely without re-pairing.
  • Typically happens automatically after successful pairing if both devices support it.

In short: Connect → Pair → Bond (optional but persistent).


More on Pairing

The pairing process can be further divided into three distinct phases:

Phase 1: Pairing Feature Exchange

  • Initiated by the central device via a Pairing Request.
  • The peripheral responds with a Pairing Response.
  • Devices exchange their:
  • I/O capabilities (e.g., keyboard, display, none).
  • OOB and MITM flags (to determine required security).
  • Supported authentication and encryption options.
  • Whether they wish to bond or just pair temporarily.
  • Based on this information, the devices will choose an appropriate pairing method in Phase 2.

Phase 2: Key Generation and Authentication

  • Devices perform cryptographic operations to generate encryption keys.
  • Depending on the pairing mode:
  • Legacy Pairing: Generates a Short Term Key (STK) from a Temporary Key (TK).
  • LE Secure Connections: Uses Elliptic Curve Diffie-Hellman (ECDH) to generate a Long Term Key (LTK) directly and securely.
  • The actual pairing method used depends on capabilities and flags:
  • Just Works (unauthenticated)
  • Passkey Entry (user types in/display 6-digit code)
  • Out-of-Band (OOB) (e.g., via NFC)
  • Numeric Comparison (LE Secure Connections only)
  • This phase determines how secure the pairing is — especially whether it's protected against MITM attacks.

Phase 3: Key Distribution (only if bonding)

  • If bonding is agreed upon, devices exchange and store keys:
  • LTK (used to encrypt future connections)
  • IRK (Identity Resolving Key for address privacy)
  • CSRK (for signed data, optional)
  • These keys are saved to non-volatile memory so that future reconnections can skip pairing and resume secure communication immediately.

More on Phase 2: Pairing Methods

The method used to perform pairing directly affects the security level.

Method Authentication Requires User Interaction MITM Protection Available In
Just Works ❌ No Minimal (accept prompt) ❌ No Legacy & LE Secure Connections
Passkey Entry ✅ Yes Yes (input or display) ✅ Yes Legacy & LE Secure Connections
Out of Band (OOB) ✅ Yes External channel (e.g., NFC) ✅ Yes Legacy & LE Secure Connections
Numeric Comparison ✅ Yes Yes (compare 6-digit number) ✅ Yes LE Secure Connections only

Details

Just Works

  • The simplest pairing method.
  • Devices derive encryption keys without authentication.
  • Used when neither device has a display or keyboard (e.g., most wearables).
  • Susceptible to MITM attacks, as the user cannot verify peer identity.
  • Results in Security Level 2 (unauthenticated encryption).

Passkey Entry

  • A 6-digit passkey is shown on one device and entered on the other.
  • Which device displays or inputs depends on I/O capabilities.
  • Provides authenticated pairing, resistant to MITM.
  • Yields Security Level 3 (authenticated encryption).

Out of Band (OOB)

  • Key material is exchanged over an external medium (e.g., NFC, QR codes).
  • Ideal for devices with limited BLE I/O but secure secondary channels.
  • Only one device needs to support OOB for it to be used (in Secure Connections).
  • Provides MITM protection, assuming the OOB channel is trusted.

Numeric Comparison

  • Both devices display the same 6-digit number.
  • The user confirms if the numbers match (presses “Yes”).
  • Ensures that no attacker is relaying the connection.
  • Only available in LE Secure Connections.
  • Offers strong MITM protection and Security Level 4.

How Pairing Method is Selected

Pairing method selection is automatic and based on: - Devices' I/O capabilities - OOB and MITM flags exchanged in Phase 1 - Support for LE Secure Connections

If OOB is supported, it is chosen.
Otherwise, if MITM is required, Passkey Entry or Numeric Comparison is used.
If neither applies, Just Works is used by default.


In Action

In this section, we’ll define two writable characteristics:
- One that requires encryption to access
- Another that requires authentication

When you try to write to either from your phone app, the device will prompt you to pair, depending on the required security level.


1. Enable pairing support

Enable the Security Manager Protocol (SMP) in your prj.conf:

CONFIG_BT_SMP=y

2. Declare the characteristic UUIDs

Just like in previous examples, define custom UUIDs for the service and its characteristics:

#define BT_UUID_TEST_SERVICE_VAL BT_UUID_128_ENCODE(0x12345678, 0x9abc, 0xdef0, 0x1234, 0x56789abcdef0)
#define BT_UUID_TEST_ENCRYPT_VAL BT_UUID_128_ENCODE(0x12345678, 0x9abc, 0xdef0, 0x1234, 0x56789abcdef1)

#define BT_UUID_TEST_SERVICE BT_UUID_DECLARE_128(BT_UUID_TEST_SERVICE_VAL)
#define BT_UUID_TEST_ENCRYPT BT_UUID_DECLARE_128(BT_UUID_TEST_ENCRYPT_VAL)

3. Define the characteristics

We use BT_GATT_PERM_WRITE_ENCRYPT to require link encryption for writing:

BT_GATT_SERVICE_DEFINE(my_svc,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_TEST_SERVICE),

    // Encrypted write characteristic
    BT_GATT_CHARACTERISTIC(BT_UUID_TEST_ENCRYPT, BT_GATT_CHRC_WRITE,
                           BT_GATT_PERM_WRITE_ENCRYPT, NULL, encrypt_write, NULL),
);

4. Implement the write handler

Same as earlier examples (not shown here). It will be called once security is sufficient.


5. Track security level changes

Use the security_changed callback to monitor when the connection is encrypted or authenticated:

static void on_security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
{
    char peer_addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), peer_addr, sizeof(peer_addr));

    if (err == 0) {
        LOG_INF("Link secured with %s (level %u)", peer_addr, level);
    } else {
        LOG_WRN("Security setup failed with %s (level %u, err %d)", peer_addr, level, err);
    }
}

struct bt_conn_cb connection_callbacks = {
    .connected = on_connected,
    .disconnected = on_disconnected,
    .security_changed = on_security_changed,
};

6. Attempt a write

When you try to write to the encrypted characteristic, the phone will prompt you to pair.
Once encrypted, the connection will be promoted to Security Level 2.


7. Unpair before moving on

To start fresh: - On your phone, unpair the device via Bluetooth settings. - In firmware, clear bonding info with:

bt_unpair(BT_ID_DEFAULT, NULL);

8. Add an authenticated characteristic

Now define a second characteristic that requires authentication:

#define BT_UUID_TEST_AUTHEN_VAL BT_UUID_128_ENCODE(0x12345678, 0x9abc, 0xdef0, 0x1234, 0x56789abcdef2)
#define BT_UUID_TEST_AUTHEN BT_UUID_DECLARE_128(BT_UUID_TEST_AUTHEN_VAL)

BT_GATT_SERVICE_DEFINE(my_svc,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_TEST_SERVICE),

    // Encrypted write characteristic
    BT_GATT_CHARACTERISTIC(BT_UUID_TEST_ENCRYPT, BT_GATT_CHRC_WRITE,
                           BT_GATT_PERM_WRITE_ENCRYPT, NULL, encrypt_write, NULL),

    // Authenticated write characteristic
    BT_GATT_CHARACTERISTIC(BT_UUID_TEST_AUTHEN, BT_GATT_CHRC_WRITE,
                           BT_GATT_PERM_WRITE_AUTHEN, NULL, authen_write, NULL),
);

9. Display the passkey

To complete an authenticated pairing, we need to show the user a passkey. This is done using the authentication callbacks:

static void display_passkey(struct bt_conn *conn, unsigned int passkey)
{
    char peer_addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), peer_addr, sizeof(peer_addr));

    LOG_INF("Enter passkey on %s: %06u", peer_addr, passkey);
}

static void cancel_authentication(struct bt_conn *conn)
{
    char peer_addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), peer_addr, sizeof(peer_addr));

    LOG_INF("Pairing canceled by remote: %s", peer_addr);
}

static const struct bt_conn_auth_cb auth_callbacks = {
    .passkey_display = display_passkey,
    .cancel = cancel_authentication,
};

10. Register authentication handlers

Don't forget to register your callbacks during initialization:

int main(void)
{
    bt_unpair(BT_ID_DEFAULT, NULL);
    bt_conn_auth_cb_register(&auth_callbacks);
    bt_conn_cb_register(&connection_callbacks);

    bt_enable(NULL);
    // Other setup...
}