mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +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
26
src/ApiToken.h
Normal file
26
src/ApiToken.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef apitoken_h
|
||||
#define apitoken_h
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// A token is HMAC-SHA256(token key, PIN or username:password + ":" + client
|
||||
// IP) in hex. The token key is 32 random bytes the device makes on first boot
|
||||
// and keeps in NVS: it never leaves the device.
|
||||
#define API_TOKEN_KEY_SIZE 32
|
||||
|
||||
// True when given is the expected token. It takes the same time whatever
|
||||
// given holds, so the time doesn't tell how much of a guess was right. Only
|
||||
// expected's length shows, and every token has the same one.
|
||||
inline bool tokenMatches(const char *expected, const char *given) {
|
||||
if(!expected || !given || !expected[0]) return false;
|
||||
uint8_t diff = 0;
|
||||
for(size_t i = 0; expected[i]; i++) {
|
||||
diff |= (uint8_t)(expected[i] ^ *given);
|
||||
// A shorter given compares its terminator with the rest of expected.
|
||||
if(*given) given++;
|
||||
}
|
||||
// A longer given doesn't match either.
|
||||
diff |= (uint8_t)*given;
|
||||
return diff == 0;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -5,6 +5,10 @@
|
|||
#include <esp_timer.h>
|
||||
#include "esp_log.h"
|
||||
#include "mbedtls/md.h"
|
||||
#include <Preferences.h>
|
||||
#include <esp_random.h>
|
||||
#include <bootloader_random.h>
|
||||
#include "ApiToken.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "ConfigFile.h"
|
||||
#include "Utils.h"
|
||||
|
|
@ -59,6 +63,24 @@ static uint32_t loginKey(const IPAddress &ip) { return (uint32_t)ip; }
|
|||
// The throttle's clock, in ms, which doesn't wrap as millis() does.
|
||||
static uint64_t loginNow() { return (uint64_t)esp_timer_get_time() / 1000; }
|
||||
|
||||
// The tokens' HMAC key. It has its own NVS namespace, which no settings
|
||||
// save clears and no backup holds, so only erasing NVS replaces it.
|
||||
static uint8_t tokenKey[API_TOKEN_KEY_SIZE];
|
||||
// Reads the token key, or makes one on first boot. Runs before the radio is
|
||||
// on, so the hardware RNG gets its entropy from the bootloader's source.
|
||||
static void loadTokenKey() {
|
||||
Preferences pref;
|
||||
pref.begin("TOKEN");
|
||||
if(pref.getBytes("key", tokenKey, sizeof(tokenKey)) != sizeof(tokenKey)) {
|
||||
bootloader_random_enable();
|
||||
esp_fill_random(tokenKey, sizeof(tokenKey));
|
||||
bootloader_random_disable();
|
||||
if(pref.putBytes("key", tokenKey, sizeof(tokenKey)) == sizeof(tokenKey)) ESP_LOGI(TAG, "Made a new token key");
|
||||
else ESP_LOGE(TAG, "Couldn't store the new token key: every token changes at the next boot");
|
||||
}
|
||||
pref.end();
|
||||
}
|
||||
|
||||
// Hands an item to loop(), which owns the shades. When it can't be the
|
||||
// request is answered with the reason and false is returned.
|
||||
bool queueLoopItem(AsyncWebServerRequest *request, const loop_item_t &item) {
|
||||
|
|
@ -101,7 +123,7 @@ bool Web::isAuthenticated(AsyncWebServerRequest *request, bool cfg) {
|
|||
char token[65];
|
||||
memset(token, 0x00, sizeof(token));
|
||||
this->createAPIToken(request->client()->remoteIP(), token);
|
||||
if(String(token) != request->getHeader("apikey")->value()) {
|
||||
if(!tokenMatches(token, request->getHeader("apikey")->value().c_str())) {
|
||||
request->send(401, _encoding_text, "Unauthorized API Key");
|
||||
return false;
|
||||
}
|
||||
|
|
@ -124,10 +146,12 @@ bool Web::createAPIToken(const char *payload, char *token) {
|
|||
byte hmacResult[32];
|
||||
mbedtls_md_context_t ctx;
|
||||
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
|
||||
mbedtls_md_init(&ctx);
|
||||
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
|
||||
mbedtls_md_hmac_starts(&ctx, (const unsigned char *)settings.serverId, strlen(settings.serverId));
|
||||
mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload, strlen(payload));
|
||||
mbedtls_md_hmac_starts(&ctx, tokenKey, sizeof(tokenKey));
|
||||
mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload, strlen(payload));
|
||||
mbedtls_md_hmac_finish(&ctx, hmacResult);
|
||||
mbedtls_md_free(&ctx);
|
||||
token[0] = '\0';
|
||||
for(int i = 0; i < sizeof(hmacResult); i++){
|
||||
char str[3];
|
||||
|
|
@ -378,13 +402,13 @@ void Web::handleLogin(AsyncWebServerRequest *request, JsonVariant &json) {
|
|||
char ptok[65];
|
||||
memset(ptok, 0x00, sizeof(ptok));
|
||||
this->createAPIPinToken(request->client()->remoteIP(), pin, ptok);
|
||||
if(String(ptok) == String(token)) success = true;
|
||||
success = tokenMatches(token, ptok);
|
||||
}
|
||||
else if(settings.Security.type == security_types::Password) {
|
||||
char ptok[65];
|
||||
memset(ptok, 0x00, sizeof(ptok));
|
||||
this->createAPIPasswordToken(request->client()->remoteIP(), username, password, ptok);
|
||||
if(String(ptok) == String(token)) success = true;
|
||||
success = tokenMatches(token, ptok);
|
||||
}
|
||||
if(success) {
|
||||
loginThrottle.succeeded(key);
|
||||
|
|
@ -765,6 +789,7 @@ static void timeRequest(AsyncWebServerRequest *request, ArMiddlewareNext next, c
|
|||
|
||||
void Web::begin() {
|
||||
ESP_LOGI(TAG, "Creating Web MicroServices...");
|
||||
loadTokenKey();
|
||||
asyncApiServer.addMiddleware([](AsyncWebServerRequest *r, ArMiddlewareNext next) { timeRequest(r, next, "api"); });
|
||||
asyncServer.addMiddleware([](AsyncWebServerRequest *r, ArMiddlewareNext next) { timeRequest(r, next, "web"); });
|
||||
// Async API Server (port 8081)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include "Utils.h"
|
||||
#include "SocketEmitter.h"
|
||||
#include "../entryPoints/Web.h"
|
||||
#include "ApiToken.h"
|
||||
#if HAS_ETHERNET
|
||||
#include <ETH.h>
|
||||
#endif
|
||||
|
|
@ -264,7 +265,7 @@ static bool authorized(AsyncWebServerRequest *request) {
|
|||
if(!key || !request->client()) return false;
|
||||
char token[65];
|
||||
webServer.createAPIToken(request->client()->remoteIP(), token);
|
||||
return key->value() == token;
|
||||
return tokenMatches(token, key->value().c_str());
|
||||
}
|
||||
static void onConnect(AsyncWebSocketClient *client, AsyncWebServerRequest *request) {
|
||||
const uint32_t id = client->id();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue