diff --git a/.gitignore b/.gitignore index 51d321d922..6c80b8e8e5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ wled-update.sh /wled00/Release /wled00/wled00.ino.cpp /wled00/html_*.h +_codeql_detected_source_root diff --git a/package-lock.json b/package-lock.json index b80ed0c214..a8a0d89878 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.15.1", + "version": "0.15.2-beta2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.1", + "version": "0.15.2-beta2", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 16e4471f31..a467f8f51f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -400,6 +400,8 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL int16_t extractModeDefaults(uint8_t mode, const char *segVar); void checkSettingsPIN(const char *pin); uint16_t crc16(const unsigned char* data_p, size_t length); +String computeSHA1(const String& input); +String getDeviceId(); uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0); diff --git a/wled00/json.cpp b/wled00/json.cpp index ac9b0432b4..2126de361d 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1,4 +1,5 @@ #include "wled.h" +#include "ota_update.h" #include "palettes.h" @@ -631,6 +632,8 @@ void serializeInfo(JsonObject root) root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); root[F("release")] = releaseString; + root[F("repo")] = repoString; + root[F("deviceId")] = getDeviceId(); JsonObject leds = root.createNestedObject(F("leds")); leds[F("count")] = strip.getLengthTotal(); @@ -753,6 +756,9 @@ void serializeInfo(JsonObject root) root[F("resetReason1")] = (int)rtc_get_reset_reason(1); #endif root[F("lwip")] = 0; //deprecated + #ifndef WLED_DISABLE_OTA + root[F("bootloaderSHA256")] = getBootloaderSHA256Hex(); + #endif #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 2f3d6145e6..e0a3c3c85d 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -3,12 +3,28 @@ #ifdef ESP32 #include +#include +#include #endif // Platform-specific metadata locations #ifdef ESP32 constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata #define UPDATE_ERROR errorString + +// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB +// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064 +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) +constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5) +constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#else +constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#endif + #elif defined(ESP8266) constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset #define UPDATE_ERROR getErrorString @@ -253,4 +269,55 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, // Upload complete context->uploadComplete = true; } -} \ No newline at end of file +} + +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +static String bootloaderSHA256HexCache = ""; + +// Calculate and cache the bootloader SHA256 digest as hex string +void calculateBootloaderSHA256() { + if (!bootloaderSHA256HexCache.isEmpty()) return; + + // Calculate SHA256 + uint8_t sha256[32]; + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) + + const size_t chunkSize = 256; + uint8_t buffer[chunkSize]; + + for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) { + size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { // use esp_flash_read for V4 framework (-S2, -S3, -C3) +#else + if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32) +#endif + mbedtls_sha256_update(&ctx, buffer, readSize); + } + } + + mbedtls_sha256_finish(&ctx, sha256); + mbedtls_sha256_free(&ctx); + + // Convert to hex string and cache it + char hex[65]; + for (int i = 0; i < 32; i++) { + sprintf(hex + (i * 2), "%02x", sha256[i]); + } + hex[64] = '\0'; + bootloaderSHA256HexCache = hex; +} + +// Get bootloader SHA256 as hex string +String getBootloaderSHA256Hex() { + calculateBootloaderSHA256(); + return bootloaderSHA256HexCache; +} + +// Invalidate cached bootloader SHA256 (call after bootloader update) +void invalidateBootloaderSHA256Cache() { + bootloaderSHA256HexCache = ""; +} +#endif \ No newline at end of file diff --git a/wled00/ota_update.h b/wled00/ota_update.h index c8fd702643..6513e9750c 100644 --- a/wled00/ota_update.h +++ b/wled00/ota_update.h @@ -50,3 +50,23 @@ std::pair getOTAResult(AsyncWebServerRequest *request); * @return bool indicating if a reply is necessary; string with error message if the update failed. */ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); + +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +/** + * Calculate and cache the bootloader SHA256 digest + * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash + */ +void calculateBootloaderSHA256(); + +/** + * Get bootloader SHA256 as hex string + * @return String containing 64-character hex representation of SHA256 hash + */ +String getBootloaderSHA256Hex(); + +/** + * Invalidate cached bootloader SHA256 (call after bootloader update) + * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex + */ +void invalidateBootloaderSHA256Cache(); +#endif diff --git a/wled00/util.cpp b/wled00/util.cpp index 301b037b70..d96a6efc78 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -3,6 +3,7 @@ #include "const.h" #ifdef ESP8266 #include "user_interface.h" // for bootloop detection +#include // for SHA1 on ESP8266 #else #include #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) @@ -10,6 +11,8 @@ #elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0) #include "soc/rtc.h" #endif +#include "mbedtls/sha1.h" // for SHA1 on ESP32 +#include "esp_adc_cal.h" #endif @@ -745,3 +748,99 @@ void handleBootLoop() { ESP.restart(); // restart cleanly and don't wait for another crash } + +// 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 +String generateDeviceFingerprint() { + uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + esp_efuse_mac_get_default((uint8_t*)fp); + fp[1] ^= ESP.getFlashChipSize(); + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4) + fp[0] ^= chip_info.full_revision | (chip_info.model << 16); + #else + fp[0] ^= chip_info.revision | (chip_info.model << 16); + #endif + // mix in ADC calibration data: + esp_adc_cal_characteristics_t ch; + #if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC + constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13; + #else + constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12; + #endif + esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, myBIT_WIDTH, 1100, &ch); + fp[0] ^= ch.coeff_a; + fp[1] ^= ch.coeff_b; + if (ch.low_curve) { + for (int i = 0; i < 8; i++) { + fp[0] ^= ch.low_curve[i]; + } + } + if (ch.high_curve) { + for (int i = 0; i < 8; i++) { + fp[1] ^= ch.high_curve[i]; + } + } + char fp_string[17]; // 16 hex chars + null terminator + sprintf(fp_string, "%08X%08X", fp[1], fp[0]); + return String(fp_string); +} +#else // ESP8266 +String generateDeviceFingerprint() { + uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint + WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base + fp[0] ^= ESP.getFlashChipId(); + fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16; + char fp_string[17]; // 16 hex chars + null terminator + sprintf(fp_string, "%08X%08X", fp[1], fp[0]); + return String(fp_string); +} +#endif + +// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info +// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total) +String getDeviceId() { + static String cachedDeviceId = ""; + if (cachedDeviceId.length() > 0) return cachedDeviceId; + // 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 + + String firstHash = computeSHA1(generateDeviceFingerprint()); + + // 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; +}