diff --git a/wled00/const.h b/wled00/const.h index 14ec23b58..6435eec38 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -438,6 +438,11 @@ #define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented) #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) +#define ERR_NONCE 40 // Invalid nonce +#define ERR_REPLAY 41 // Replay attack detected +#define ERR_HMAC 42 // HMAC verification failed +#define ERR_HMAC_MISS 43 // HMAC missing +#define ERR_HMAC_GEN 44 // HMAC handling error // Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness diff --git a/wled00/crypto.cpp b/wled00/crypto.cpp index 002e0d53c..fa37c214b 100644 --- a/wled00/crypto.cpp +++ b/wled00/crypto.cpp @@ -3,6 +3,74 @@ #define HMAC_KEY_SIZE 32 +#define SESSION_ID_SIZE 16 +#define MAX_SESSION_IDS 8 + +void getNonce(byte* nonce) { + RNG::fill(nonce, SESSION_ID_SIZE); +} + +struct Nonce { + byte sessionId[SESSION_ID_SIZE]; + uint32_t counter; +}; + +Nonce knownSessions[MAX_SESSION_IDS] = {}; + +void moveToFirst(uint32_t i) { + if (i >= MAX_SESSION_IDS) return; + + Nonce tmp = knownSessions[i]; + for (int j = i; j > 0; j--) { + knownSessions[j] = knownSessions[j - 1]; + } + knownSessions[0] = tmp; +} + +bool verifyNonce(const byte* sid, uint32_t counter) { + for (int i = 0; i < MAX_SESSION_IDS; i++) { + if (memcmp(knownSessions[i].sessionId, sid, SESSION_ID_SIZE) == 0) { + if (counter <= knownSessions[i].counter) { + Serial.println(F("Retransmission detected!")); + return false; + } + knownSessions[i].counter = counter; + // nonce good, move this entry to the first position of knownSessions + moveToFirst(i); + return true; + } + } + Serial.println(F("Unknown session ID!")); + return false; +} + +void addSession(const char* sid) { + byte sid_new[SESSION_ID_SIZE]; + RNG::fill(sid_new, SESSION_ID_SIZE); + + // first, try to find a completely unused slot + for (int i = 0; i < MAX_SESSION_IDS; i++) { + // this is not perfect, but it is extremely unlikely that the first 32 bit of a random session ID are all zeroes + if ((uint32_t)(knownSessions[i].sessionId) == 0 && knownSessions[i].counter == 0) { + memcpy(knownSessions[i].sessionId, sid, SESSION_ID_SIZE); + moveToFirst(i); + return; + } + } + // next, find oldest slot that has counter 0 (not used before) + // but leave the two most recent slots alone + for (int i = MAX_SESSION_IDS - 1; i > 1; i--) { + if (knownSessions[i].counter == 0) { + memcpy(knownSessions[i].sessionId, sid, SESSION_ID_SIZE); + moveToFirst(i); + return; + } + } + // if all else fails, overwrite the oldest slot + memcpy(knownSessions[MAX_SESSION_IDS - 1].sessionId, sid, SESSION_ID_SIZE); + moveToFirst(MAX_SESSION_IDS - 1); +} + void printByteArray(const byte* arr, size_t len) { for (size_t i = 0; i < len; i++) { Serial.print(arr[i], HEX); @@ -34,8 +102,8 @@ void hmacSign(const byte* message, size_t msgLen, const char* pskHex, byte* sign bool hmacVerify(const byte* message, size_t msgLen, const char* pskHex, const byte* signature) { byte sigCalculated[SHA256HMAC_SIZE]; hmacSign(message, msgLen, pskHex, sigCalculated); - Serial.print(F("Calculated: ")); - printByteArray(sigCalculated, SHA256HMAC_SIZE); + //Serial.print(F("Calculated: ")); + //printByteArray(sigCalculated, SHA256HMAC_SIZE); if (memcmp(sigCalculated, signature, SHA256HMAC_SIZE) != 0) { Serial.println(F("HMAC verification failed!")); Serial.print(F("Expected: ")); @@ -49,16 +117,16 @@ bool hmacVerify(const byte* message, size_t msgLen, const char* pskHex, const by #define WLED_HMAC_TEST_PW "guessihadthekeyafterall" #define WLED_HMAC_TEST_PSK "a6f8488da62c5888d7f640276676e78da8639faf0495110b43e226b35ac37a4c" -bool verifyHmacFromJsonString0Term(byte* jsonStr, size_t len) { +uint8_t verifyHmacFromJsonString0Term(byte* jsonStr, size_t len) { // Zero-terminate the JSON string (replace the last character, usually '}', with a null terminator temporarily) - char lastChar = jsonStr[len-1]; + byte lastChar = jsonStr[len-1]; jsonStr[len-1] = '\0'; - bool result = verifyHmacFromJsonStr((const char*)jsonStr, len); + uint8_t result = verifyHmacFromJsonStr((const char*)jsonStr, len); jsonStr[len-1] = lastChar; return result; } -bool verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen) { +uint8_t verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen) { // Extract the signature from the JSON string size_t jsonLen = strlen(jsonStr); Serial.print(F("Length: ")); @@ -67,27 +135,27 @@ bool verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen) { Serial.print(F("JSON string too long!")); Serial.print(F(", max: ")); Serial.println(maxLen); - return false; + return ERR_HMAC_GEN; } Serial.print(F("Received JSON: ")); Serial.println(jsonStr); - char* macPos = strstr(jsonStr, "\"mac\":\""); + const char* macPos = strstr(jsonStr, "\"mac\":\""); if (macPos == nullptr) { Serial.println(F("No MAC found in JSON.")); - return false; + return ERR_HMAC_MISS; } - StaticJsonDocument<256> doc; - DeserializationError error = deserializeJson(doc, macPos +6); + StaticJsonDocument<128> macDoc; + DeserializationError error = deserializeJson(macDoc, macPos +6); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.c_str()); - return false; + return ERR_HMAC_GEN; } - const char* mac = doc.as(); + const char* mac = macDoc.as(); if (mac == nullptr) { Serial.println(F("Failed MAC JSON.")); - return false; + return ERR_HMAC_GEN; } Serial.print(F("Received MAC: ")); Serial.println(mac); @@ -97,7 +165,7 @@ bool verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen) { char* objStart = strchr(msgPos + 6, '{'); if (objStart == nullptr) { Serial.println(F("Couldn't find msg object start.")); - return false; + return ERR_HMAC_GEN; } size_t maxObjLen = jsonLen - (objStart - jsonStr); Serial.print(F("Max object length: ")); Serial.println(maxObjLen); @@ -118,20 +186,62 @@ bool verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen) { } if (objEnd == nullptr) { Serial.println(F("Couldn't find msg object end.")); - return false; + return ERR_HMAC_GEN; } + // get nonce (note: the nonce implementation uses "nc" for the key instead of "n" to avoid conflicts with segment names) + const char* noncePos = strstr(objStart, "\"nc\":"); + if (noncePos == nullptr || noncePos > objEnd) { + // note that it is critical to check that the nonce is within the "msg" object and thus authenticated + Serial.println(F("No nonce found in msg.")); + return ERR_HMAC_GEN; + } + // { + // StaticJsonDocument<128> nonceDoc; + // DeserializationError error = deserializeJson(nonceDoc, noncePos +5); + // if (error) { + // Serial.print(F("deser nc failed: ")); + // Serial.println(error.c_str()); + // return false; + // } + // JsonObject nonceObj = nonceDoc.as(); + // if (nonceObj.isNull()) { + // Serial.println(F("Failed nonce JSON.")); + // return false; + // } + // const char* sessionId = nonceObj["sid"]; + // if (sessionId == nullptr) { + // Serial.println(F("No session ID found in nonce.")); + // return false; + // } + // uint32_t counter = nonceObj["c"] | 0; + // if (counter == 0) { + // Serial.println(F("No counter found in nonce.")); + // return false; + // } + // if (counter > UINT32_MAX - 100) { + // Serial.println(F("Counter too large.")); + // return false; + // } + // byte sidBytes[SESSION_ID_SIZE]; + // hexStringToByteArray(sessionId, sidBytes, SESSION_ID_SIZE); + // if (!verifyNonce(sidBytes, counter)) { + // return false; + // } + // } + // Convert the MAC from hex string to byte array size_t len = strlen(mac) / 2; // This will drop the last character if the string has an odd length if (len != SHA256HMAC_SIZE) { Serial.println(F("Received MAC not expected size!")); - return false; + return ERR_HMAC_GEN; } unsigned char macByteArray[len]; hexStringToByteArray(mac, macByteArray, len); // Calculate the HMAC of the message object - return hmacVerify((const byte*)objStart, objEnd - objStart + 1, WLED_HMAC_TEST_PSK, macByteArray); + bool hmacOk = hmacVerify((const byte*)objStart, objEnd - objStart + 1, WLED_HMAC_TEST_PSK, macByteArray); + return hmacOk ? ERR_NONE : ERR_HMAC; } bool hmacTest() { @@ -152,4 +262,33 @@ bool hmacTest() { Serial.print(millis() - start); Serial.println(F("ms to verify MAC.")); return result; +} + +void printDuration(unsigned long start) { + unsigned long end = millis(); + Serial.print(F("Took ")); + Serial.print(end - start); + Serial.println(F(" ms.")); + yield(); +} + +#define HMAC_BENCH_ITERATIONS 100 + +void hmacBenchmark(const char* message) { + Serial.print(F("Starting HMAC benchmark with message length:")); + Serial.println(strlen(message)); + Serial.println(F("100 iterations signing message.")); + unsigned long start = millis(); + byte mac[SHA256HMAC_SIZE]; + for (int i = 0; i < HMAC_BENCH_ITERATIONS; i++) { + hmacSign((const byte*)message, strlen(message), WLED_HMAC_TEST_PSK, mac); + } + printDuration(start); + + Serial.println(F("100 iterations verifying message.")); + start = millis(); + for (int i = 0; i < HMAC_BENCH_ITERATIONS; i++) { + hmacVerify((const byte*)message, strlen(message), WLED_HMAC_TEST_PSK, mac); + } + printDuration(start); } \ No newline at end of file diff --git a/wled00/data/index.js b/wled00/data/index.js index 22561a2e3..19ab60b8f 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -327,6 +327,7 @@ function handleWindowMessageEvent(event) { sraOrigin = event.origin; } else if (json['wled-rc'] === 'hmac') { console.log(`Received HMAC: ${json['mac']}`); + // Pass the message containing the HMAC to the ESP requestJson(json); } } diff --git a/wled00/data/liveview.htm b/wled00/data/liveview.htm index 8c10ba962..cd53fe386 100644 --- a/wled00/data/liveview.htm +++ b/wled00/data/liveview.htm @@ -67,7 +67,7 @@ } catch (e) {} if (ws && ws.readyState === WebSocket.OPEN) { //console.info("Peek uses top WS"); - ws.send("{'lv':true}"); + ws.send('{"lv":true}'); } else { //console.info("Peek WS opening"); let l = window.location; @@ -80,7 +80,7 @@ ws = new WebSocket(url+"/ws"); ws.onopen = function () { //console.info("Peek WS open"); - ws.send("{'lv':true}"); + ws.send('{"lv":true}'); } } ws.binaryType = "arraybuffer"; diff --git a/wled00/data/liveviewws2D.htm b/wled00/data/liveviewws2D.htm index c50f40fbc..9c2d97849 100644 --- a/wled00/data/liveviewws2D.htm +++ b/wled00/data/liveviewws2D.htm @@ -31,7 +31,7 @@ ws = top.window.ws; } catch (e) {} if (ws && ws.readyState === WebSocket.OPEN) { - ws.send("{'lv':true}"); + ws.send('{"lv":true}'); } else { let l = window.location; let pathn = l.pathname; @@ -42,7 +42,7 @@ } ws = new WebSocket(url+"/ws"); ws.onopen = ()=>{ - ws.send("{'lv':true}"); + ws.send('{"lv":true}'); } } ws.binaryType = "arraybuffer"; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2b47a4d83..b979ea899 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -98,9 +98,10 @@ void setRandomColor(byte* rgb); //crypto.cpp void hmacSign(const byte* message, size_t msgLen, const char* pskHex, byte* signature); bool hmacVerify(const byte* message, size_t msgLen, const char* pskHex, const byte* signature); -bool verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen); -bool verifyHmacFromJsonString0Term(byte* jsonStr, size_t len); +uint8_t verifyHmacFromJsonStr(const char* jsonStr, uint32_t maxLen); +uint8_t verifyHmacFromJsonString0Term(byte* jsonStr, size_t len); bool hmacTest(); +void hmacBenchmark(const char* message); //dmx.cpp void initDMX(); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 64562a134..e5fc62558 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -359,6 +359,13 @@ void WLED::setup() #if !defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DEBUG_HOST) && ARDUINO_USB_CDC_ON_BOOT Serial.setDebugOutput(false); // switch off kernel messages when using USBCDC #endif + { + //hmacTest(); + //const char testMsg[] = "WLED HMAC test!!"; + //hmacBenchmark(testMsg); + //const char longMsg[] = "LoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIps"; + //hmacBenchmark(longMsg); + } DEBUG_PRINTLN(); DEBUG_PRINTF_P(PSTR("---WLED %s %u INIT---\n"), versionString, VERSION); DEBUG_PRINTLN(); @@ -555,8 +562,6 @@ void WLED::setup() #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET) WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector #endif - - hmacTest(); } void WLED::beginStrip() diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 401cd47fd..5c9d1f011 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -11,6 +11,22 @@ unsigned long wsLastLiveTime = 0; #define WS_LIVE_INTERVAL 40 +void sendWsError(AsyncWebSocketClient * client, uint8_t error) +{ + if (!ws.count()) return; + + char errorStr[16]; + strcpy_P(errorStr, PSTR("{\"error\":")); + strcpy(errorStr + 9, itoa(error, errorStr + 9, 10)); + strcat(errorStr + 10, "}"); + + if (client) { + client->text(errorStr); // ERR_NOBUF + } else { + ws.textAll(errorStr); // ERR_NOBUF + } +} + void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { if(type == WS_EVT_CONNECT){ @@ -36,36 +52,47 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp } bool verboseResponse = false; - if (!requestJSONBufferLock(11)) { - client->text(F("{\"error\":3}")); // ERR_NOBUF - return; - } Serial.print(F("WS message: ")); - Serial.println((const char*)data); - verifyHmacFromJsonString0Term(data, len); + Serial.write(data, len); + Serial.println(); - DeserializationError error = deserializeJson(*pDoc, data, len); - verifyHmacFromJsonString0Term(data, len); - JsonObject root = pDoc->as(); - if (error || root.isNull()) { - releaseJSONBufferLock(); - return; - } - if (root["v"] && root.size() == 1) { + if (len < 11 && memcmp(data, "{\"v\":true}", 10) == 0) { // if the received value is just "{"v":true}", send only to this client verboseResponse = true; - } else if (root.containsKey("lv")) { - wsLiveClientId = root["lv"] ? client->id() : 0; + Serial.println(F("Simple state query.")); + } else if (len < 13 && memcmp(data, "{\"lv\":", 6) == 0) { + wsLiveClientId = data[6] == 't' ? client->id() : 0; } else { - // if (!verifyHmacFromJsonString0Term(data, len)) { - // releaseJSONBufferLock(); - // client->text(F("{\"error\":1}")); // ERR_DENIED - // return; - // } + // check HMAC, must do before parsing JSON as that modifies "data" to store strings + uint8_t hmacVerificationResult = verifyHmacFromJsonString0Term(data, len); + if (hmacVerificationResult != ERR_NONE) { + sendWsError(client, hmacVerificationResult); + return; + } + + if (!requestJSONBufferLock(11)) { + sendWsError(client, 3); // ERR_NOBUF + return; + } + + Serial.print(F("deser input: ")); + Serial.write(data, len); + Serial.println(); + DeserializationError error = deserializeJson(*pDoc, data, len); + JsonObject root = pDoc->as(); + if (error || root.isNull()) { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(error.c_str()); + //Serial.println(F("WS JSON parse F!")); + sendWsError(client, 9); // ERR_JSON + releaseJSONBufferLock(); + return; + } verboseResponse = deserializeState(root["msg"]); + + releaseJSONBufferLock(); } - releaseJSONBufferLock(); if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon if (verboseResponse) { @@ -92,7 +119,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp if((info->index + len) == info->len){ if(info->final){ if(info->message_opcode == WS_TEXT) { - client->text(F("{\"error\":9}")); // ERR_JSON we do not handle split packets right now + sendWsError(client, 9); // ERR_JSON we do not handle split packets right now } } }