ESP-NOW Secure Pairing Protocol

Overview

This document describes the secure pairing protocol used between the Remote Controller and Fatigue Test Unit for ESP-NOW communication. The protocol provides mutual authentication using HMAC-SHA256 challenge-response, ensuring both devices share a pre-configured secret before establishing communication.

Security Goals

  1. Mutual Authentication: Both devices prove knowledge of the shared secret
  2. Replay Attack Prevention: Random challenges prevent message replay
  3. Device Type Verification: Controllers only pair with testers, and vice versa
  4. Explicit User Action: Pairing requires manual action on both devices
  5. Persistence: Approved peers survive device reboots
  6. Backward Compatibility: Pre-configured MAC addresses work without pairing

Protocol Participants

Role Device Description
Initiator Remote Controller Starts pairing, sends challenge, verifies response
Responder Fatigue Test Unit Receives request (when in pairing mode), proves identity

Shared Secret Configuration

Both devices must be compiled with the same 16-byte pairing secret. The secret is injected at build time and never hardcoded in the source code.

Configuration Methods (Priority Order)

Method Usage Best For
--secret argument ./build_app.sh app Release --secret <hex> CI/CD pipelines
Environment variable ESPNOW_PAIRING_SECRET=<hex> ./build_app.sh Development sessions
secrets.local.yml Copy from template, add secret Local development

Quick Setup

1
2
3
4
5
6
7
8
9
10
11
# 1. Generate a secret
openssl rand -hex 16

# 2. Copy the template
cp secrets.template.yml secrets.local.yml

# 3. Edit secrets.local.yml with your secret
espnow_pairing_secret: "your_32_char_hex_secret"

# 4. Build (secret is automatically loaded)
./scripts/build_app.sh fatigue_test_espnow_unit Release

Build Behavior

Build Type Without Secret With Secret
Debug Uses placeholder (warning shown) Uses configured secret
Release Compile error with instructions Uses configured secret

The secret is parsed at compile time into the PAIRING_SECRET byte array for HMAC-SHA256 computation.


Message Types

Type Value Direction Description
PairingRequest 20 Controller β†’ Broadcast Initiate pairing
PairingResponse 21 Tester β†’ Controller Respond with HMAC proof
PairingConfirm 22 Controller β†’ Tester Confirm mutual authentication
PairingReject 23 Tester β†’ Controller Reject pairing request
Unpair 24 Either direction Remove a paired device

Message Structures

PairingRequest (Controller β†’ Broadcast)

1
2
3
4
5
6
7
struct PairingRequestPayload {
    uint8_t  requester_mac[6];       // Controller's MAC address
    uint8_t  device_type;            // DeviceType::RemoteController (1)
    uint8_t  expected_peer_type;     // DeviceType::FatigueTester (2)
    uint8_t  challenge[8];           // Random 8-byte nonce
    uint8_t  protocol_version;       // Must be 1
};

Size: 18 bytes

PairingResponse (Tester β†’ Controller)

1
2
3
4
5
6
7
struct PairingResponsePayload {
    uint8_t  responder_mac[6];       // Tester's MAC address
    uint8_t  device_type;            // DeviceType::FatigueTester (2)
    uint8_t  challenge[8];           // Counter-challenge for mutual auth
    uint8_t  hmac_response[16];      // HMAC(secret, requester_challenge)
    char     device_name[16];        // Human-readable name
};

Size: 47 bytes

PairingConfirm (Controller β†’ Tester)

1
2
3
4
5
struct PairingConfirmPayload {
    uint8_t  confirmer_mac[6];       // Controller's MAC address
    uint8_t  hmac_response[16];      // HMAC(secret, responder_challenge)
    uint8_t  success;                // 1 = success, 0 = failure
};

Size: 23 bytes

PairingReject (Tester β†’ Controller)

1
2
3
4
struct PairingRejectPayload {
    uint8_t  rejecter_mac[6];        // Tester's MAC address
    uint8_t  reason;                 // Rejection reason code
};

Rejection Reasons: | Code | Reason | |β€”β€”|——–| | 0 | Not in pairing mode | | 1 | Wrong device type | | 2 | HMAC verification failed | | 3 | Peer list full | | 4 | Protocol version mismatch |


Protocol Flow

Successful Pairing Sequence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Remote Controller β”‚                              β”‚ Fatigue Test Unit  β”‚
β”‚   (Initiator)     β”‚                              β”‚    (Responder)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                                                   β”‚
          β”‚  User: Selects "Pair Device" in UI menu           β”‚
          │◄─────────────────────────────────────────         β”‚
          β”‚                                                   β”‚
          β”‚                           User: Types "pair" in UART terminal
          β”‚                         ──────────────────────────►│
          β”‚                                                   β”‚
          β”‚              [Tester enters PAIRING MODE for 30s] β”‚
          β”‚                                                   β”‚
          β”‚  1. Generate random 8-byte challenge              β”‚
          β”‚  ───────────────────────────────────              β”‚
          β”‚                                                   β”‚
          β”‚  2. PAIRING_REQUEST (broadcast)                   β”‚
          β”‚  ─────────────────────────────────────────────────►│
          β”‚  - requester_mac = Controller MAC                 β”‚
          β”‚  - device_type = RemoteController (1)             β”‚
          β”‚  - expected_peer_type = FatigueTester (2)         β”‚
          β”‚  - challenge = [8 random bytes]                   β”‚
          β”‚  - protocol_version = 1                           β”‚
          β”‚                                                   β”‚
          β”‚                  [Tester validates request]       β”‚
          β”‚                  - In pairing mode? βœ“             β”‚
          β”‚                  - Expected type matches? βœ“       β”‚
          β”‚                  - Protocol version matches? βœ“    β”‚
          β”‚                                                   β”‚
          β”‚                  [Tester computes HMAC response]  β”‚
          β”‚                  hmac = HMAC-SHA256(secret, challenge)
          β”‚                                                   β”‚
          β”‚  3. PAIRING_RESPONSE (unicast to controller)      β”‚
          │◄─────────────────────────────────────────────────│
          β”‚  - responder_mac = Tester MAC                     β”‚
          β”‚  - device_type = FatigueTester (2)                β”‚
          β”‚  - challenge = [8 new random bytes]               β”‚
          β”‚  - hmac_response = HMAC of controller's challenge β”‚
          β”‚  - device_name = "Fatigue Tester"                 β”‚
          β”‚                                                   β”‚
          β”‚  [Controller verifies HMAC]                       β”‚
          β”‚  expected = HMAC-SHA256(secret, my_challenge)     β”‚
          β”‚  if (expected == received) β†’ Tester is authentic  β”‚
          β”‚                                                   β”‚
          β”‚  [Controller computes HMAC for tester's challenge]β”‚
          β”‚  my_hmac = HMAC-SHA256(secret, tester_challenge)  β”‚
          β”‚                                                   β”‚
          β”‚  4. PAIRING_CONFIRM (unicast to tester)           β”‚
          β”‚  ─────────────────────────────────────────────────►│
          β”‚  - confirmer_mac = Controller MAC                 β”‚
          β”‚  - hmac_response = HMAC of tester's challenge     β”‚
          β”‚  - success = 1                                    β”‚
          β”‚                                                   β”‚
          β”‚                  [Tester verifies HMAC]           β”‚
          β”‚                  expected = HMAC-SHA256(secret, my_challenge)
          β”‚                  if (expected == received) β†’ Controller authentic
          β”‚                                                   β”‚
          β”‚  [Controller adds Tester to approved list]        β”‚
          β”‚                  [Tester adds Controller to approved list]
          β”‚                                                   β”‚
          β”‚  ═══════════ PAIRING COMPLETE ═══════════         β”‚
          β”‚                                                   β”‚
          β”‚  [Both save peer lists to NVS]                    β”‚
          β”‚                                                   β”‚
          β–Ό                                                   β–Ό

Failed Pairing: Not in Pairing Mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Remote Controller β”‚                              β”‚ Fatigue Test Unit  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                                                   β”‚
          β”‚  PAIRING_REQUEST (broadcast)                      β”‚
          β”‚  ─────────────────────────────────────────────────►│
          β”‚                                                   β”‚
          β”‚                  [Tester checks: In pairing mode?]β”‚
          β”‚                  Answer: NO                       β”‚
          β”‚                                                   β”‚
          β”‚  PAIRING_REJECT                                   β”‚
          │◄─────────────────────────────────────────────────│
          β”‚  - reason = 0 (Not in pairing mode)               β”‚
          β”‚                                                   β”‚
          β”‚  [Controller shows error in UI]                   β”‚
          β–Ό                                                   β–Ό

Failed Pairing: HMAC Verification Failed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Remote Controller β”‚                              β”‚ Fatigue Test Unit  β”‚
β”‚ (Wrong Secret)    β”‚                              β”‚ (Correct Secret)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                                                   β”‚
          β”‚  PAIRING_REQUEST                                  β”‚
          β”‚  ─────────────────────────────────────────────────►│
          β”‚                                                   β”‚
          β”‚  PAIRING_RESPONSE                                 β”‚
          │◄─────────────────────────────────────────────────│
          β”‚                                                   β”‚
          β”‚  [Controller computes HMAC with wrong secret]     β”‚
          β”‚  [HMAC doesn't match response β†’ REJECT]           β”‚
          β”‚                                                   β”‚
          β”‚  [Controller does NOT send PAIRING_CONFIRM]       β”‚
          β”‚  [Pairing fails with "Unauthorized device" error] β”‚
          β”‚                                                   β”‚
          β–Ό                                                   β–Ό

HMAC Computation

HMAC is computed using HMAC-SHA256 with the shared secret as the key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ComputePairingHmac(const uint8_t* challenge, size_t challenge_len,
                        uint8_t out[16]) noexcept
{
    uint8_t full_hmac[32];  // SHA-256 output
    
    mbedtls_md_hmac(
        mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
        PAIRING_SECRET, 16,      // Key
        challenge, challenge_len, // Message
        full_hmac                 // Output
    );
    
    // Truncate to 16 bytes
    memcpy(out, full_hmac, 16);
}

Constant-Time Comparison: HMAC verification uses constant-time comparison to prevent timing attacks:

1
2
3
4
5
6
7
8
9
10
11
12
bool VerifyPairingHmac(const uint8_t* challenge, size_t len,
                       const uint8_t received[16]) noexcept
{
    uint8_t expected[16];
    ComputePairingHmac(challenge, len, expected);
    
    uint8_t diff = 0;
    for (size_t i = 0; i < 16; ++i) {
        diff |= (expected[i] ^ received[i]);
    }
    return diff == 0;  // Constant-time comparison
}

State Machines

Fatigue Test Unit (Responder)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚  IDLE   │◄────────────────────────────┐
                    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                             β”‚
                         β”‚ "pair" UART command              β”‚
                         β–Ό                                  β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”‚ PAIRING_MODE   │──────┐                   β”‚
         β”‚      β”‚ (30s timeout)  β”‚      β”‚                   β”‚
         β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚                   β”‚
         β”‚              β”‚               β”‚                   β”‚
   timeout expires      β”‚ PairingRequest received           β”‚
         β”‚              β–Ό               β”‚                   β”‚
         β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚                   β”‚
         β”‚   β”‚ AWAITING_CONFIRM β”‚       β”‚                   β”‚
         β”‚   β”‚  (5s timeout)    β”‚       β”‚                   β”‚
         β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚                   β”‚
         β”‚            β”‚                 β”‚                   β”‚
         β”‚  PairingConfirm received     β”‚                   β”‚
         β”‚            β”‚                 β”‚                   β”‚
         β”‚            β–Ό                 β”‚                   β”‚
         β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚                   β”‚
         β”‚   β”‚ VERIFY HMAC      β”‚       β”‚                   β”‚
         β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚                   β”‚
         β”‚            β”‚                 β”‚                   β”‚
         β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”         β”‚                   β”‚
         β”‚    β”‚               β”‚         β”‚                   β”‚
         β”‚  PASS            FAIL        β”‚                   β”‚
         β”‚    β”‚               β”‚         β”‚                   β”‚
         β”‚    β–Ό               β”‚         β”‚                   β”‚
         β”‚  ADD TO            β”‚         β”‚                   β”‚
         β”‚  APPROVED          β”‚         β”‚                   β”‚
         β”‚  LIST              β”‚         β”‚                   β”‚
         β”‚    β”‚               β”‚         β”‚                   β”‚
         β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Remote Controller (Initiator)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚  IDLE   │◄────────────────────────────┐
                    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                             β”‚
                         β”‚ User selects "Pair"              β”‚
                         β–Ό                                  β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
              β”‚ WAITING_FOR_RESPONSE β”‚                      β”‚
              β”‚   (10s timeout)      │──────┐               β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚               β”‚
                         β”‚                  β”‚               β”‚
           PairingResponse received    timeout expires      β”‚
                         β”‚                  β”‚               β”‚
                         β–Ό                  β”‚               β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚               β”‚
              β”‚   VERIFY HMAC        β”‚      β”‚               β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚               β”‚
                         β”‚                  β”‚               β”‚
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”          β”‚               β”‚
                 β”‚               β”‚          β”‚               β”‚
               PASS            FAIL         β”‚               β”‚
                 β”‚               β”‚          β”‚               β”‚
                 β–Ό               β–Ό          β–Ό               β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
         β”‚SEND CONFIRM β”‚   β”‚ FAILED  β”‚ β”‚ FAILED  β”‚          β”‚
         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜          β”‚
                β”‚               β”‚           β”‚               β”‚
                β–Ό               β”‚           β”‚               β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚           β”‚               β”‚
         β”‚ ADD TO LIST β”‚        β”‚           β”‚               β”‚
         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜        β”‚           β”‚               β”‚
                β”‚               β”‚           β”‚               β”‚
                β–Ό               β”‚           β”‚               β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚           β”‚               β”‚
         β”‚  COMPLETE   β”‚        β”‚           β”‚               β”‚
         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜        β”‚           β”‚               β”‚
                β”‚               β”‚           β”‚               β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

NVS Storage

Storage Format

Approved peers are stored in NVS under namespace espnow_peers:

Key Type Description
peers Blob Array of ApprovedPeer structures
peers_crc u32 CRC32 for data integrity

ApprovedPeer Structure

1
2
3
4
5
6
7
struct ApprovedPeer {
    uint8_t  mac[6];             // Peer's MAC address
    uint8_t  device_type;        // DeviceType enum value
    char     name[16];           // Human-readable name
    uint32_t paired_timestamp;   // When paired (or 0)
    bool     valid;              // Slot in use?
};

Maximum peers: 4 per device

Pre-Configured Peer

For backward compatibility, the compile-time MAC address (TEST_UNIT_MAC_ or UI_BOARD_MAC) is always trusted and does not consume an NVS slot.


Message Validation

Security Gate

Every received message (except pairing messages) passes through this validation:

1
2
3
4
5
6
7
8
9
10
bool ValidateMessageSource(const uint8_t* sender_mac, MsgType type) {
    // Pairing messages bypass validation
    if (type == PairingRequest || type == PairingResponse ||
        type == PairingConfirm || type == PairingReject) {
        return true;
    }
    
    // All other messages must be from approved peers
    return PeerStore::IsPeerApproved(security_settings, sender_mac);
}

Rejected messages are silently dropped to avoid giving attackers information about why their messages were rejected.


API Reference

Fatigue Test Unit (espnow_receiver.hpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace EspNowReceiver {
    // Enter pairing mode for specified duration (default 30s)
    void enter_pairing_mode(uint32_t timeout_sec = 30);
    
    // Exit pairing mode early
    void exit_pairing_mode();
    
    // Check if currently in pairing mode
    bool is_in_pairing_mode();
    
    // Access security settings for peer management
    SecuritySettings& get_security_settings();
    
    // Manually add/remove approved peers
    bool add_approved_peer(const uint8_t mac[6], DeviceType type, const char* name);
    bool remove_approved_peer(const uint8_t mac[6]);
    
    // Get peer count
    size_t get_approved_peer_count();
}

Remote Controller (espnow_protocol.hpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace espnow {
    // Start pairing (broadcasts discovery)
    bool StartPairing() noexcept;
    
    // Cancel pairing attempt
    void CancelPairing() noexcept;
    
    // Get current pairing state
    PairingState GetPairingState() noexcept;
    
    // Peer management
    bool IsPeerApproved(const uint8_t mac[6]) noexcept;
    bool AddApprovedPeer(const uint8_t mac[6], DeviceType type, const char* name) noexcept;
    bool RemoveApprovedPeer(const uint8_t mac[6]) noexcept;
    size_t GetApprovedPeerCount() noexcept;
    
    // Get target device MAC for sending
    bool GetTargetDeviceMac(uint8_t mac_out[6]) noexcept;
}

UART Commands (Fatigue Test Unit)

Command Description
pair Enter pairing mode for 30 seconds

Example Session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> pair
╔══════════════════════════════════════════════════════════════════════════════╗
β•‘                              PAIRING MODE                                     β•‘
╠══════════════════════════════════════════════════════════════════════════════╣
β•‘ Pairing mode enabled for 30 seconds.                                         β•‘
β•‘ Start pairing from your remote controller now.                               β•‘
╠══════════════════════════════════════════════════════════════════════════════╣
β•‘ Current approved peers:                                        1             β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

[After successful pairing]
╔══════════════════════════════════════════════════════════════════════════════╗
β•‘ PAIRING SUCCESSFUL!                                                           β•‘
β•‘ Remote controller: 9C:9E:6E:77:24:F8                                         β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Security Considerations

Threat Model

Threat Mitigation
Unauthorized device sends commands All messages validated against approved peer list
Rogue device tries to pair HMAC verification ensures shared secret knowledge
Replay attack Random 8-byte challenge in each pairing attempt
Timing attack on HMAC Constant-time comparison
Eavesdropping during pairing HMAC protects secret (only challenge/response visible)
Physical access to device Pairing mode requires explicit action

Recommendations

  1. Change the default secret before production deployment
  2. Limit pairing mode duration (30 seconds is reasonable)
  3. Review approved peer list periodically
  4. Consider enabling ESP-NOW encryption for encrypted data transmission

Troubleshooting

Pairing Fails with β€œNot in pairing mode”

Cause: Fatigue test unit is not in pairing mode when request is sent.

Solution:

  1. On the fatigue test unit, type pair in UART terminal
  2. Within 30 seconds, start pairing from the remote controller

Pairing Fails with β€œHMAC verification failed”

Cause: Devices have different PAIRING_SECRET values.

Solution: Ensure both devices are compiled with identical espnow_security.hpp files.

Device Not Responding to Commands After Pairing

Cause: Message validation is rejecting messages from the peer.

Solution:

  1. Check that pairing completed successfully on both sides
  2. Verify peer was added to approved list: get_approved_peer_count()
  3. Check NVS storage is working (no corruption)

Pairing Timeout

Cause: Response not received within 10 seconds.

Solution:

  1. Ensure devices are on same WiFi channel
  2. Check for RF interference
  3. Verify test unit is powered and in pairing mode