EWSP Wire Format

See how WakeLink clients and agents authenticate sessions, derive per-session keys, and exchange encrypted command packets over untrusted networks.

💡Tip

Security properties

  • XChaCha20-Poly1305 AEAD — authenticated encryption, tampering detected automatically
  • HKDF-SHA256 session key derivation — unique key per session, forward secrecy
  • HMAC-SHA256 handshake auth — device identity verified before any data flows
  • 64-bit sequence numbers with 64-packet replay-protection sliding window
  • Blind relay — server never holds session keys, never decrypts payloads
  • MAC addresses and commands are always inside ciphertext, opaque to the server

Cryptographic Primitives

EWSP uses industry-standard cryptographic algorithms:

AlgorithmStandardPurpose
SHA-256
FIPS 180-4
Master key derivation from agent token
HMAC-SHA256
RFC 2104
Handshake authentication (session_start)
HKDF-SHA256
RFC 5869
Session key derivation from shared randomness
XChaCha20-Poly1305
RFC 8439 / extended nonce
AEAD encryption of all payloads
Poly1305
RFC 7539
16-byte authentication tag appended to each ciphertext

Protocol Constants

EWSP protocol constants and limits:

ConstantValueDescription
SESSION_ID_SIZE
8 bytes
Session identifier (transmitted as 16-char hex)
RANDOM_SIZE
16 bytes
client_random / device_random challenge length
NONCE_SIZE
12 bytes
XChaCha20-Poly1305 nonce (session_id[0:4] ‖ seq[4B BE] ‖ 0x00000000)
AEAD_TAG_SIZE
16 bytes
Poly1305 authentication tag, appended to ciphertext
MAX_SESSIONS
8
Concurrent EWSP sessions per device
MAX_PAYLOAD
500 bytes
Maximum plaintext payload size (DoS limit)
SEQ_WINDOW
64 packets
Replay protection sliding window
SESSION_TTL
3600 s
Session expires after 1 hour of inactivity

Packet Structure

💡Tip

Every EWSP packet has an unencrypted outer envelope (visible to the relay for routing) and an encrypted inner payload (opaque to everyone except client + device).

Outer envelope — plaintext, relay reads this to route
json
{
"v":   "1.0",                      // protocol version string
"sid": "a1b2c3d4e5f6a7b8",        // 8-byte session ID as 16-char hex
"seq": 42,                         // 64-bit unsigned sequence number
"p":   "base64url(ciphertext)"     // encrypted inner payload + 16-byte Poly1305 tag
}
Inner payload — encrypted, server never sees this
json
{
"cmd": "wake",
"d":   { "mac": "AA:BB:CC:DD:EE:FF" },
"rid": "X7K2M9P1"                  // 8-char request ID for response matching
}
💡Tip

The server only sees v, sid, and seq — enough to route and detect replays. The mac address is always inside the ciphertext.

Session Handshake

A 2-message handshake establishes a shared session key before any encrypted packets flow. The relay forwards these messages without reading them.

1. Client → Device: session_start
json
{
"type":          "session_start",
"client_random": "hex(16 random bytes)",                      // 32-char hex
"auth":          "hex(HMAC-SHA256(master_key, client_random))"// 64-char hex
}

// master_key = SHA-256(agent_token)
// agent_token: stored in ESP32 firmware, never transmitted
2. Device → Client: session_ready
json
{
"type":          "session_ready",
"session_id":    "hex(8 random bytes)",   // 16-char hex — used for routing
"device_random": "hex(16 random bytes)",  // 32-char hex
"expires_in":    3600
}
Key derivation — both sides, independently, never transmitted
json
master_key  = SHA-256(agent_token)          // 32 bytes

session_key = HKDF-SHA256(
ikm  = client_random || device_random,     // 32 bytes concatenated
salt = master_key,
info = b"ewsp_session",
len  = 32                                  // 32 bytes output
)
// session_key is NEVER sent over the network
💡Tip

After the handshake, both sides independently derive the same session_key. The server never sees it. Past sessions remain confidential even if future keys are compromised.

Encryption

Nonce construction — 12 bytes
json
nonce = session_id[0:4]             // first 4 bytes of session_id
    || seq.to_bytes(4, 'big')       // sequence number, big-endian 4 bytes
    || b'\x00\x00\x00\x00'       // 4 zero padding bytes
// Total: 12 bytes (XChaCha20-Poly1305 nonce size)
AAD — Additional Authenticated Data (not encrypted, but signed)
json
aad = f"1.0|{session_id_hex}|{seq}"
// Example: "1.0|a1b2c3d4e5f6a7b8|42"
// Prevents version downgrade and cross-session attacks
Encrypt (sender)
json
ciphertext = XChaCha20_Poly1305.encrypt(
key       = session_key,    // 32 bytes, HKDF-derived
nonce     = nonce,          // 12 bytes
plaintext = inner_json,     // {"cmd":"wake","d":{...},"rid":"..."}
aad       = aad
)
// Output: ciphertext || 16-byte Poly1305 tag
// Encoded as base64url → stored in packet["p"]
Decrypt (receiver)
json
inner_json = XChaCha20_Poly1305.decrypt(
key        = session_key,
nonce      = nonce,                    // reconstructed from sid + seq
ciphertext = base64url_decode(p),      // includes tag
aad        = aad
)
// Poly1305 tag verification is automatic
// If ciphertext is tampered → decryption raises AuthenticationError, packet dropped

Replay Protection

json
// Receiver maintains sliding window bitmap of last 64 sequence numbers
// For each incoming packet with seq N:
if seq <= last_accepted - 64:
  reject("too old")
if seq in bitmap:
  reject("replay detected")
if seq > last_accepted + 1000:
  reject("sequence jump too large — DoS protection")
// Otherwise: accept, mark seq in bitmap

// Because nonce includes seq, a replayed packet would reuse the same nonce.
// Poly1305 would technically verify it (same key+nonce = same tag).
// The sliding window catches this before Poly1305 is even invoked.

Complete Wake Flow

Full end-to-end wake command flow:

StepPhaseDirectionDescription
1
Auth
Client → Server
WebSocket upgrade + auth (api_token, client_type: client)
2
Auth
ESP32 → Server
WebSocket upgrade + auth (api_token, client_type: device, agent_id)
3
Handshake
Client → Device
session_start with client_random, HMAC-SHA256(master_key, client_random)
4
Handshake
Device → Client
session_ready with session_id, device_random, expires_in: 3600
5
Encrypt
Client (local)
session_key = HKDF(client_random + device_random, master_key, "ewsp_session")
6
Send
Client → Server → Device
Encrypted payload with v:1.0, sid, seq, ciphertext+tag
7
WOL
Device (local)
Decrypt inner payload, send UDP magic packet to AA:BB:CC:DD:EE:FF
8
ACK
Device → Client
wake_ack with rid, ok:true (encrypted)
💡Tip

Server only sees: sid, seq, v — MAC address and command are always inside ciphertext