mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
feat: sign tokens with a secret key, compare them in constant time
Tokens were HMAC(serverId, credentials + client IP). serverId is public (mDNS, MQTT, /loginContext), so anyone who saw a token could test PIN guesses offline. The key is now 32 random bytes made on first boot in their own NVS namespace, which no settings save clears and no backup holds. It is made before WiFi starts, from the bootloader's entropy source. Tokens are compared with tokenMatches, which takes the same time whatever the guess, on the apikey header, the socket upgrade and /login. createAPIToken never freed its mbedtls context, so every token it made leaked: 563 bytes per login on the device, until it ran out of memory after about 150 logins. It now frees it: 0 bytes over 500 logins. Every token made before the upgrade is refused once. The web UI asks for login again, and Home Assistant 3.0 logs in again by itself. Closes #80 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff7517e10f
commit
43191ba312
8 changed files with 144 additions and 13 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Plan: authentication on every entry point, one HTTP server
|
||||
|
||||
**Status:** agreed, nothing built yet.
|
||||
**Status:** steps 1 to 3, PR B and PR C built; step 4 next.
|
||||
**Written:** 2026-10-18, on top of PR #64 (stored secrets are write-only). **Revised:** 2026-10-22 for socket protocol 2: two security modes, eleven HTTP routes, the socket on port 80 ([ADR 0005](adr/0005-one-websocket-protocol.md), [ADR 0006](adr/0006-two-security-modes-eleven-http-routes.md)). Terms are defined in [GLOSSARY.md](../GLOSSARY.md).
|
||||
|
||||
## Why
|
||||
|
|
@ -50,8 +50,9 @@
|
|||
|
||||
### Token key (PR B)
|
||||
- The token keeps its shape, `HMAC(key, PIN-or-password + client IP)`: stateless, and it survives reboots. Only the key changes.
|
||||
- **Key:** 32 random bytes from `esp_fill_random`, generated on first boot in their own NVS namespace. Never sent anywhere, never in a backup, replaced only when NVS is erased. There is no rotate button: changing the PIN or password already changes every token.
|
||||
- **Comparison:** constant-time, used for HTTP and the websocket handshake alike.
|
||||
- **Key:** 32 random bytes from `esp_fill_random`, generated on first boot in their own NVS namespace (`TOKEN`). Never sent anywhere, never in a backup, replaced only when NVS is erased. There is no rotate button: changing the PIN or password already changes every token.
|
||||
- The key is made in `Web::begin`, before WiFi starts, so the RNG takes its entropy from the bootloader's source (`bootloader_random_enable`) rather than the radio.
|
||||
- **Comparison:** constant-time (`tokenMatches` in [src/ApiToken.h](../src/ApiToken.h)), used for HTTP, the websocket handshake and `/login` alike.
|
||||
- **Migration:** none needed. The browser logs in again when its token is refused, and HA logs in again at runtime.
|
||||
|
||||
### Login throttle (PR C)
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
- Today HA logs in only during the config flow, on a temporary API object, and sends `apikey: ""` at runtime. It only works with security None or Secure Config Only.
|
||||
- **Setup:** the config flow reads the device info and security mode from `/loginContext`, then logs in with `/login`.
|
||||
- **Login:** at `async_setup_entry` it logs in with the PIN or password stored in `entry.data`. On a refusal it logs in again once and retries; if that fails, it raises HA's reauth flow.
|
||||
- **Socket:** it connects to `ws://host/ws?apikey=<token>` and speaks protocol 2. Against firmware that sends no `hello`, it falls back to the old HTTP API on 8081 and events on 8080.
|
||||
- **Socket:** it connects to `ws://host/ws?apikey=<token>` and speaks protocol 2. Against firmware whose `/ws` answers 404 or serves a web page, it falls back to the old HTTP API on 8081 and events on 8080.
|
||||
|
||||
## Order
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ The rules go into small pure helpers with native tests, following [src/Secrets.h
|
|||
- the Origin/Host match
|
||||
- the login throttle's schedule
|
||||
|
||||
The e2e suite (`test/e2e`) checks, when given credentials, that a socket without a token is refused, that the HTTP routes answer 401, and that a token works. It never changes the device's security settings itself.
|
||||
The e2e suite (`test/e2e`) checks, when given credentials, that a socket without a token is refused, that the HTTP routes answer 401, that a token keyed with `serverId` is refused, and that a token works. It never changes the device's security settings itself.
|
||||
|
||||
## Not in this plan
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue