Device Provisioning

How an ESP32 running the WakeLink firmware gets its Wi-Fi credentials, its end-to-end EWSP agent token, and its per-device relay token — using the firmware's built-in AP setup portal. Your master account token never leaves your phone.

ℹ️Info
The firmware exposes two equivalent surfaces while in provisioning mode: a JSON HTTP API under /api/* consumed by the Android setup wizard (the primary path), and a browser-friendly HTML form on GET / kept as a fallback for desktop users. Both share the same NVS AP password and write to the same wakelink namespace.

Three secrets, three scopes

Provisioning binds three unrelated secrets to the device. Conflating them is the #1 source of configuration bugs.

EWSP agent token

  • End-to-end secret shared only between your client and your ESP. The relay never sees it.
  • You choose its value during provisioning — anything ≥ 32 random characters. Generate with openssl rand -hex 16.
  • EWSP session keys are HKDF-derived from this token, then used with an XChaCha20-Poly1305 AEAD on every wake packet.
  • Stored in the wakelink NVS namespace as plaintext (never transmitted to server).

Per-device relay token (wld_…)

  • Generated by the relay when you register an agent via the Android app or CLI. Format: wld_<64 hex chars>.
  • Stored as a SHA-256 hash in the relay's devices table; the ESP32 stores the plaintext in NVS.
  • Used by the firmware to authenticate its WebSocket session to the relay, routing packets to the right device.
  • Your master account token (wl_…) is never stored on the ESP32. The Android app uses it once to create the agent and receive this wld_ token, then passes it to the device.
ℹ️Info
Master account token (wl_…): This is your user-level API token, used by the Android app and CLI to call the relay REST API. It is never provisioned to or stored on the ESP32 hardware. When you create an agent via POST /api/v1/agents/, the relay returns a fresh wld_… device relay token — that is what the app then sends to the ESP32.

AP portal flow

When the firmware boots without saved credentials it starts a soft-AP and waits for you on a tiny HTML form served on its own subnet.

1ESP boots into provisioning mode

The device starts WakeLink-Setup as a soft-AP at 192.168.4.1. On first boot it generates a random 8-character AP password via esp_random() and persists it in NVS (namespace wl_prov, key ap_pass). The current firmware logs [Prov] Starting AP portal: WakeLink-Setup (password set in NVS) but does not echo the password value itself to serial.

2Retrieve the AP password from NVS

Dump the NVS partition with esptool.py read_flash and parse it with the ESP-IDF nvs_partition_gen.py tool, or temporarily add a Serial.printf("[Prov] ap_pass=%s\n", ap_pass); line in provisioning.cpp after _get_or_create_ap_pass() and reflash. Wiping the wl_prov namespace (e.g. pio run --target erase) forces a fresh password on next boot.

3Join the AP from your phone or laptop

Connect to SSID WakeLink-Setup using the password from NVS. Your client should auto-receive 192.168.4.x via DHCP.

4Open the form at http://192.168.4.1/

The firmware serves both a CSRF-protected HTML form (GET / + POST /save) and a JSON API under /api/*. The Android wizard targets the JSON API; the form is the desktop fallback.

5Submit credentials (form) or run the wizard (JSON)

Either fill in the HTML fields and press Save & Connect, or — when using the Android app — let it call POST /api/login, GET /api/scan, POST /api/wifi and finally POST /api/save-and-restart. Both paths end with the device rebooting into normal mode using the saved credentials.

JSON API (wizard path)

The provisioning JSON API is what the Android setup wizard speaks. Every endpoint accepts and returns application/json. Authentication is a two-step CSRF-token flow rooted in the NVS-stored AP password.

ℹ️Info
Authentication flow: Call POST /api/login with {"ap_password":"<8 chars>"} to receive a csrf_token and a Set-Cookie: wl_verified=1 header. Every subsequent /api/* call must include X-CSRF-Token: <token> (preferred) or the wl_verified cookie. Login is rate-limited: 5 failures in 5 minutes triggers a 60 s lockout (HTTP 429).

Endpoints

EndpointDescription
POST /api/login
Obtain CSRF token. Returns {ok,csrf_token,device_info:{chip,mac,fw_version}} + Set-Cookie: wl_verified=1.
GET /api/info
Returns {chip,mac,fw_version,provisioned,agent_id,…}.
GET /api/scan
Runs a live Wi-Fi scan. Returns {networks:[{ssid,rssi,encryption,bssid,channel,…}]}.
POST /api/wifi
Body: {ssid,password}. Tests connection with a 15 s timeout. Returns {ok,ip,rssi} or HTTP 400.
POST /api/save
Writes any subset of NVS fields: ssid, password, server_host, server_port, tls_enabled, agent_id, agent_token, api_token.
POST /api/save-and-restart
Same as /api/save plus a reboot 500 ms later.
POST /api/restart
Reboot in 500 ms.
POST /api/factory-reset
Wipes both wakelink and wl_prov NVS namespaces, then reboots.
GET /api/totp
POST /api/verify
POST /api/apply-config
Legacy aliases retained for older clients.
bash
# End-to-end wizard call sequence
curl -X POST http://192.168.4.1/api/login \
     -H 'Content-Type: application/json' \
     -d '{"ap_password":"AbCdEfGh"}'
# → {"ok":true,"csrf_token":"…","device_info":{"chip":"esp32","mac":"…","fw_version":"1.0.0"}}

CSRF=…
curl -H "X-CSRF-Token: $CSRF" http://192.168.4.1/api/scan
curl -X POST -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
     -d '{"ssid":"home","password":"…"}' http://192.168.4.1/api/wifi
curl -X POST -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
     -d '{"server_host":"wakelink-project.org","server_port":443, \
          "tls_enabled":true,"agent_id":"WL001", \
          "agent_token":"<32+ random hex>","api_token":"wld_…"}' \
     http://192.168.4.1/api/save-and-restart

HTML form fallback

The form posts application/x-www-form-urlencoded to POST /save. The CSRF token is embedded in the rendered HTML page; you do not need to fill it manually.

FieldRequiredDescription
wifi_ssid
Yes
Your Wi-Fi network name.
wifi_pass
Wi-Fi password.
server_host
Relay host, e.g. wakelink-project.org.
server_port
Defaults to 443. If empty or 0, falls back to 8080.
tls_enabled
Checkbox. Automatically forced on for port 443.
agent_id
Yes
Public device identifier, format WL<8 hex>. Assigned by the server.
agent_token
Yes
Your EWSP secret. The relay never sees this.
api_token
Yes
Your per-device relay token (wld_…). Not your master account token.
bash
# Generate a strong EWSP agent token (32 random hex chars = 128 bits of entropy)
openssl rand -hex 16
ℹ️Info
The agent_id must already exist on the relay before the firmware tries to connect. Create the agent in the dashboard or Android app first — this also generates the wld_… relay token. Copy both the WL<…> agent ID and the wld_… token into the form, then set the same agent_token EWSP secret on both the client app and the ESP32.

Rotation & re-provisioning

Reset to AP mode

Hold the firmware's reset button for 10 seconds — saved credentials in the wakelink NVS namespace are wiped. The wl_prov/ap_pass survives; to also rotate the AP password, run pio run --target erase to wipe the full NVS partition.

Rotate EWSP agent token

Reset to AP mode, re-provision with a new agent_token, and update your client app to match. The relay is not involved — both ends only need to share the same secret.

Rotate per-device relay token (wld_…)

Call POST /api/v1/agents/{id}/rotate-token from the Android app or CLI. The relay returns a new wld_… token. Reset the ESP32 to AP mode and re-provision with the new value.

Rotate master account token (wl_…)

Regenerate it in the dashboard via POST /api/v1/auth/regenerate-token and update your Android app and CLI config. The ESP32 does not need re-provisioning — it stores wld, not your master token.

Revoke an agent

Deleting an agent from the dashboard makes the relay reject WebSocket sessions for that agent_id, even if the ESP32 still holds the wld token. Local TCP wake is unaffected until you also reset the device.

Why this is safe

End-to-end encryption

Every wake payload is sealed by the EWSP layer derived from agent_token before it ever leaves your client.

Blind relay

The server only forwards opaque ciphertext between an authenticated wld_… session and the matching agent_id. It never holds the EWSP agent token and cannot decrypt payloads.

Master token isolation

Your wl_… master account token never reaches the ESP32 hardware. Even if a device is physically stolen, an attacker cannot derive your account credentials from it.

Local-only AP setup

The setup portal is reachable only from the ESP's own subnet (~10 m range). It shuts down the moment a config is saved and the device reboots.

CSRF-protected submit

A per-session token embedded in the HTML page binds the form, preventing rogue requests from other browser tabs.

Random AP password per device

Generated with the hardware TRNG (esp_random()) on first boot and stored only in NVS. Not derivable from MAC address or any observable value.

Factory reset is local-only

The post-provisioning HTTP server accepts POST /factory-reset only from the ESP's AP subnet and requires a matching agent_id.