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
Mutual Authentication: Both devices prove knowledge of the shared secret
Replay Attack Prevention: Random challenges prevent message replay
Device Type Verification: Controllers only pair with testers, and vice versa
Explicit User Action: Pairing requires manual action on both devices
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 templatecp 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
structPairingRequestPayload{uint8_trequester_mac[6];// Controller's MAC addressuint8_tdevice_type;// DeviceType::RemoteController (1)uint8_texpected_peer_type;// DeviceType::FatigueTester (2)uint8_tchallenge[8];// Random 8-byte nonceuint8_tprotocol_version;// Must be 1};
Size: 18 bytes
PairingResponse (Tester β Controller)
1
2
3
4
5
6
7
structPairingResponsePayload{uint8_tresponder_mac[6];// Tester's MAC addressuint8_tdevice_type;// DeviceType::FatigueTester (2)uint8_tchallenge[8];// Counter-challenge for mutual authuint8_thmac_response[16];// HMAC(secret, requester_challenge)chardevice_name[16];// Human-readable name};
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
structApprovedPeer{uint8_tmac[6];// Peer's MAC addressuint8_tdevice_type;// DeviceType enum valuecharname[16];// Human-readable nameuint32_tpaired_timestamp;// When paired (or 0)boolvalid;// 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
boolValidateMessageSource(constuint8_t*sender_mac,MsgTypetype){// Pairing messages bypass validationif(type==PairingRequest||type==PairingResponse||type==PairingConfirm||type==PairingReject){returntrue;}// All other messages must be from approved peersreturnPeerStore::IsPeerApproved(security_settings,sender_mac);}
Rejected messages are silently dropped to avoid giving attackers information about why their messages were rejected.
namespaceEspNowReceiver{// Enter pairing mode for specified duration (default 30s)voidenter_pairing_mode(uint32_ttimeout_sec=30);// Exit pairing mode earlyvoidexit_pairing_mode();// Check if currently in pairing modeboolis_in_pairing_mode();// Access security settings for peer managementSecuritySettings&get_security_settings();// Manually add/remove approved peersbooladd_approved_peer(constuint8_tmac[6],DeviceTypetype,constchar*name);boolremove_approved_peer(constuint8_tmac[6]);// Get peer countsize_tget_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
namespaceespnow{// Start pairing (broadcasts discovery)boolStartPairing()noexcept;// Cancel pairing attemptvoidCancelPairing()noexcept;// Get current pairing statePairingStateGetPairingState()noexcept;// Peer managementboolIsPeerApproved(constuint8_tmac[6])noexcept;boolAddApprovedPeer(constuint8_tmac[6],DeviceTypetype,constchar*name)noexcept;boolRemoveApprovedPeer(constuint8_tmac[6])noexcept;size_tGetApprovedPeerCount()noexcept;// Get target device MAC for sendingboolGetTargetDeviceMac(uint8_tmac_out[6])noexcept;}