Skip to content

Commit 53b88ca

Browse files
authored
Merge pull request #5126 from wled/copilot/backport-version-reporting-0-15-x
Backport version reporting (PR #5093 and #5111) to 0.15.x
2 parents bc41b38 + a374165 commit 53b88ca

File tree

7 files changed

+198
-3
lines changed

7 files changed

+198
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ wled-update.sh
2323
/wled00/Release
2424
/wled00/wled00.ino.cpp
2525
/wled00/html_*.h
26+
_codeql_detected_source_root

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wled00/fcn_declare.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL
400400
int16_t extractModeDefaults(uint8_t mode, const char *segVar);
401401
void checkSettingsPIN(const char *pin);
402402
uint16_t crc16(const unsigned char* data_p, size_t length);
403+
String computeSHA1(const String& input);
404+
String getDeviceId();
403405
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);
404406
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);
405407
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);

wled00/json.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "wled.h"
2+
#include "ota_update.h"
23

34
#include "palettes.h"
45

@@ -631,6 +632,8 @@ void serializeInfo(JsonObject root)
631632
root[F("vid")] = VERSION;
632633
root[F("cn")] = F(WLED_CODENAME);
633634
root[F("release")] = releaseString;
635+
root[F("repo")] = repoString;
636+
root[F("deviceId")] = getDeviceId();
634637

635638
JsonObject leds = root.createNestedObject(F("leds"));
636639
leds[F("count")] = strip.getLengthTotal();
@@ -753,6 +756,9 @@ void serializeInfo(JsonObject root)
753756
root[F("resetReason1")] = (int)rtc_get_reset_reason(1);
754757
#endif
755758
root[F("lwip")] = 0; //deprecated
759+
#ifndef WLED_DISABLE_OTA
760+
root[F("bootloaderSHA256")] = getBootloaderSHA256Hex();
761+
#endif
756762
#else
757763
root[F("arch")] = "esp8266";
758764
root[F("core")] = ESP.getCoreVersion();

wled00/ota_update.cpp

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,28 @@
33

44
#ifdef ESP32
55
#include <esp_ota_ops.h>
6+
#include <esp_spi_flash.h>
7+
#include <mbedtls/sha256.h>
68
#endif
79

810
// Platform-specific metadata locations
911
#ifdef ESP32
1012
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
1113
#define UPDATE_ERROR errorString
14+
15+
// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB
16+
// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064
17+
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
18+
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
19+
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
20+
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
21+
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
22+
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
23+
#else
24+
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
25+
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
26+
#endif
27+
1228
#elif defined(ESP8266)
1329
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
1430
#define UPDATE_ERROR getErrorString
@@ -253,4 +269,55 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data,
253269
// Upload complete
254270
context->uploadComplete = true;
255271
}
256-
}
272+
}
273+
274+
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
275+
static String bootloaderSHA256HexCache = "";
276+
277+
// Calculate and cache the bootloader SHA256 digest as hex string
278+
void calculateBootloaderSHA256() {
279+
if (!bootloaderSHA256HexCache.isEmpty()) return;
280+
281+
// Calculate SHA256
282+
uint8_t sha256[32];
283+
mbedtls_sha256_context ctx;
284+
mbedtls_sha256_init(&ctx);
285+
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
286+
287+
const size_t chunkSize = 256;
288+
uint8_t buffer[chunkSize];
289+
290+
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
291+
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
292+
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
293+
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { // use esp_flash_read for V4 framework (-S2, -S3, -C3)
294+
#else
295+
if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32)
296+
#endif
297+
mbedtls_sha256_update(&ctx, buffer, readSize);
298+
}
299+
}
300+
301+
mbedtls_sha256_finish(&ctx, sha256);
302+
mbedtls_sha256_free(&ctx);
303+
304+
// Convert to hex string and cache it
305+
char hex[65];
306+
for (int i = 0; i < 32; i++) {
307+
sprintf(hex + (i * 2), "%02x", sha256[i]);
308+
}
309+
hex[64] = '\0';
310+
bootloaderSHA256HexCache = hex;
311+
}
312+
313+
// Get bootloader SHA256 as hex string
314+
String getBootloaderSHA256Hex() {
315+
calculateBootloaderSHA256();
316+
return bootloaderSHA256HexCache;
317+
}
318+
319+
// Invalidate cached bootloader SHA256 (call after bootloader update)
320+
void invalidateBootloaderSHA256Cache() {
321+
bootloaderSHA256HexCache = "";
322+
}
323+
#endif

wled00/ota_update.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,23 @@ std::pair<bool, String> getOTAResult(AsyncWebServerRequest *request);
5050
* @return bool indicating if a reply is necessary; string with error message if the update failed.
5151
*/
5252
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
53+
54+
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
55+
/**
56+
* Calculate and cache the bootloader SHA256 digest
57+
* Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash
58+
*/
59+
void calculateBootloaderSHA256();
60+
61+
/**
62+
* Get bootloader SHA256 as hex string
63+
* @return String containing 64-character hex representation of SHA256 hash
64+
*/
65+
String getBootloaderSHA256Hex();
66+
67+
/**
68+
* Invalidate cached bootloader SHA256 (call after bootloader update)
69+
* Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex
70+
*/
71+
void invalidateBootloaderSHA256Cache();
72+
#endif

wled00/util.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
#include "const.h"
44
#ifdef ESP8266
55
#include "user_interface.h" // for bootloop detection
6+
#include <Hash.h> // for SHA1 on ESP8266
67
#else
78
#include <Update.h>
89
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
910
#include "esp32/rtc.h" // for bootloop detection
1011
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
1112
#include "soc/rtc.h"
1213
#endif
14+
#include "mbedtls/sha1.h" // for SHA1 on ESP32
15+
#include "esp_adc_cal.h"
1316
#endif
1417

1518

@@ -745,3 +748,99 @@ void handleBootLoop() {
745748

746749
ESP.restart(); // restart cleanly and don't wait for another crash
747750
}
751+
752+
// Platform-agnostic SHA1 computation from String input
753+
String computeSHA1(const String& input) {
754+
#ifdef ESP8266
755+
return sha1(input); // ESP8266 has built-in sha1() function
756+
#else
757+
// ESP32: Compute SHA1 hash using mbedtls
758+
unsigned char shaResult[20]; // SHA1 produces 20 bytes
759+
mbedtls_sha1_context ctx;
760+
761+
mbedtls_sha1_init(&ctx);
762+
mbedtls_sha1_starts_ret(&ctx);
763+
mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length());
764+
mbedtls_sha1_finish_ret(&ctx, shaResult);
765+
mbedtls_sha1_free(&ctx);
766+
767+
// Convert to hexadecimal string
768+
char hexString[41];
769+
for (int i = 0; i < 20; i++) {
770+
sprintf(&hexString[i*2], "%02x", shaResult[i]);
771+
}
772+
hexString[40] = '\0';
773+
774+
return String(hexString);
775+
#endif
776+
}
777+
778+
#ifdef ESP32
779+
String generateDeviceFingerprint() {
780+
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
781+
esp_chip_info_t chip_info;
782+
esp_chip_info(&chip_info);
783+
esp_efuse_mac_get_default((uint8_t*)fp);
784+
fp[1] ^= ESP.getFlashChipSize();
785+
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4)
786+
fp[0] ^= chip_info.full_revision | (chip_info.model << 16);
787+
#else
788+
fp[0] ^= chip_info.revision | (chip_info.model << 16);
789+
#endif
790+
// mix in ADC calibration data:
791+
esp_adc_cal_characteristics_t ch;
792+
#if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC
793+
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13;
794+
#else
795+
constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12;
796+
#endif
797+
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, myBIT_WIDTH, 1100, &ch);
798+
fp[0] ^= ch.coeff_a;
799+
fp[1] ^= ch.coeff_b;
800+
if (ch.low_curve) {
801+
for (int i = 0; i < 8; i++) {
802+
fp[0] ^= ch.low_curve[i];
803+
}
804+
}
805+
if (ch.high_curve) {
806+
for (int i = 0; i < 8; i++) {
807+
fp[1] ^= ch.high_curve[i];
808+
}
809+
}
810+
char fp_string[17]; // 16 hex chars + null terminator
811+
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
812+
return String(fp_string);
813+
}
814+
#else // ESP8266
815+
String generateDeviceFingerprint() {
816+
uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint
817+
WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base
818+
fp[0] ^= ESP.getFlashChipId();
819+
fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16;
820+
char fp_string[17]; // 16 hex chars + null terminator
821+
sprintf(fp_string, "%08X%08X", fp[1], fp[0]);
822+
return String(fp_string);
823+
}
824+
#endif
825+
826+
// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info
827+
// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)
828+
String getDeviceId() {
829+
static String cachedDeviceId = "";
830+
if (cachedDeviceId.length() > 0) return cachedDeviceId;
831+
// The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase
832+
// MAC is salted with other consistent device info to avoid rainbow table attacks.
833+
// If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices,
834+
// but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable.
835+
// If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1
836+
837+
String firstHash = computeSHA1(generateDeviceFingerprint());
838+
839+
// Second hash: SHA1 of the first hash
840+
String secondHash = computeSHA1(firstHash);
841+
842+
// Concatenate first hash + last 2 chars of second hash
843+
cachedDeviceId = firstHash + secondHash.substring(38);
844+
845+
return cachedDeviceId;
846+
}

0 commit comments

Comments
 (0)