#include "wled.h" #include "fcn_declare.h" #include "const.h" #ifdef ESP8266 #include "user_interface.h" // for bootloop detection #include // for SHA1 on ESP8266 #else #include "mbedtls/sha1.h" // for SHA1 on ESP32 #include "esp_efuse.h" #endif //helper to get int value at a position in string int getNumVal(const String* req, uint16_t pos) { return req->substring(pos+3).toInt(); } //helper to get int value with in/decrementing support via ~ syntax void parseNumber(const char* str, byte* val, byte minv, byte maxv) { if (str == nullptr || str[0] == '\0') return; if (str[0] == 'r') {*val = random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 bool wrap = false; if (str[0] == 'w' && strlen(str) > 1) {str++; wrap = true;} if (str[0] == '~') { int out = atoi(str +1); if (out == 0) { if (str[1] == '0') return; if (str[1] == '-') { *val = (int)(*val -1) < (int)minv ? maxv : min((int)maxv,(*val -1)); //-1, wrap around } else { *val = (int)(*val +1) > (int)maxv ? minv : max((int)minv,(*val +1)); //+1, wrap around } } else { if (wrap && *val == maxv && out > 0) out = minv; else if (wrap && *val == minv && out < 0) out = maxv; else { out += *val; if (out > maxv) out = maxv; if (out < minv) out = minv; } *val = out; } return; } else if (minv == maxv && minv == 0) { // limits "unset" i.e. both 0 byte p1 = atoi(str); const char* str2 = strchr(str,'~'); // min/max range (for preset cycle, e.g. "1~5~") if (str2) { byte p2 = atoi(++str2); // skip ~ if (p2 > 0) { while (isdigit(*(++str2))); // skip digits parseNumber(str2, val, p1, p2); return; } } } *val = atoi(str); } bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { if (elem.is()) { if (elem < 0) return false; //ignore e.g. {"ps":-1} *val = elem; return true; } else if (elem.is()) { const char* str = elem; size_t len = strnlen(str, 12); if (len == 0 || len > 10) return false; parseNumber(str, val, vmin, vmax); return true; } return false; //key does not exist } bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv) { const char *v = strstr(req, key); if (v) v += strlen(key); else return false; parseNumber(v, val, minv, maxv); return true; } //append a numeric setting to string buffer void sappend(char stype, const char* key, int val) { char ds[] = "d.Sf."; switch(stype) { case 'c': //checkbox oappend(ds); oappend(key); oappend(".checked="); oappendi(val); oappend(";"); break; case 'v': //numeric oappend(ds); oappend(key); oappend(".value="); oappendi(val); oappend(";"); break; case 'i': //selectedIndex oappend(ds); oappend(key); oappend(SET_F(".selectedIndex=")); oappendi(val); oappend(";"); break; } } //append a string setting to buffer void sappends(char stype, const char* key, char* val) { switch(stype) { case 's': {//string (we can interpret val as char*) String buf = val; //convert "%" to "%%" to make EspAsyncWebServer happy //buf.replace("%","%%"); oappend("d.Sf."); oappend(key); oappend(".value=\""); oappend(buf.c_str()); oappend("\";"); break;} case 'm': //message oappend(SET_F("d.getElementsByClassName")); oappend(key); oappend(SET_F(".innerHTML=\"")); oappend(val); oappend("\";"); break; } } bool oappendi(int i) { char s[16]; // WLEDMM max 32bit integer needs 11 chars (sign + 10) not 10 snprintf(s, 15, "%d", i); // WLEDMM return oappend(s); } static bool squeezeStrings = false; void oappendUseDeflate(bool OnOff) { squeezeStrings = OnOff; } bool oappend(const char* txt) { String str = squeezeStrings ? String(txt) : String(""); if (squeezeStrings) { // simple fixed-dictionary deflate str.replace(F("addField("), F("adF(")); str.replace(F("addDropdown("), F("adD(")); str.replace(F("addOption("), F("adO(")); str.replace(F("addInfo("), F("adI(")); } const char* finalTxt = squeezeStrings ? str.c_str() : txt; size_t len = strlen(finalTxt); if ((obuf == nullptr) || (olen + len >= SETTINGS_STACK_BUF_SIZE)) { // sanity checks if (obuf == nullptr) { USER_PRINTLN(F("oappend() error: obuf == nullptr.")); } else { USER_PRINT(F("oappend() error: buffer full. Increase SETTINGS_STACK_BUF_SIZE for ")); USER_PRINTF("%2u bytes \t\"", len /*1 + olen + len - SETTINGS_STACK_BUF_SIZE*/); USER_PRINT(finalTxt); USER_PRINTLN(F("\"")); errorFlag = ERR_LOW_AJAX_MEM; } return false; // buffer full } strcpy(obuf + olen, finalTxt); olen += len; return true; } void prepareHostname(char* hostname) { sprintf_P(hostname, "wled-%*s", 6, escapedMac.c_str() + 6); const char *pC = serverDescription; uint8_t pos = 5; // keep "wled-" while (*pC && pos < 24) { // while !null and not over length if (isalnum(*pC)) { // if the current char is alpha-numeric append it to the hostname hostname[pos] = *pC; pos++; } else if (*pC == ' ' || *pC == '_' || *pC == '-' || *pC == '+' || *pC == '!' || *pC == '?' || *pC == '*') { hostname[pos] = '-'; pos++; } // else do nothing - no leading hyphens and do not include hyphens for all other characters. pC++; } //last character must not be hyphen if (pos > 5) { while (pos > 4 && hostname[pos -1] == '-') pos--; hostname[pos] = '\0'; // terminate string (leave at least "wled") } } bool isAsterisksOnly(const char* str, byte maxLen) { for (byte i = 0; i < maxLen; i++) { if (str[i] == 0) break; if (str[i] != '*') return false; } //at this point the password contains asterisks only return (str[0] != 0); //false on empty string } //threading/network callback details: https://github.com/Aircoookie/WLED/pull/2336#discussion_r762276994 bool requestJSONBufferLock(uint8_t module) { unsigned long now = millis(); while (jsonBufferLock && millis()-now < 1100) delay(1); // wait for fraction for buffer lock if (jsonBufferLock) { USER_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by ")); USER_PRINT(jsonBufferLock); USER_PRINTLN(")"); return false; // waiting time-outed } jsonBufferLock = module ? module : 255; DEBUG_PRINT(F("JSON buffer locked. (")); DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); fileDoc = &doc; // used for applying presets (presets.cpp) doc.clear(); return true; } void releaseJSONBufferLock() { DEBUG_PRINT(F("JSON buffer released. (")); DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); fileDoc = nullptr; jsonBufferLock = 0; } // extracts effect mode (or palette) name from names serialized string // caller must provide large enough buffer for name (including SR extensions)! uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen) { if (src == JSON_mode_names || src == nullptr) { if (mode < strip.getModeCount()) { char lineBuffer[256] = { '\0' }; //strcpy_P(lineBuffer, (const char*)pgm_read_dword(&(WS2812FX::_modeData[mode]))); strncpy_P(lineBuffer, strip.getModeData(mode), sizeof(lineBuffer)/sizeof(char)-1); lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string size_t len = strlen(lineBuffer); size_t j = 0; for (; j < maxLen && j < len; j++) { if (lineBuffer[j] == '\0' || lineBuffer[j] == '@') break; dest[j] = lineBuffer[j]; } dest[j] = 0; // terminate string return strlen(dest); } else return 0; } if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) { snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode); dest[maxLen-1] = '\0'; return strlen(dest); } uint8_t qComma = 0; bool insideQuotes = false; uint8_t printedChars = 0; char singleJsonSymbol; size_t len = strlen_P(src); // Find the mode name in JSON for (size_t i = 0; i < len; i++) { singleJsonSymbol = pgm_read_byte_near(src + i); if (singleJsonSymbol == '\0') break; if (singleJsonSymbol == '@' && insideQuotes && qComma == mode) break; //stop when SR extension encountered switch (singleJsonSymbol) { case '"': insideQuotes = !insideQuotes; break; case '[': case ']': break; case ',': if (!insideQuotes) qComma++; default: if (!insideQuotes || (qComma != mode)) break; dest[printedChars++] = singleJsonSymbol; } if ((qComma > mode) || (printedChars >= maxLen)) break; } dest[printedChars] = '\0'; return strlen(dest); } // extracts effect slider data (1st group after @) uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var) { dest[0] = '\0'; // start by clearing buffer if (mode < strip.getModeCount()) { String lineBuffer = FPSTR(strip.getModeData(mode)); if (lineBuffer.length() > 0) { int16_t start = lineBuffer.indexOf('@'); int16_t stop = lineBuffer.indexOf(';', start); if (start>0 && stop>0) { String names = lineBuffer.substring(start, stop); // include @ int16_t nameBegin = 1, nameEnd, nameDefault; if (slider < 10) { for (size_t i=0; i<=slider; i++) { const char *tmpstr; dest[0] = '\0'; //clear dest buffer if (nameBegin == 0) break; // there are no more names nameEnd = names.indexOf(',', nameBegin); if (i == slider) { nameDefault = names.indexOf('=', nameBegin); // find default value if (nameDefault > 0 && var && ((nameEnd>0 && nameDefault= 0) { nameEnd = names.indexOf(';', nameBegin+1); if (!isdigit(names[nameBegin+1])) nameBegin = names.indexOf('=', nameBegin+1); // look for default value if (nameEnd >= 0 && nameBegin > nameEnd) nameBegin = -1; if (nameBegin >= 0 && var) { *var = (uint8_t)atoi(names.substring(nameBegin+1).c_str()); } } } // we have slider name (including default value) in the dest buffer for (size_t i=0; i 0) && (strchr(unwantedChars, in[len-1]) != nullptr)) { in[len-1] = '\0'; // deletes last char len--; } // delete leading garbage while ((len > 0) && (strchr(unwantedChars, in[0]) != nullptr)) { (void) memmove(in, in+1, len); // shifts string left by one len--; } return(in); } // 32 bit hardware random number generator, inlining uses more code, use hw_random16() if speed is critical (see fcn_declare.h) uint32_t hw_random(uint32_t upperlimit) { uint32_t rnd = hw_random(); uint64_t scaled = uint64_t(rnd) * uint64_t(upperlimit); return scaled >> 32; } int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { if(lowerlimit >= upperlimit) { return lowerlimit; } uint32_t diff = upperlimit - lowerlimit; return hw_random(diff) + lowerlimit; } // Platform-agnostic SHA1 computation from String input String computeSHA1(const String& input) { #ifdef ESP8266 return sha1(input); // ESP8266 has built-in sha1() function #else // ESP32: Compute SHA1 hash using mbedtls unsigned char shaResult[20]; // SHA1 produces 20 bytes mbedtls_sha1_context ctx; mbedtls_sha1_init(&ctx); mbedtls_sha1_starts_ret(&ctx); mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length()); mbedtls_sha1_finish_ret(&ctx, shaResult); mbedtls_sha1_free(&ctx); // Convert to hexadecimal string char hexString[41]; for (int i = 0; i < 20; i++) { sprintf(&hexString[i*2], "%02x", shaResult[i]); } hexString[40] = '\0'; return String(hexString); #endif } #ifdef ESP32 static String dump_raw_block(esp_efuse_block_t block) { const int WORDS = 8; // ESP32: 8×32-bit words per block i.e. 256bits uint32_t buf[WORDS] = {0}; const esp_efuse_desc_t d = { .efuse_block = block, .bit_start = 0, .bit_count = WORDS * 32 }; const esp_efuse_desc_t* field[2] = { &d, NULL }; esp_err_t err = esp_efuse_read_field_blob(field, buf, WORDS * 32); if (err != ESP_OK) { return ""; } String result = ""; for (const unsigned int i : buf) { char line[32]; sprintf(line, "0x%08X", i); result += line; } return result; } #endif // Generate a device ID based on SHA1 hash of MAC address salted with "WLED" // Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total) String getDeviceId() { static String cachedDeviceId = ""; if (cachedDeviceId.length() > 0) return cachedDeviceId; uint8_t mac[6]; WiFi.macAddress(mac); char macStr[18]; sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase // MAC is salted with other consistent device info to avoid rainbow table attacks. // If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices, // but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable. // If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1 #ifdef ESP8266 String deviceString = String(macStr) + "WLED" + ESP.getFlashChipId(); #else String deviceString = String(macStr) + "WLED" + ESP.getChipModel() + ESP.getChipRevision(); deviceString += dump_raw_block(EFUSE_BLK0); deviceString += dump_raw_block(EFUSE_BLK1); deviceString += dump_raw_block(EFUSE_BLK2); deviceString += dump_raw_block(EFUSE_BLK3); #endif String firstHash = computeSHA1(deviceString); // Second hash: SHA1 of the first hash String secondHash = computeSHA1(firstHash); // Concatenate first hash + last 2 chars of second hash cachedDeviceId = firstHash + secondHash.substring(38); return cachedDeviceId; }