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:
cjkas 2026-10-27 19:41:43 +01:00
parent ff7517e10f
commit 43191ba312
8 changed files with 144 additions and 13 deletions

View file

@ -5,8 +5,6 @@
- [#77](https://github.com/cjkas/ESPSomfy-RTS/issues/77) Port 80 routes don't check login
- [#78](https://github.com/cjkas/ESPSomfy-RTS/issues/78) CORS allows any origin
- [#79](https://github.com/cjkas/ESPSomfy-RTS/issues/79) Websocket has no auth
- [#80](https://github.com/cjkas/ESPSomfy-RTS/issues/80) API token key is public
- [#81](https://github.com/cjkas/ESPSomfy-RTS/issues/81) `/login` has no rate limit
- [#82](https://github.com/cjkas/ESPSomfy-RTS/issues/82) A saved WiFi passphrase or MQTT password can't be cleared from the UI
- [#83](https://github.com/cjkas/ESPSomfy-RTS/issues/83) Installing "Main" from the update dialog always fails
- [#84](https://github.com/cjkas/ESPSomfy-RTS/issues/84) Transmitting a frame stops `loop()` for about 365 ms