19 KiB
BLE for ESP32 Arduino Core
A comprehensive reference for ESP32 Bluetooth Low Energy (BLE) pairing and security implementation using the ESP32 Arduino BLE library.
Overview
This guide provides ESP32 developers with comprehensive information about BLE security implementation using the ESP32 Arduino BLE library. It covers both Bluedroid (ESP32) and NimBLE (other SoCs) implementations with realistic scenarios and troubleshooting guidance.
Issues and questions should be raised here: https://github.com/espressif/arduino-esp32/issues
(please don't use https://github.com/nkolban/esp32-snippets/issues or https://github.com/h2zero/NimBLE-Arduino/issues)
Security
Quick Start
- Choose your ESP32's IO capabilities using
ESP_IO_CAP_*constants - Configure authentication requirements with properties or permissions
- Set up security using
BLESecurityclass methods - Handle stack differences between Bluedroid (ESP32) and NimBLE (other SoCs)
- Test with NVS clearing during development
Understanding BLE Pairing
Pairing vs Bonding
- Pairing: The process of establishing encryption keys between devices
- Bonding: Storing those keys for future reconnections (persistent pairing)
Pairing Types
- Legacy Pairing: Original BLE pairing (Bluetooth 4.0/4.1)
- Secure Connections: Enhanced security (Bluetooth 4.2+) using FIPS-approved algorithms
Security Levels
- Just Works: Encryption without user verification (vulnerable to passive eavesdropping)
- MITM Protected: User verification prevents man-in-the-middle attacks
IO Capabilities Explained
The ESP32 BLE library defines the following IO capabilities:
| Capability | Library Constant | Can Display | Can Input | Can Confirm | Example Devices |
|---|---|---|---|---|---|
| No Input No Output | ESP_IO_CAP_NONE |
❌ | ❌ | ❌ | Sensor nodes, beacons, simple actuators |
| Display Only | ESP_IO_CAP_OUT |
✅ | ❌ | ❌ | E-ink displays, LED matrix displays |
| Keyboard Only | ESP_IO_CAP_IN |
❌ | ✅ | ❌ | Button-only devices, rotary encoders |
| Display Yes/No | ESP_IO_CAP_IO |
✅ | ❌ | ✅ | Devices with display + confirmation button |
| Keyboard Display | ESP_IO_CAP_KBDISP |
✅ | ✅ | ✅ | Full-featured ESP32 devices with UI |
Pairing Methods Explained
🔓 Just Works
- Security: Encryption only (no MITM protection)
- User Experience: Automatic, no user interaction
- Use Case: Convenience over security (fitness trackers, mice)
- Vulnerability: Susceptible to passive eavesdropping during pairing
🔐 Passkey Entry
- Security: Full MITM protection
- User Experience: One device shows 6-digit code, other inputs it
- Use Case: Keyboards pairing to computers
- Process:
- Display device shows random 6-digit number (000000-999999)
- Input device user types the number
- Pairing succeeds if numbers match
🔐 Numeric Comparison (Secure Connections Only)
- Security: Full MITM protection
- User Experience: Both devices show same number, user confirms match
- Use Case: Two smartphones/tablets pairing
- Process:
- Both devices display identical 6-digit number
- User verifies numbers match on both screens
- User confirms "Yes" on both devices
🔐 Out-of-Band (OOB) (Not supported by this library)
- Security: Highest security level
- User Experience: Uses external channel (NFC, QR code)
- Use Case: High-security applications
- Priority: Always used when OOB data is available
Pairing Methods Compatibility Matrix
Here is the compatibility matrix for the pairing methods depending on the IO capabilities of the devices. Note that the initiator is the device that starts the pairing process (usually the client) and the responder is the device that accepts the pairing request (usually the server).
Bluedroid vs NimBLE
Bluedroid and NimBLE are two different Bluetooth stack implementations.
Bluedroid
Bluedroid is the default Bluetooth stack in ESP-IDF. It supports both Bluetooth Classic and Bluetooth LE. It is used by the ESP32 in the Arduino Core.
Bluedroid requires more flash and RAM than NimBLE and access permissions for characteristics and descriptors are set using a specific API through the setAccessPermissions() function.
The original source of the Bluedroid project, which is not maintained anymore, can be found here: https://github.com/nkolban/esp32-snippets
Bluedroid will be replaced by NimBLE in version 4.0.0 of the Arduino Core. Bluetooth Classic and Bluedroid will no longer be supported but can be used by using Arduino as an ESP-IDF component.
NimBLE
NimBLE is a lightweight Bluetooth stack for Bluetooth LE only. It is used by all SoCs that are not the ESP32.
NimBLE requires less flash and RAM than Bluedroid. Access permissions for characteristics are set using exclusive properties in the characteristic creation. Access permissions for descriptors are set using the setAccessPermissions() function just like in Bluedroid.
Some parts of the NimBLE implementation are based on the work of h2zero, which can be found here: https://github.com/h2zero/NimBLE-Arduino. For a more customizable and feature-rich implementation of the NimBLE stack, you can use the NimBLE-Arduino library.
Common Scenarios
Here are some common scenarios for the pairing methods depending on the IO capabilities of the devices. Check also the secure BLE examples in the ESP32 Arduino Core for more detailed usage examples.
Scenario 1: Mobile App ↔ ESP32 Sensor Node
- Devices: Mobile (
ESP_IO_CAP_IO) ↔ ESP32 Sensor (ESP_IO_CAP_NONE) - MITM: Not achievable with this IO combination (falls back to Just Works)
- Characteristic Authentication: Bonding only
- Result: Just Works with bonding for reconnection
- Use Case: Weather stations, environmental monitors
Scenario 2: ESP32 Smart Lock ↔ Mobile App
- Devices: ESP32 Lock (
ESP_IO_CAP_OUT) ↔ Mobile (ESP_IO_CAP_KBDISP) - MITM: Required for security
- Characteristic Authentication: Bonding + Secure Connection + MITM
- Result: Passkey Entry (ESP32 displays, mobile enters)
- Implementation: Static passkey or dynamic display
Scenario 3: ESP32 Configuration Device ↔ Admin Tool
- Devices: ESP32 (
ESP_IO_CAP_KBDISP) ↔ Admin Tool (ESP_IO_CAP_KBDISP) - MITM: Required for configuration security
- Characteristic Authentication: Bonding + Secure Connection + MITM
- Result:
- Legacy: Passkey Entry
- Secure Connections: Numeric Comparison
- Use Case: Industrial IoT configuration, network setup
Scenario 4: ESP32 Beacon ↔ Scanner App
- Devices: ESP32 Beacon (
ESP_IO_CAP_NONE) ↔ Scanner (ESP_IO_CAP_IO) - MITM: Not required (broadcast only)
- Characteristic Authentication: None
- Result: No pairing required
- Use Case: Asset tracking, proximity detection
Scenario 5: ESP32 Smart Home Hub ↔ Multiple Devices
- Devices: ESP32 Hub (
ESP_IO_CAP_IO) ↔ Various sensors (ESP_IO_CAP_NONE) - MITM: Not possible when any of the peers are
ESP_IO_CAP_NONE - Characteristic Authentication: Bonding only (no MITM possible with
ESP_IO_CAP_NONE) - Result: Just Works only
- Use Case: Centralized home automation controller
Implementation Guidelines
For ESP32 Device Developers
Choosing IO Capabilities
#include <BLESecurity.h>
// Conservative approach - limits pairing methods but ensures compatibility
pSecurity->setCapability(ESP_IO_CAP_NONE); // Just Works only
// Balanced approach - good UX with optional security
pSecurity->setCapability(ESP_IO_CAP_IO); // Just Works or Numeric Comparison
// Maximum security - supports all methods
pSecurity->setCapability(ESP_IO_CAP_KBDISP); // All pairing methods available
Authentication Configuration
BLESecurity *pSecurity = new BLESecurity();
// Low security applications (sensors, environmental monitoring)
pSecurity->setAuthenticationMode(ESP_LE_AUTH_NO_BOND);
// Standard security with bonding (smart home devices)
pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
// MITM protection required (access control, payments)
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_MITM | ESP_LE_AUTH_BOND);
// Maximum security with Secure Connections (critical systems)
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
// Alternative syntax (more readable for complex requirements)
pSecurity->setAuthenticationMode(true, true, true); // Bonding, MITM, Secure Connections
Static Passkey Example (from secure examples)
// Set a static passkey for consistent pairing experience
#define DEVICE_PASSKEY 123456
pSecurity->setPassKey(true, DEVICE_PASSKEY); // static=true, passkey=123456
pSecurity->setCapability(ESP_IO_CAP_KBDISP); // Required for MITM even with static passkey
Security Considerations
When to Require MITM
- Always: Payment systems, medical devices, access control
- Usually: File transfers, personal data sync, keyboards
- Optional: Fitness trackers, environmental sensors, mice
- Never: Beacons, broadcast-only devices
Legacy vs Secure Connections
- Legacy: Compatible with all BLE devices (2010+)
- Secure Connections: Better security but requires Bluetooth 4.2+ (2014+)
- Recommendation: Support both, prefer Secure Connections when available
Implementation Differences
// Basic characteristic properties (both stacks)
uint32_t properties = BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE;
// NimBLE: Add authentication properties (ignored by Bluedroid)
properties |= BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_WRITE_AUTHEN;
BLECharacteristic *pCharacteristic = pService->createCharacteristic(CHAR_UUID, properties);
// Bluedroid: Set access permissions (ignored by NimBLE)
pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM | ESP_GATT_PERM_WRITE_ENC_MITM);
// Check which stack is running
String stackType = BLEDevice::getBLEStackString();
Serial.println("Using BLE stack: " + stackType);
Known Vulnerabilities
- Just Works: Vulnerable to passive eavesdropping during initial pairing
- Legacy Pairing: Uses weaker cryptographic algorithms
- Passkey Brute Force: 6-digit passkeys have only 1M combinations
- Physical Security: Displayed passkeys can be shoulder-surfed
Troubleshooting
Common Issues
Pairing Always Uses Just Works
// ❌ Problem: Missing MITM flag
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);
// ✅ Solution: Add MITM protection
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
Static passkey not being requested / Nothing happens when trying to read secure characteristic
// ❌ Problem: Wrong IO capability for MITM
pSecurity->setCapability(ESP_IO_CAP_NONE); // Can't support MITM
// ✅ Solution: Set proper capability even for static passkey
pSecurity->setCapability(ESP_IO_CAP_KBDISP); // Required for MITM
pSecurity->setPassKey(true, 123456);
Secure Characteristic Access Fails
// ❌ Problem: Wrong security method for stack
// Bluedroid approach (won't work on NimBLE)
uint32_t properties = BLECharacteristic::PROPERTY_READ;
pCharacteristic = pService->createCharacteristic(uuid, properties);
pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM); // Ignored by NimBLE!
// ✅ Solution: Use both methods for cross-compatibility
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN; // For NimBLE
pCharacteristic = pService->createCharacteristic(uuid, properties);
pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM); // For Bluedroid
Pairing Works Once, Then Fails (NVS Cache Issue)
// ✅ Solution: Clear NVS for testing/development
Serial.println("Clearing NVS pairing data for testing...");
nvs_flash_erase();
nvs_flash_init();
Default Passkey Warning
*WARNING* Using default passkey: 123456
*WARNING* Please use a random passkey or set a different static passkey
// ✅ Solution: Change from default
#define CUSTOM_PASSKEY 567890 // Your unique passkey
pSecurity->setPassKey(true, CUSTOM_PASSKEY);
Connection drops during pairing
// ✅ Solution: Implement security callbacks for better error handling
class MySecurityCallbacks : public BLESecurityCallbacks {
void onAuthenticationComplete(esp_ble_auth_cmpl_t param) override {
if (param.success) {
Serial.println("Pairing successful!");
} else {
Serial.printf("Pairing failed, reason: %d\n", param.fail_reason);
}
}
};
BLEDevice::setSecurityCallbacks(new MySecurityCallbacks());
Cross-Platform Best Practice
// Always use both methods for maximum compatibility
uint32_t secure_properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_READ_AUTHEN | // NimBLE
BLECharacteristic::PROPERTY_WRITE_AUTHEN; // NimBLE
BLECharacteristic *pChar = pService->createCharacteristic(uuid, secure_properties);
// Bluedroid permissions (ignored by NimBLE, but doesn't hurt)
pChar->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM | ESP_GATT_PERM_WRITE_ENC_MITM);
Complete Properties and Permissions Reference
Bluedroid
Bluedroid uses properties to define the capabilities of a characteristic and permissions to define the access permissions. NimBLE will ignore the access permissions.
Supported Properties
BLECharacteristic::PROPERTY_READ // Read operation
BLECharacteristic::PROPERTY_WRITE // Write operation
BLECharacteristic::PROPERTY_WRITE_NR // Write without response
BLECharacteristic::PROPERTY_NOTIFY // Notifications
BLECharacteristic::PROPERTY_INDICATE // Indications
BLECharacteristic::PROPERTY_BROADCAST // Broadcast
Characteristic and Descriptor Access Permissions
// Basic permissions
ESP_GATT_PERM_READ // Read allowed
ESP_GATT_PERM_WRITE // Write allowed
// Encryption required
ESP_GATT_PERM_READ_ENCRYPTED // Read requires encryption
ESP_GATT_PERM_WRITE_ENCRYPTED // Write requires encryption
// Authentication required (MITM protection)
ESP_GATT_PERM_READ_ENC_MITM // Read requires encryption + MITM
ESP_GATT_PERM_WRITE_ENC_MITM // Write requires encryption + MITM
// Authorization required
ESP_GATT_PERM_READ_AUTHORIZATION // Read requires authorization callback
ESP_GATT_PERM_WRITE_AUTHORIZATION // Write requires authorization callback
NimBLE
NimBLE uses properties to define both the capabilities of a characteristic and the access permissions. Bluedroid will ignore the NimBLE exclusive properties.
Supported Properties
// Basic properties
BLECharacteristic::PROPERTY_READ // Read operation
BLECharacteristic::PROPERTY_WRITE // Write operation
BLECharacteristic::PROPERTY_WRITE_NR // Write without response
BLECharacteristic::PROPERTY_NOTIFY // Notifications
BLECharacteristic::PROPERTY_INDICATE // Indications
BLECharacteristic::PROPERTY_BROADCAST // Broadcast
// NimBLE specific properties
// Encryption required
BLECharacteristic::PROPERTY_READ_ENC // Read requires encryption
BLECharacteristic::PROPERTY_WRITE_ENC // Write requires encryption
// Authentication required (MITM protection)
BLECharacteristic::PROPERTY_READ_AUTHEN // Read requires encryption + MITM protection
BLECharacteristic::PROPERTY_WRITE_AUTHEN // Write requires encryption + MITM protection
// Authorization required
BLECharacteristic::PROPERTY_READ_AUTHOR // Read requires authorization callback
BLECharacteristic::PROPERTY_WRITE_AUTHOR // Write requires authorization callback
Descriptor Access Permissions
// Basic permissions
ESP_GATT_PERM_READ // Read allowed
ESP_GATT_PERM_WRITE // Write allowed
// Encryption required
ESP_GATT_PERM_READ_ENCRYPTED // Read requires encryption
ESP_GATT_PERM_WRITE_ENCRYPTED // Write requires encryption
// Authentication required (MITM protection)
ESP_GATT_PERM_READ_ENC_MITM // Read requires encryption + MITM
ESP_GATT_PERM_WRITE_ENC_MITM // Write requires encryption + MITM
// Authorization required
ESP_GATT_PERM_READ_AUTHORIZATION // Read requires authorization callback
ESP_GATT_PERM_WRITE_AUTHORIZATION // Write requires authorization callback
Usage Examples by Security Level
No Security (Both Stacks)
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE;
Encryption Only
// NimBLE
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_READ_ENC |
BLECharacteristic::PROPERTY_WRITE_ENC;
// Bluedroid
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE;
pChar->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
MITM Protection (Authentication)
// NimBLE
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_READ_AUTHEN |
BLECharacteristic::PROPERTY_WRITE_AUTHEN;
// Bluedroid
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE;
pChar->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM | ESP_GATT_PERM_WRITE_ENC_MITM);
Authorization Required
// NimBLE
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_READ_AUTHOR |
BLECharacteristic::PROPERTY_WRITE_AUTHOR;
// Bluedroid
uint32_t properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE;
pChar->setAccessPermissions(ESP_GATT_PERM_READ_AUTHORIZATION | ESP_GATT_PERM_WRITE_AUTHORIZATION);
Debug Tips
- Log pairing features exchanged between devices
- Monitor pairing method selected by the stack
- Check timeout values for user input methods
- Verify key distribution flags match on both sides
Standards References
- Bluetooth Core Specification v5.4: Volume 3, Part H (Security Manager)
- Bluetooth Assigned Numbers: IO Capability values
- FIPS-140-2: Cryptographic standards for Secure Connections
This guide is based on the ESP32 Arduino BLE library implementation and the official Bluetooth Core Specification. For the latest API documentation, refer to the ESP32 Arduino BLE library source code and examples.
