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 â agent 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:
| Algorithm | Standard | Purpose |
|---|---|---|
| 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:
| Constant | Value | Description |
|---|---|---|
| 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 agent |
| 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 + agent).
OUTER ENVELOPE â PLAINTEXT, RELAY READS THIS TO ROUTE
{
"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
{
"cmd": "wake",
"d": { "mac": "AA:BB:CC:DD:EE:FF" },
"rid": "X7K2M9P1" # 8-char request ID for response matching
}Note
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 -> AGENT: SESSION_START
{
"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. AGENT -> CLIENT: SESSION_READY
{
"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
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 networkTip
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
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)
aad = f"1.0|{session_id_hex}|{seq}"
# Example: "1.0|a1b2c3d4e5f6a7b8|42"
# Prevents version downgrade and cross-session attacks ENCRYPT (SENDER)
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)
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 droppedReplay Protection
# 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:
| Step | Phase | Direction | Description |
|---|---|---|---|
| 1 | Auth | Client -> Server | WebSocket upgrade + auth (api_token, client_type: client) |
| 2 | Auth | ESP32 -> Server | WebSocket upgrade + auth (api_token, client_type: agent, agent_id) |
| 3 | Handshake | Client <-> agent | session_start with client_random, HMAC-SHA256(master_key, client_random) |
| 4 | Handshake | agent -> 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 -> agent | Encrypted payload with v 1.0, sid, seq, ciphertext+tag |
| 7 | WOL | agent (local) | Decrypt inner payload, send UDP magic packet to AA BB CC DD EE FF |
| 8 | ACK | agent -> Client | wake_ack with rid, status ok (encrypted) |
Note
Server only sees:
sid, seq, v â MAC address and command are always inside ciphertext