0036: BLE Bonding and Pairing Security (Phase 1b.3)¶
Date: 2025-11-15 Phase: Phase 1b.3 Status: Approved Type: Security
Summary (Y-Statement)¶
In the context of peer-to-peer device discovery lacking authentication, facing real security concerns (malicious battery values, command injection, peer impersonation), we decided to implement BLE bonding/pairing with LE Secure Connections + Just Works + button confirmation, and neglected passkey display, out-of-band pairing, or no pairing approaches, to achieve secure authentication preventing unauthorized connections before Phase 1c and Phase 2, accepting additional complexity (pairing state machine, timeout handling, NVS storage, test environment).
Problem Statement¶
Phase 1b.2 implements peer-to-peer discovery and connection, but lacks authentication. Any nearby BLE device advertising the Bilateral Control Service UUID can connect as a "peer", potentially: - Sending malicious battery values to influence role assignment (Phase 1c) - Injecting malicious commands during bilateral control (Phase 2) - Impersonating legitimate peer device - Causing denial of service by connecting and disconnecting repeatedly
This is a real security concern for BLE devices in public spaces (therapy offices, clinics) vulnerable to malicious connections from nearby attackers.
Context¶
Background: - Phase 1b.2 implements peer discovery but no authentication - Phase 1c (battery-based role assignment) requires trusted battery values - Phase 2 (command-and-control) requires authenticated command channel - Devices lack displays for passkey entry (button-only input) - NimBLE supports multiple pairing methods
Security Threats: - Peer Impersonation: Attacker advertises as peer device, connects - Battery Manipulation: Attacker sends false battery level to become SERVER - Command Injection: Attacker sends malicious commands (Phase 2) - Denial of Service: Repeated connect/disconnect prevents legitimate pairing
Requirements: - Secure authentication before Phase 1c and Phase 2 - No display required (button-only confirmation) - User-friendly pairing workflow - Bonding data persistence (no re-pairing after reboot) - Test mode (no NVS wear during development) - Bounded timeouts (JPL compliance)
Decision¶
Implement BLE bonding/pairing with LE Secure Connections + Just Works + button confirmation for peer connections BEFORE implementing Phase 1c (battery-based role assignment) and Phase 2 (command-and-control).
Security Architecture¶
1. BLE Pairing Method: LE Secure Connections with Just Works + Button Confirmation
NimBLE supports multiple pairing methods. We chose Just Works with button confirmation because: - ✅ No Display Required: Devices lack screens for passkey display - ✅ MITM Protection via Button: User confirms pairing by pressing button (prevents passive attackers) - ✅ LE Secure Connections: Uses ECDH (Elliptic Curve Diffie-Hellman) for key exchange - ✅ Bonding: Long-term keys stored in NVS, no re-pairing after reboot - ✅ User Experience: Simple "press button to pair" workflow
NimBLE Security Configuration:
static const struct ble_gap_security_params security_params = {
.bonding = 1, // Enable bonding (store keys in NVS)
.mitm = 1, // Require MITM protection (button confirmation)
.sc = 1, // Use LE Secure Connections (ECDH key exchange)
.keypress = 0, // No keypress notifications
.io_cap = BLE_HS_IO_KEYBOARD_DISPLAY, // Support passkey input/display
.oob = 0, // No out-of-band pairing
.min_key_size = 16, // Require maximum key strength
.max_key_size = 16,
.our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID,
.their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID,
};
2. Pairing Flow¶
Device A Boot Device B Boot
↓ ↓
[PAIRING_WAIT] [PAIRING_WAIT]
Purple LED Purple LED
GPIO15 ON GPIO15 ON
↓ ↓
Advertising + Advertising +
Scanning Scanning
↓ ↓
└──── Peer Discovery ────────┘
↓
BLE Connection
↓
┌─── Pairing Request ───┐
│ (NimBLE automatic) │
└────────────────────────┘
↓
Purple Pulsing LED Purple Pulsing LED
"Press button to pair" "Press button to pair"
↓
User presses button User presses button
(short press < 1s) (short press < 1s)
↓
┌──── Confirmation ─────┐
│ (numeric comparison) │
└────────────────────────┘
↓
Bonding Success
↓
Green 3× blink Green 3× blink
GPIO15 OFF GPIO15 OFF
↓
[MOTOR_ACTIVE] [MOTOR_ACTIVE]
Session timer starts Session timer starts
3. Bonding Data Storage¶
Production Mode (xiao_esp32c6 environment):
- Bonding keys stored in NVS partition
- Persistent across reboots (no re-pairing needed)
- NVS namespace: "ble_sec" (NimBLE default)
Test Mode (xiao_esp32c6_ble_no_nvs environment):
- Build flag: -DBLE_PAIRING_TEST_MODE=1
- Bonding data NOT written to NVS (prevents flash wear during testing)
- Forces fresh pairing every boot
- Allows unlimited pairing testing without NVS degradation
4. Timeout Handling¶
// BLE Task State Machine
case BLE_STATE_PAIRING: {
uint32_t pairing_elapsed = esp_timer_get_time()/1000 - pairing_start_time;
if (pairing_elapsed >= 30000) { // 30-second timeout (JPL bounded wait)
ESP_LOGW(TAG, "Pairing timeout (30 seconds), disconnecting peer");
ble_gap_terminate(peer_conn_handle, BLE_ERR_REM_USER_CONN_TERM);
// Send failure message to motor task
task_message_t msg = { .type = MSG_PAIRING_FAILED };
xQueueSend(ble_to_motor_queue, &msg, 0);
// LED feedback: Red 3× blink
status_led_pattern(STATUS_PATTERN_PAIRING_FAILED);
ESP_LOGI(TAG, "State: PAIRING → IDLE");
state = BLE_STATE_IDLE;
}
// Feed watchdog during wait (JPL Rule #7)
esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(100));
}
5. Status LED Feedback (GPIO15 + WS2812B Synchronized)¶
| State | GPIO15 (Discrete LED) | WS2812B (RGB LED) | Duration |
|---|---|---|---|
| Waiting for Peer | ON (solid) | Purple solid | Until peer discovered |
| Pairing in Progress | Pulsing (1 Hz) | Purple pulsing | Until user confirms or timeout |
| Pairing Success | OFF | Green 3× blink | 1.5 seconds |
| Pairing Failed | OFF | Red 3× blink | 1.5 seconds |
Implementation:
// status_led.c - Synchronize GPIO15 with WS2812B patterns
void status_led_pattern(status_pattern_t pattern) {
switch (pattern) {
case STATUS_PATTERN_PAIRING_WAIT:
gpio_set_level(GPIO_STATUS_LED, 0); // ON (active low)
set_ws2812b_color(PURPLE);
break;
case STATUS_PATTERN_PAIRING_PROGRESS:
// Pulse GPIO15 and WS2812B together (1 Hz)
start_led_pulse(PURPLE, 1000); // Handles both LEDs
break;
case STATUS_PATTERN_PAIRING_SUCCESS:
gpio_set_level(GPIO_STATUS_LED, 1); // OFF
blink_led(GREEN, 3, 250); // 3× blink, 250ms each
break;
case STATUS_PATTERN_PAIRING_FAILED:
gpio_set_level(GPIO_STATUS_LED, 1); // OFF
blink_led(RED, 3, 250);
break;
}
}
6. Motor Task Delayed Start¶
Motor task and session timer do NOT start until pairing completes:
// motor_task.c
void motor_task(void *pvParameters) {
motor_state_t state = MOTOR_STATE_PAIRING_WAIT;
while (state != MOTOR_STATE_SHUTDOWN) {
switch (state) {
case MOTOR_STATE_PAIRING_WAIT: {
// Wait for pairing completion message
task_message_t msg;
if (xQueueReceive(ble_to_motor_queue, &msg, pdMS_TO_TICKS(100)) == pdTRUE) {
if (msg.type == MSG_PAIRING_COMPLETE) {
ESP_LOGI(TAG, "Pairing complete, starting session");
// Initialize session timer NOW (not in main.c)
session_start_time_ms = esp_timer_get_time() / 1000;
ESP_LOGI(TAG, "State: PAIRING_WAIT → CHECK_MESSAGES");
state = MOTOR_STATE_CHECK_MESSAGES;
} else if (msg.type == MSG_PAIRING_FAILED) {
ESP_LOGW(TAG, "Pairing failed, retrying...");
// Stay in PAIRING_WAIT, user can trigger re-pair via button
} else if (msg.type == MSG_EMERGENCY_SHUTDOWN) {
ESP_LOGI(TAG, "State: PAIRING_WAIT → SHUTDOWN");
state = MOTOR_STATE_SHUTDOWN;
}
}
// Feed watchdog during wait
esp_task_wdt_reset();
break;
}
// ... rest of motor states ...
}
}
}
7. Button Task Pairing Confirmation¶
Short button press (< 1 second) during pairing confirms pairing:
// button_task.c
case BTN_STATE_PRESSED: {
uint32_t press_duration = esp_timer_get_time()/1000 - press_start_time;
if (gpio_get_level(GPIO_BUTTON) == 1) { // Button released
ESP_LOGI(TAG, "Button released after %u ms", press_duration);
// Check if we're in pairing mode
if (ble_is_pairing()) {
// ANY short press during pairing = confirmation
ESP_LOGI(TAG, "Pairing confirmation (button press)");
ble_sm_inject_io(peer_conn_handle, BLE_SM_IOACT_NUMCMP, 1); // Confirm
ESP_LOGI(TAG, "State: PRESSED → IDLE");
state = BTN_STATE_IDLE;
}
else if (press_duration < 1000) {
// Normal mode change (existing code)
// ...
}
// ... rest of button logic ...
}
}
Consequences¶
Benefits¶
- ✅ Security: Prevents unauthorized peer connections and command injection
- ✅ User Experience: Simple "press button to pair" workflow, no configuration needed
- ✅ Testing: Separate test environment prevents NVS wear during development
- ✅ JPL Compliance: Bounded timeouts, watchdog feeding, defensive logging
- ✅ Foundation: Establishes authentication for Phase 1c and Phase 2
- ✅ MITM Protection: Button confirmation prevents passive eavesdropping attacks
- ✅ Encryption: LE Secure Connections uses ECDH for secure key exchange
- ✅ Persistence: Bonding data stored in NVS (no re-pairing after reboot)
Drawbacks¶
- Additional pairing step adds session start latency (30 seconds max)
- User must press button on both devices (two-handed operation during pairing)
- NVS storage required for bonding data (flash wear over device lifetime)
- Test mode requires separate build environment
- Pairing timeout may be too short for users with motor impairments (consider extending)
Options Considered¶
Option A: Passkey Display¶
Pros: - Strongest MITM protection - Standard BLE pairing method
Cons: - Requires display (not available on device) - Poor accessibility (vision impairment)
Selected: NO Rationale: Hardware constraint (no display)
Option B: Out-of-Band (NFC)¶
Pros: - Very secure pairing - Good user experience (tap to pair)
Cons: - Requires NFC hardware (not available) - Additional cost and PCB complexity
Selected: NO Rationale: Hardware constraint (no NFC)
Option C: Just Works (no button confirmation)¶
Pros: - Simple UX (automatic pairing) - No user action required
Cons: - No MITM protection (vulnerable to active attacks) - Passive eavesdropping possible
Selected: NO Rationale: Insufficient security for therapeutic device
Option D: Just Works + Button Confirmation (Selected)¶
Pros: - Good security (MITM protection via button) - Simple UX (press button to pair) - No display required - Standard BLE practice for button-only devices
Cons: - Requires user action (acceptable tradeoff) - Two-handed operation during pairing
Selected: YES Rationale: Best balance of security and UX for button-only device
Option E: No Pairing¶
Pros: - Simplest implementation - Fastest session start
Cons: - Completely insecure (unacceptable for Phase 2) - No protection against impersonation or command injection
Selected: NO Rationale: Unacceptable security risk for therapeutic device
Related Decisions¶
Related¶
- AD028: Command-and-Control Architecture - Requires authenticated command channel (Phase 2)
- AD035: Battery-Based Initial Role Assignment - Requires trusted battery values (Phase 1c)
- AD030: Bilateral Control Service - Defines peer communication GATT service
Implementation Notes¶
Code References¶
Modified Files:
1. src/motor_task.h - Add MSG_PAIRING_COMPLETE, MSG_PAIRING_FAILED, MOTOR_STATE_PAIRING_WAIT
2. src/motor_task.c - Implement pairing wait state, delay session timer
3. src/ble_task.h - Add BLE_STATE_PAIRING
4. src/ble_task.c - Add pairing timeout handling
5. src/ble_manager.c - Add NimBLE security callbacks, bonding config
6. src/button_task.c - Add pairing confirmation handler
7. src/status_led.c/h - Add pairing LED patterns with GPIO15 sync
8. src/main.c - Remove session timer init (moved to motor task)
9. platformio.ini - Add xiao_esp32c6_ble_no_nvs environment
Build Environment¶
Production Mode:
- Environment Name: xiao_esp32c6
- Configuration File: sdkconfig.xiao_esp32c6
- NVS Partition: Bonding data stored persistently
Test Mode:
- Environment Name: xiao_esp32c6_ble_no_nvs
- Configuration File: sdkconfig.xiao_esp32c6_ble_no_nvs
- Build Flag: -DBLE_PAIRING_TEST_MODE=1
- NVS: Bonding data NOT written (prevents flash wear)
Testing & Verification¶
1. Production Mode Testing: - Pair two devices - Verify bonding persists after reboot - Verify automatic reconnection without re-pairing - Test timeout handling (wait 30 seconds without button press)
2. Test Mode Testing: - Flash both devices with test environment - Verify fresh pairing required every boot - Test rapid pairing cycles (20+ iterations) - Verify no NVS errors from repeated pairing
3. Security Testing: - Attempt connection from unpaired third device (should fail) - Verify bonding data cleared on factory reset - Test pairing rejection (don't press button within 30s)
Attack Mitigation¶
| Attack Vector | Mitigation |
|---|---|
| Peer Impersonation | Bonding required - only previously paired devices can reconnect |
| Malicious Battery Values | Authenticated connection required before battery exchange |
| Command Injection (Phase 2) | All commands authenticated via bonded connection |
| Denial of Service | Pairing timeout (30s) prevents indefinite blocking |
| Passive Eavesdropping | LE Secure Connections uses ECDH encryption |
| Man-in-the-Middle | Button confirmation required (MITM protection enabled) |
JPL Power of Ten Compliance¶
- ✅ Rule #2 (Fixed loop bounds): Pairing timeout enforced (30 seconds max)
- ✅ Rule #6 (No unbounded waits): All pairing waits bounded by timeout
- ✅ Rule #7 (Watchdog compliance): Watchdog fed during pairing wait states
- ✅ Rule #8 (Defensive logging): All pairing events logged (success/failure/timeout)
Phase Dependencies¶
Phase 1b.1: Peer Discovery ✅ COMPLETE
Phase 1b.2: Bug Fixes (#7-#17) ✅ COMPLETE
Phase 1b.3: BLE Bonding/Pairing ⏳ IN PROGRESS ← We are here
Phase 1c: Battery-Based Role Assignment (depends on 1b.3 for security)
Phase 2: Command-and-Control (depends on 1b.3 for authentication)
Migration Notes¶
Migrated from docs/architecture_decisions.md on 2025-11-21
Original location: AD036 (lines 3666-3981)
Git commit: TBD (migration commit)
Template Version: MADR 4.0.0 (Customized for EMDR Pulser Project) Last Updated: 2025-11-21