diff --git a/.github/scripts/merge_packages.py b/.github/scripts/merge_packages.py index 8d1f200ec5c..3ca781678e1 100755 --- a/.github/scripts/merge_packages.py +++ b/.github/scripts/merge_packages.py @@ -17,7 +17,8 @@ def load_package(filename): - pkg = json.load(open(filename))["packages"][0] + with open(filename) as f: + pkg = json.load(f)["packages"][0] print("Loaded package {0} from {1}".format(pkg["name"], filename), file=sys.stderr) print("{0} platform(s), {1} tools".format(len(pkg["platforms"]), len(pkg["tools"])), file=sys.stderr) return pkg diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c9800dfe2b..ac306bfb1a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ set(CORE_SRCS cores/esp32/freertos_stats.cpp cores/esp32/FunctionalInterrupt.cpp cores/esp32/HardwareSerial.cpp + cores/esp32/HashBuilder.cpp cores/esp32/HEXBuilder.cpp cores/esp32/IPAddress.cpp cores/esp32/libb64/cdecode.c @@ -62,7 +63,6 @@ set(CORE_SRCS cores/esp32/main.cpp cores/esp32/MD5Builder.cpp cores/esp32/Print.cpp - cores/esp32/SHA1Builder.cpp cores/esp32/stdlib_noniso.c cores/esp32/Stream.cpp cores/esp32/StreamString.cpp @@ -93,6 +93,7 @@ set(ARDUINO_ALL_LIBRARIES Ethernet FFat FS + Hash HTTPClient HTTPUpdate Insights @@ -154,6 +155,13 @@ set(ARDUINO_LIBRARY_FS_SRCS libraries/FS/src/FS.cpp libraries/FS/src/vfs_api.cpp) +set(ARDUINO_LIBRARY_Hash_SRCS + libraries/Hash/src/SHA1Builder.cpp + libraries/Hash/src/SHA2Builder.cpp + libraries/Hash/src/SHA3Builder.cpp + libraries/Hash/src/PBKDF2_HMACBuilder.cpp + ) + set(ARDUINO_LIBRARY_HTTPClient_SRCS libraries/HTTPClient/src/HTTPClient.cpp) set(ARDUINO_LIBRARY_HTTPUpdate_SRCS libraries/HTTPUpdate/src/HTTPUpdate.cpp) diff --git a/cores/esp32/HEXBuilder.cpp b/cores/esp32/HEXBuilder.cpp index 6154f58b384..4298ad65d6a 100644 --- a/cores/esp32/HEXBuilder.cpp +++ b/cores/esp32/HEXBuilder.cpp @@ -17,8 +17,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include -#include +#include "HEXBuilder.h" static uint8_t hex_char_to_byte(uint8_t c) { return (c >= 'a' && c <= 'f') ? (c - ((uint8_t)'a' - 0xa)) diff --git a/cores/esp32/HEXBuilder.h b/cores/esp32/HEXBuilder.h index 0c35fbc1acc..c5b8a8a88f4 100644 --- a/cores/esp32/HEXBuilder.h +++ b/cores/esp32/HEXBuilder.h @@ -23,6 +23,8 @@ #include #include +// Basic hex/byte conversion class to be used by hash builders + class HEXBuilder { public: static size_t hex2bytes(unsigned char *out, size_t maxlen, String &in); diff --git a/cores/esp32/HashBuilder.cpp b/cores/esp32/HashBuilder.cpp new file mode 100644 index 00000000000..be3f67e2f78 --- /dev/null +++ b/cores/esp32/HashBuilder.cpp @@ -0,0 +1,38 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "HashBuilder.h" + +void HashBuilder::add(const char *data) { + add((const uint8_t *)data, strlen(data)); +} + +void HashBuilder::add(String data) { + add(data.c_str()); +} + +void HashBuilder::addHexString(const char *data) { + size_t len = strlen(data); + uint8_t *tmp = (uint8_t *)malloc(len / 2); + if (tmp == NULL) { + return; + } + hex2bytes(tmp, len / 2, data); + add(tmp, len / 2); + free(tmp); +} + +void HashBuilder::addHexString(String data) { + addHexString(data.c_str()); +} diff --git a/cores/esp32/HashBuilder.h b/cores/esp32/HashBuilder.h index 77d1c71dbde..597c7b9a2a3 100644 --- a/cores/esp32/HashBuilder.h +++ b/cores/esp32/HashBuilder.h @@ -20,29 +20,26 @@ #include "HEXBuilder.h" +// Base class for hash builders + class HashBuilder : public HEXBuilder { public: virtual ~HashBuilder() {} virtual void begin() = 0; virtual void add(const uint8_t *data, size_t len) = 0; - virtual void add(const char *data) { - add((const uint8_t *)data, strlen(data)); - } - virtual void add(String data) { - add(data.c_str()); - } - - virtual void addHexString(const char *data) = 0; - virtual void addHexString(String data) { - addHexString(data.c_str()); - } + void add(const char *data); + void add(String data); + + void addHexString(const char *data); + void addHexString(String data); virtual bool addStream(Stream &stream, const size_t maxLen) = 0; virtual void calculate() = 0; virtual void getBytes(uint8_t *output) = 0; virtual void getChars(char *output) = 0; virtual String toString() = 0; + virtual size_t getHashSize() const = 0; }; #endif diff --git a/cores/esp32/MD5Builder.cpp b/cores/esp32/MD5Builder.cpp index cd8aa31b6cc..3c578491c13 100644 --- a/cores/esp32/MD5Builder.cpp +++ b/cores/esp32/MD5Builder.cpp @@ -17,9 +17,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include -#include -#include +#include "HEXBuilder.h" +#include "MD5Builder.h" void MD5Builder::begin(void) { memset(_buf, 0x00, ESP_ROM_MD5_DIGEST_LEN); @@ -30,17 +29,6 @@ void MD5Builder::add(const uint8_t *data, size_t len) { esp_rom_md5_update(&_ctx, data, len); } -void MD5Builder::addHexString(const char *data) { - size_t len = strlen(data); - uint8_t *tmp = (uint8_t *)malloc(len / 2); - if (tmp == NULL) { - return; - } - hex2bytes(tmp, len / 2, data); - add(tmp, len / 2); - free(tmp); -} - bool MD5Builder::addStream(Stream &stream, const size_t maxLen) { const int buf_size = 512; int maxLengthLeft = maxLen; diff --git a/cores/esp32/MD5Builder.h b/cores/esp32/MD5Builder.h index 5728bd3bac0..7633670a610 100644 --- a/cores/esp32/MD5Builder.h +++ b/cores/esp32/MD5Builder.h @@ -35,19 +35,16 @@ class MD5Builder : public HashBuilder { uint8_t _buf[ESP_ROM_MD5_DIGEST_LEN]; public: - void begin(void) override; - using HashBuilder::add; - void add(const uint8_t *data, size_t len) override; - - using HashBuilder::addHexString; - void addHexString(const char *data) override; + void begin(void) override; + void add(const uint8_t *data, size_t len) override; bool addStream(Stream &stream, const size_t maxLen) override; void calculate(void) override; void getBytes(uint8_t *output) override; void getChars(char *output) override; String toString(void) override; + size_t getHashSize() const override { return ESP_ROM_MD5_DIGEST_LEN; } }; #endif diff --git a/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino b/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino index b3b01be61cd..934789a52bf 100644 --- a/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino +++ b/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino @@ -19,6 +19,7 @@ const char *ssid = ".........."; const char *password = ".........."; +uint32_t last_ota_time = 0; void setup() { Serial.begin(115200); @@ -40,9 +41,13 @@ void setup() { // No authentication by default // ArduinoOTA.setPassword("admin"); - // Password can be set with it's md5 value as well - // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 - // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); + // Password can be set with plain text (will be hashed internally) + // The authentication uses PBKDF2-HMAC-SHA256 with 10,000 iterations + // ArduinoOTA.setPassword("admin"); + + // Or set password with pre-hashed value (SHA256 hash of "admin") + // SHA256(admin) = 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 + // ArduinoOTA.setPasswordHash("8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"); ArduinoOTA .onStart([]() { @@ -60,7 +65,10 @@ void setup() { Serial.println("\nEnd"); }) .onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + if (millis() - last_ota_time > 500) { + Serial.printf("Progress: %u%%\n", (progress / (total / 100))); + last_ota_time = millis(); + } }) .onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); diff --git a/libraries/ArduinoOTA/src/ArduinoOTA.cpp b/libraries/ArduinoOTA/src/ArduinoOTA.cpp index cb3ddc1e797..308660c3ce7 100644 --- a/libraries/ArduinoOTA/src/ArduinoOTA.cpp +++ b/libraries/ArduinoOTA/src/ArduinoOTA.cpp @@ -19,7 +19,8 @@ #include "ArduinoOTA.h" #include "NetworkClient.h" #include "ESPmDNS.h" -#include "MD5Builder.h" +#include "SHA2Builder.h" +#include "PBKDF2_HMACBuilder.h" #include "Update.h" // #define OTA_DEBUG Serial @@ -72,18 +73,20 @@ String ArduinoOTAClass::getHostname() { ArduinoOTAClass &ArduinoOTAClass::setPassword(const char *password) { if (_state == OTA_IDLE && password) { - MD5Builder passmd5; - passmd5.begin(); - passmd5.add(password); - passmd5.calculate(); + // Hash the password with SHA256 for storage (not plain text) + SHA256Builder pass_hash; + pass_hash.begin(); + pass_hash.add(password); + pass_hash.calculate(); _password.clear(); - _password = passmd5.toString(); + _password = pass_hash.toString(); } return *this; } ArduinoOTAClass &ArduinoOTAClass::setPasswordHash(const char *password) { if (_state == OTA_IDLE && password) { + // Store the pre-hashed password directly _password.clear(); _password = password; } @@ -188,17 +191,18 @@ void ArduinoOTAClass::_onRx() { _udp_ota.read(); _md5 = readStringUntil('\n'); _md5.trim(); - if (_md5.length() != 32) { + if (_md5.length() != 32) { // MD5 produces 32 character hex string for firmware integrity log_e("bad md5 length"); return; } if (_password.length()) { - MD5Builder nonce_md5; - nonce_md5.begin(); - nonce_md5.add(String(micros())); - nonce_md5.calculate(); - _nonce = nonce_md5.toString(); + // Generate a random challenge (nonce) + SHA256Builder nonce_sha256; + nonce_sha256.begin(); + nonce_sha256.add(String(micros()) + String(random(1000000))); + nonce_sha256.calculate(); + _nonce = nonce_sha256.toString(); _udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort()); _udp_ota.printf("AUTH %s", _nonce.c_str()); @@ -222,20 +226,37 @@ void ArduinoOTAClass::_onRx() { _udp_ota.read(); String cnonce = readStringUntil(' '); String response = readStringUntil('\n'); - if (cnonce.length() != 32 || response.length() != 32) { + if (cnonce.length() != 64 || response.length() != 64) { // SHA256 produces 64 character hex string log_e("auth param fail"); _state = OTA_IDLE; return; } - String challenge = _password + ":" + String(_nonce) + ":" + cnonce; - MD5Builder _challengemd5; - _challengemd5.begin(); - _challengemd5.add(challenge); - _challengemd5.calculate(); - String result = _challengemd5.toString(); - - if (result.equals(response)) { + // Verify the challenge/response using PBKDF2-HMAC-SHA256 + // The client should derive a key using PBKDF2-HMAC-SHA256 with: + // - password: the OTA password (or its hash if using setPasswordHash) + // - salt: nonce + cnonce + // - iterations: 10000 (or configurable) + // Then hash the challenge with the derived key + + String salt = _nonce + ":" + cnonce; + SHA256Builder sha256; + // Use the stored password hash for PBKDF2 derivation + PBKDF2_HMACBuilder pbkdf2(&sha256, _password, salt, 10000); + + pbkdf2.begin(); + pbkdf2.calculate(); + String derived_key = pbkdf2.toString(); + + // Create challenge: derived_key + nonce + cnonce + String challenge = derived_key + ":" + _nonce + ":" + cnonce; + SHA256Builder challenge_sha256; + challenge_sha256.begin(); + challenge_sha256.add(challenge); + challenge_sha256.calculate(); + String expected_response = challenge_sha256.toString(); + + if (expected_response.equals(response)) { _udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort()); _udp_ota.print("OK"); _udp_ota.endPacket(); @@ -266,7 +287,8 @@ void ArduinoOTAClass::_runUpdate() { _state = OTA_IDLE; return; } - Update.setMD5(_md5.c_str()); + + Update.setMD5(_md5.c_str()); // Note: Update library still uses MD5 for firmware integrity, this is separate from authentication if (_start_callback) { _start_callback(); diff --git a/libraries/ArduinoOTA/src/ArduinoOTA.h b/libraries/ArduinoOTA/src/ArduinoOTA.h index 7916e3b328d..a946388c4aa 100644 --- a/libraries/ArduinoOTA/src/ArduinoOTA.h +++ b/libraries/ArduinoOTA/src/ArduinoOTA.h @@ -54,7 +54,7 @@ class ArduinoOTAClass { //Sets the password that will be required for OTA. Default NULL ArduinoOTAClass &setPassword(const char *password); - //Sets the password as above but in the form MD5(password). Default NULL + //Sets the password as above but in the form SHA256(password). Default NULL ArduinoOTAClass &setPasswordHash(const char *password); //Sets the partition label to write to when updating SPIFFS. Default NULL diff --git a/libraries/ESP32/examples/Utilities/HEXBuilder/HEXBuilder.ino b/libraries/Hash/examples/HEX/HEX.ino similarity index 94% rename from libraries/ESP32/examples/Utilities/HEXBuilder/HEXBuilder.ino rename to libraries/Hash/examples/HEX/HEX.ino index f580a763b54..4ad78db3ad9 100644 --- a/libraries/ESP32/examples/Utilities/HEXBuilder/HEXBuilder.ino +++ b/libraries/Hash/examples/HEX/HEX.ino @@ -1,3 +1,9 @@ +/* + Usage example for the HEXBuilder class. + + This example shows how to convert a HEX string to a binary buffer and vice versa. +*/ + #include void setup() { diff --git a/libraries/ESP32/examples/Utilities/MD5Builder/MD5Builder.ino b/libraries/Hash/examples/MD5/MD5.ino similarity index 100% rename from libraries/ESP32/examples/Utilities/MD5Builder/MD5Builder.ino rename to libraries/Hash/examples/MD5/MD5.ino diff --git a/libraries/Hash/examples/PBKDF2_HMAC/PBKDF2_HMAC.ino b/libraries/Hash/examples/PBKDF2_HMAC/PBKDF2_HMAC.ino new file mode 100644 index 00000000000..fd4d4a8bdb8 --- /dev/null +++ b/libraries/Hash/examples/PBKDF2_HMAC/PBKDF2_HMAC.ino @@ -0,0 +1,178 @@ +/* + Usage example for the PBKDF2_HMACBuilder class. + + This example shows how to use the Hash library to hash data using the PBKDF2_HMACBuilder class. + PBKDF2_HMAC (Password-Based Key Derivation Function 2) is a key derivation function that uses a password and a salt to derive a key. + + The PBKDF2_HMACBuilder class takes for arguments: + - A HashBuilder object to use for the HMAC (SHA1Builder, SHA2Builder, SHA3Builder, etc.) + - A password string (default: empty) + - A salt string (default: empty) + - The number of iterations (default: 1000) +*/ + +#include +#include +#include + +void setup() { + Serial.begin(115200); + Serial.println("\n\nPBKDF2-HMAC Example"); + Serial.println("==================="); + + // Test 1: Basic PBKDF2-HMAC-SHA1 + Serial.println("\n1. PBKDF2-HMAC-SHA1 Test (1 iteration)"); + { + SHA1Builder sha1; + PBKDF2_HMACBuilder pbkdf2(&sha1, "password", "salt", 1); + + pbkdf2.begin(); + pbkdf2.calculate(); + + Serial.print("Password: "); + Serial.println("password"); + Serial.print("Salt: "); + Serial.println("salt"); + Serial.print("Iterations: "); + Serial.println(1); + Serial.print("Output (hex): "); + Serial.println(pbkdf2.toString()); + + // Expected: 0c60c80f961f0e71f3a9b524af6012062fe037a6 + String expected = "0c60c80f961f0e71f3a9b524af6012062fe037a6"; + String result = pbkdf2.toString(); + + if (result.equalsIgnoreCase(expected)) { + Serial.println("✓ PASS: Output matches expected value"); + } else { + Serial.println("✗ FAIL: Output does not match expected value"); + Serial.print("Expected: "); + Serial.println(expected); + Serial.print("Got: "); + Serial.println(result); + } + } + + // Test 2: PBKDF2-HMAC-SHA1 with more iterations + Serial.println("\n2. PBKDF2-HMAC-SHA1 Test (1000 iterations)"); + { + SHA1Builder sha1; + PBKDF2_HMACBuilder pbkdf2(&sha1); + + const char* password = "password"; + const char* salt = "salt"; + + pbkdf2.begin(); + pbkdf2.setPassword(password); + pbkdf2.setSalt(salt); + pbkdf2.setIterations(1000); + pbkdf2.calculate(); + + Serial.print("Password: "); + Serial.println(password); + Serial.print("Salt: "); + Serial.println(salt); + Serial.print("Iterations: "); + Serial.println(1000); + Serial.print("Output (hex): "); + Serial.println(pbkdf2.toString()); + + // Expected: 6e88be8bad7eae9d9e10aa061224034fed48d03f + String expected = "6e88be8bad7eae9d9e10aa061224034fed48d03f"; + String result = pbkdf2.toString(); + + if (result.equalsIgnoreCase(expected)) { + Serial.println("✓ PASS: Output matches expected value"); + } else { + Serial.println("✗ FAIL: Output does not match expected value"); + Serial.print("Expected: "); + Serial.println(expected); + Serial.print("Got: "); + Serial.println(result); + } + } + + // Test 3: PBKDF2-HMAC-SHA256 with different password and salt + Serial.println("\n3. PBKDF2-HMAC-SHA256 Test"); + { + SHA256Builder sha256; + PBKDF2_HMACBuilder pbkdf2(&sha256, "mySecretPassword", "randomSalt123", 100); + + pbkdf2.begin(); + pbkdf2.calculate(); + + Serial.print("Password: "); + Serial.println("mySecretPassword"); + Serial.print("Salt: "); + Serial.println("randomSalt123"); + Serial.print("Iterations: "); + Serial.println(100); + Serial.print("Output (hex): "); + Serial.println(pbkdf2.toString()); + + // Expected: 4ce309e56a37e0a4b9b84b98ed4a94e6c5cd5926cfd3baca3a6dea8c5d7903e8 + String expected = "4ce309e56a37e0a4b9b84b98ed4a94e6c5cd5926cfd3baca3a6dea8c5d7903e8"; + String result = pbkdf2.toString(); + + if (result.equalsIgnoreCase(expected)) { + Serial.println("✓ PASS: Output matches expected value"); + } else { + Serial.println("✗ FAIL: Output does not match expected value"); + Serial.print("Expected: "); + Serial.println(expected); + Serial.print("Got: "); + Serial.println(result); + } + } + + // Test 4: PBKDF2-HMAC-SHA1 with byte arrays + Serial.println("\n4. PBKDF2-HMAC-SHA1 Test (byte arrays)"); + { + SHA1Builder sha1; // or any other hash algorithm based on HashBuilder + PBKDF2_HMACBuilder pbkdf2(&sha1); + + uint8_t password[] = {0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64}; // "password" in bytes + uint8_t salt[] = {0x73, 0x61, 0x6c, 0x74}; // "salt" in bytes + + pbkdf2.begin(); + pbkdf2.setPassword(password, sizeof(password)); + pbkdf2.setSalt(salt, sizeof(salt)); + pbkdf2.setIterations(1); + pbkdf2.calculate(); + + Serial.print("Password (bytes): "); + for (int i = 0; i < sizeof(password); i++) { + Serial.print((char)password[i]); + } + Serial.println(); + Serial.print("Salt (bytes): "); + for (int i = 0; i < sizeof(salt); i++) { + Serial.print((char)salt[i]); + } + Serial.println(); + Serial.print("Iterations: "); + Serial.println(1); + Serial.print("Output (hex): "); + Serial.println(pbkdf2.toString()); + + // Expected: 0c60c80f961f0e71f3a9b524af6012062fe037a6 (same as test 1) + String expected = "0c60c80f961f0e71f3a9b524af6012062fe037a6"; + String result = pbkdf2.toString(); + + if (result.equalsIgnoreCase(expected)) { + Serial.println("✓ PASS: Output matches expected value"); + } else { + Serial.println("✗ FAIL: Output does not match expected value"); + Serial.print("Expected: "); + Serial.println(expected); + Serial.print("Got: "); + Serial.println(result); + } + } + + Serial.println("\nPBKDF2-HMAC tests completed!"); +} + +void loop() { + // Nothing to do in loop +} diff --git a/libraries/ESP32/examples/Utilities/SHA1Builder/SHA1Builder.ino b/libraries/Hash/examples/SHA1/SHA1.ino similarity index 100% rename from libraries/ESP32/examples/Utilities/SHA1Builder/SHA1Builder.ino rename to libraries/Hash/examples/SHA1/SHA1.ino diff --git a/libraries/Hash/examples/SHA2/SHA2.ino b/libraries/Hash/examples/SHA2/SHA2.ino new file mode 100644 index 00000000000..9de4f954ae4 --- /dev/null +++ b/libraries/Hash/examples/SHA2/SHA2.ino @@ -0,0 +1,94 @@ +/* + Usage example for the SHA2Builder class. + + This example shows how to use the SHA2 library to hash data using the SHA2Builder class. + SHA2 (Secure Hash Algorithm 2) provides different output sizes: SHA-224, SHA-256, SHA-384, and SHA-512. + + Available constructors: + - SHA224Builder(): 224-bit hash output + - SHA256Builder(): 256-bit hash output + - SHA384Builder(): 384-bit hash output + - SHA512Builder(): 512-bit hash output + - SHA2Builder(size_t hash_size): Generic class that can be used to create any SHA2 variant implemented +*/ + +#include + +// Expected hash values for validation +const char* EXPECTED_HELLO_WORLD_SHA256 = "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"; +const char* EXPECTED_HELLO_WORLD_SHA512 = "2c74fd17edafd80e8447b0d46741ee243b7eb74dd2149a0ab1b9246fb30382f27e853d8585719e0e67cbda0daa8f51671064615d645ae27acb15bfb1447f459b"; +const char* EXPECTED_TEST_MESSAGE_SHA224 = "155b033d801d4dd59b783d76ac3059053c00b2c28340a5a36a427a76"; +const char* EXPECTED_TEST_MESSAGE_SHA384 = "efd336618cbc96551936e5897e6af391d2480513ff8d4fc744e34462edb3111477d2b889c4d5e80e23b5f9d1b636fbd7"; + +// Validation function +bool validateHash(const String& calculated, const char* expected, const String& test_name) { + bool passed = (calculated == expected); + Serial.print(test_name); + Serial.print(": "); + Serial.println(passed ? "PASS" : "FAIL"); + Serial.print(" Expected: "); + Serial.println(expected); + Serial.print(" Got: "); + Serial.println(calculated); + return passed; +} + +void setup() { + Serial.begin(115200); + + Serial.println("\n\n\nStart."); + + // Using SHA2Builder class directly with different hash sizes + { + String test_data = "Hello World"; + Serial.println("Test data: " + test_data); + + // Create SHA-256 (default hash size) + SHA2Builder sha2_256; + sha2_256.begin(); + sha2_256.add(test_data); + sha2_256.calculate(); + String hash_256 = sha2_256.toString(); + validateHash(hash_256, EXPECTED_HELLO_WORLD_SHA256, "SHA-256 validation"); + + // Create SHA-512 + SHA2Builder sha2_512(SHA2_512_HASH_SIZE); + sha2_512.begin(); + sha2_512.add(test_data); + sha2_512.calculate(); + String hash_512 = sha2_512.toString(); + validateHash(hash_512, EXPECTED_HELLO_WORLD_SHA512, "SHA-512 validation"); + } + + // Example using SHA224Builder and SHA384Builder + // There are other constructors for other hash sizes available: + // - SHA224Builder() + // - SHA256Builder() + // - SHA384Builder() + // - SHA512Builder() + // - SHA2Builder(size_t hash_size) + { + String test_data = "Test message"; + Serial.println("Test data: " + test_data); + + // Create SHA-224 using specific constructor + SHA224Builder sha2_224; + sha2_224.begin(); + sha2_224.add(test_data); + sha2_224.calculate(); + String hash_224 = sha2_224.toString(); + validateHash(hash_224, EXPECTED_TEST_MESSAGE_SHA224, "SHA224Builder validation"); + + // Create SHA-384 using specific constructor + SHA384Builder sha2_384; + sha2_384.begin(); + sha2_384.add(test_data); + sha2_384.calculate(); + String hash_384 = sha2_384.toString(); + validateHash(hash_384, EXPECTED_TEST_MESSAGE_SHA384, "SHA384Builder validation"); + } + + Serial.println("Done."); +} + +void loop() {} diff --git a/libraries/Hash/examples/SHA3/SHA3.ino b/libraries/Hash/examples/SHA3/SHA3.ino new file mode 100644 index 00000000000..82e163f0840 --- /dev/null +++ b/libraries/Hash/examples/SHA3/SHA3.ino @@ -0,0 +1,94 @@ +/* + Usage example for the SHA3Builder class. + + This example shows how to use the SHA3 library to hash data using the SHA3Builder class. + SHA3 (Secure Hash Algorithm 3) provides different output sizes: SHA3-224, SHA3-256, SHA3-384, and SHA3-512. + + Available constructors: + - SHA3_224Builder(): 224-bit hash output + - SHA3_256Builder(): 256-bit hash output + - SHA3_384Builder(): 384-bit hash output + - SHA3_512Builder(): 512-bit hash output + - SHA3Builder(size_t hash_size): Generic class that can be used to create any SHA3 variant implemented +*/ + +#include + +// Expected hash values for validation +const char* EXPECTED_HELLO_WORLD_SHA3_256 = "e167f68d6563d75bb25f3aa49c29ef612d41352dc00606de7cbd630bb2665f51"; +const char* EXPECTED_HELLO_WORLD_SHA3_512 = "3d58a719c6866b0214f96b0a67b37e51a91e233ce0be126a08f35fdf4c043c6126f40139bfbc338d44eb2a03de9f7bb8eff0ac260b3629811e389a5fbee8a894"; +const char* EXPECTED_TEST_MESSAGE_SHA3_224 = "27af391bcb3b86f21b73c42c4abbde4791c395dc650243eede85de0c"; +const char* EXPECTED_TEST_MESSAGE_SHA3_384 = "adb18f6b164672c566950bfefa48c5a851d48ee184f249a19e723d753b7536fcd048c3443aff7ebe433fce63c81726ea"; + +// Validation function +bool validateHash(const String& calculated, const char* expected, const String& test_name) { + bool passed = (calculated == expected); + Serial.print(test_name); + Serial.print(": "); + Serial.println(passed ? "PASS" : "FAIL"); + Serial.print(" Expected: "); + Serial.println(expected); + Serial.print(" Got: "); + Serial.println(calculated); + return passed; +} + +void setup() { + Serial.begin(115200); + + Serial.println("\n\n\nStart."); + + // Using SHA3Builder class directly with different hash sizes + { + String test_data = "Hello World"; + Serial.println("Test data: " + test_data); + + // Create SHA3-256 (default hash size) + SHA3Builder sha3_256; + sha3_256.begin(); + sha3_256.add(test_data); + sha3_256.calculate(); + String hash_256 = sha3_256.toString(); + validateHash(hash_256, EXPECTED_HELLO_WORLD_SHA3_256, "SHA3-256 validation"); + + // Create SHA3-512 + SHA3Builder sha3_512(SHA3_512_HASH_SIZE); + sha3_512.begin(); + sha3_512.add(test_data); + sha3_512.calculate(); + String hash_512 = sha3_512.toString(); + validateHash(hash_512, EXPECTED_HELLO_WORLD_SHA3_512, "SHA3-512 validation"); + } + + // Example using SHA3_224Builder and SHA3_384Builder + // There are other constructors for other hash sizes available: + // - SHA3_224Builder() + // - SHA3_256Builder() + // - SHA3_384Builder() + // - SHA3_512Builder() + // - SHA3Builder(size_t hash_size) + { + String test_data = "Test message"; + Serial.println("Test data: " + test_data); + + // Create SHA3-224 using specific constructor + SHA3_224Builder sha3_224; + sha3_224.begin(); + sha3_224.add(test_data); + sha3_224.calculate(); + String hash_224 = sha3_224.toString(); + validateHash(hash_224, EXPECTED_TEST_MESSAGE_SHA3_224, "SHA3_224Builder validation"); + + // Create SHA3-384 using specific constructor + SHA3_384Builder sha3_384; + sha3_384.begin(); + sha3_384.add(test_data); + sha3_384.calculate(); + String hash_384 = sha3_384.toString(); + validateHash(hash_384, EXPECTED_TEST_MESSAGE_SHA3_384, "SHA3_384Builder validation"); + } + + Serial.println("Done."); +} + +void loop() {} diff --git a/libraries/Hash/examples/SHA3Stream/SHA3Stream.ino b/libraries/Hash/examples/SHA3Stream/SHA3Stream.ino new file mode 100644 index 00000000000..ca43cb58409 --- /dev/null +++ b/libraries/Hash/examples/SHA3Stream/SHA3Stream.ino @@ -0,0 +1,166 @@ +/* + Usage example for the SHA3Builder class with streams. + + This example shows how to use the SHA3 library to hash data from streams using the addStream method. + This is useful for hashing large files or data that comes from various stream sources like: + - File streams + - Network streams + - Memory streams + - Custom stream implementations + + Available constructors: + - SHA3_224Builder(): 224-bit hash output + - SHA3_256Builder(): 256-bit hash output + - SHA3_384Builder(): 384-bit hash output + - SHA3_512Builder(): 512-bit hash output + - SHA3Builder(size_t hash_size): Generic class that can be used to create any SHA3 variant implemented +*/ + +#include +#include + +// Expected hash values for validation +const char* EXPECTED_STREAM_TEST_SHA3_256 = "7094efc774885c7a785b408c5da86636cb8adc79156c0f162c6fd7e49f4c505e"; +const char* EXPECTED_MAX_SHA3_224_FULL = "ad0e69e04a7258d7cab4272a08ac69f8b43f4e45f9c49c9abb0628af"; +const char* EXPECTED_MAX_SHA3_224_10 = "9b55096e998cda6b96d3f2828c4ccda8c9964a1ad98989fb8b0fcd26"; +const char* EXPECTED_COMBINED_SHA3_256 = "4a32307fe03bf9f600c5d124419985fd4d42c1639e6a23ab044f107c3b95a189"; + +// Validation function +bool validateHash(const String& calculated, const char* expected, const String& test_name) { + bool passed = (calculated == expected); + Serial.print(test_name); + Serial.print(": "); + Serial.println(passed ? "PASS" : "FAIL"); + Serial.print(" Expected: "); + Serial.println(expected); + Serial.print(" Got: "); + Serial.println(calculated); + return passed; +} + +// Custom stream class for demonstration +class TestStream : public Stream { +private: + String data; + size_t position; + +public: + TestStream(String input_data) : data(input_data), position(0) {} + + virtual int available() override { + return data.length() - position; + } + + virtual int read() override { + if (position < data.length()) { + return data.charAt(position++); + } + return -1; + } + + virtual int peek() override { + if (position < data.length()) { + return data.charAt(position); + } + return -1; + } + + virtual size_t write(uint8_t) override { + return 0; // Read-only stream + } + + size_t length() { + return data.length(); + } + + void reset() { + position = 0; + } +}; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("\n\nSHA3 Stream Example"); + Serial.println("==================="); + + // Example 1: Using addStream with a custom stream + { + Serial.println("\n1. Hashing data from a custom stream:"); + + const char* test_data = "This is a test message for streaming hash calculation. " + "It contains multiple sentences to demonstrate how the " + "addStream method processes data in chunks."; + + TestStream stream(test_data); + + SHA3_256Builder sha3_256; + sha3_256.begin(); + + // Hash the entire stream + // First argument is the stream, second argument is the maximum length to be read from the stream + sha3_256.addStream(stream, stream.length()); // Reading the entire stream + sha3_256.calculate(); + String hash_256 = sha3_256.toString(); + validateHash(hash_256, EXPECTED_STREAM_TEST_SHA3_256, "Stream test validation"); + } + + // Example 2: Using addStream with different maximum lengths + { + Serial.println("\n2. Comparing different maximum lengths with streams:"); + + const char* test_data = "Streaming hash test with different maximum lengths"; + TestStream stream(test_data); + + // SHA3-224 with a hardcoded maximum length + stream.reset(); + SHA3_224Builder sha3_224_10; + sha3_224_10.begin(); + sha3_224_10.addStream(stream, 10); // Passing a hardcoded maximum length to be read from the stream + sha3_224_10.calculate(); + String hash_224_10 = sha3_224_10.toString(); + validateHash(hash_224_10, EXPECTED_MAX_SHA3_224_10, "SHA3-224 with 10 bytes"); + + // SHA3-224 with the full stream + stream.reset(); + SHA3_224Builder sha3_224_full; + sha3_224_full.begin(); + sha3_224_full.addStream(stream, stream.length()); // Reading the entire stream + sha3_224_full.calculate(); + String hash_224_full = sha3_224_full.toString(); + validateHash(hash_224_full, EXPECTED_MAX_SHA3_224_FULL, "SHA3-224 with full stream"); + } + + // Example 3: Combining add() and addStream() + { + Serial.println("\n3. Combining add() and addStream():"); + + const char* stream_data = "Additional data from stream"; + TestStream stream(stream_data); + + SHA3_256Builder sha3_256; + sha3_256.begin(); + + // Add some data directly + sha3_256.add("Initial data: "); + + // Add data from stream + sha3_256.addStream(stream, stream.length()); + + // Add more data directly + sha3_256.add(" : Final data"); + + sha3_256.calculate(); + String hash_256 = sha3_256.toString(); + validateHash(hash_256, EXPECTED_COMBINED_SHA3_256, "Combined data validation"); + } + + Serial.println("\nStream example completed!"); +} + +void loop() { + // Nothing to do in loop +} diff --git a/libraries/Hash/keywords.txt b/libraries/Hash/keywords.txt new file mode 100644 index 00000000000..d553b7b428e --- /dev/null +++ b/libraries/Hash/keywords.txt @@ -0,0 +1,51 @@ +####################################### +# Syntax Coloring Map For Hash +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +HashBuilder KEYWORD1 +HEXBuilder KEYWORD1 +MD5Builder KEYWORD1 +SHA1Builder KEYWORD1 +SHA2Builder KEYWORD1 +SHA224Builder KEYWORD1 +SHA256Builder KEYWORD1 +SHA384Builder KEYWORD1 +SHA512Builder KEYWORD1 +SHA3Builder KEYWORD1 +PBKDF2_HMACBuilder KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +begin KEYWORD2 +add KEYWORD2 +addHexString KEYWORD2 +addStream KEYWORD2 +calculate KEYWORD2 +getBytes KEYWORD2 +getChars KEYWORD2 +toString KEYWORD2 +hex2bytes KEYWORD2 +bytes2hex KEYWORD2 +getHashSize KEYWORD2 +setPassword KEYWORD2 +setSalt KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### + +SHA1_HASH_SIZE LITERAL1 +SHA2_224_HASH_SIZE LITERAL1 +SHA2_256_HASH_SIZE LITERAL1 +SHA2_384_HASH_SIZE LITERAL1 +SHA2_512_HASH_SIZE LITERAL1 +SHA3_224_HASH_SIZE LITERAL1 +SHA3_256_HASH_SIZE LITERAL1 +SHA3_384_HASH_SIZE LITERAL1 +SHA3_512_HASH_SIZE LITERAL1 diff --git a/libraries/Hash/library.properties b/libraries/Hash/library.properties new file mode 100644 index 00000000000..5db4df17c2d --- /dev/null +++ b/libraries/Hash/library.properties @@ -0,0 +1,9 @@ +name=Hash +version=3.3.0 +author=lucasssvaz +maintainer=lucasssvaz +sentence=Bundle of hashing functions for the ESP32 +paragraph=This library provides a set of hashing functions to be used in the sketches +category=Security +url= +architectures=esp32 diff --git a/libraries/Hash/src/PBKDF2_HMACBuilder.cpp b/libraries/Hash/src/PBKDF2_HMACBuilder.cpp new file mode 100644 index 00000000000..5dcec96f87e --- /dev/null +++ b/libraries/Hash/src/PBKDF2_HMACBuilder.cpp @@ -0,0 +1,258 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "PBKDF2_HMACBuilder.h" + +// Block size for HMAC (64 bytes for SHA-1, SHA-256, SHA-512) +#define HMAC_BLOCK_SIZE 64 + +PBKDF2_HMACBuilder::PBKDF2_HMACBuilder(HashBuilder* hash, String password, String salt, uint32_t iterations) { + this->hashBuilder = hash; + this->hashSize = hashBuilder->getHashSize(); + this->iterations = iterations; + + // Initialize pointers + this->password = nullptr; + this->salt = nullptr; + this->passwordLen = 0; + this->saltLen = 0; + this->derivedKey = nullptr; + this->derivedKeyLen = 0; + this->calculated = false; + + if (password.length() > 0) { + setPassword(password); + } + + if (salt.length() > 0) { + setSalt(salt); + } +} + +PBKDF2_HMACBuilder::~PBKDF2_HMACBuilder() { + clearData(); +} + +void PBKDF2_HMACBuilder::clearData() { + if (derivedKey != nullptr) { + delete[] derivedKey; + derivedKey = nullptr; + } + derivedKeyLen = 0; + calculated = false; +} + +void PBKDF2_HMACBuilder::hmac(const uint8_t* key, size_t keyLen, const uint8_t* data, size_t dataLen, uint8_t* output) { + uint8_t keyPad[HMAC_BLOCK_SIZE]; + uint8_t outerPad[HMAC_BLOCK_SIZE]; + uint8_t innerHash[64]; // Large enough for any hash + + // Prepare key + if (keyLen > HMAC_BLOCK_SIZE) { + // Key is longer than block size, hash it + hashBuilder->begin(); + hashBuilder->add(key, keyLen); + hashBuilder->calculate(); + hashBuilder->getBytes(keyPad); + keyLen = hashSize; + } else { + // Copy key to keyPad + memcpy(keyPad, key, keyLen); + } + + // Pad key with zeros if necessary + if (keyLen < HMAC_BLOCK_SIZE) { + memset(keyPad + keyLen, 0, HMAC_BLOCK_SIZE - keyLen); + } + + // Create outer and inner pads + for (int i = 0; i < HMAC_BLOCK_SIZE; i++) { + outerPad[i] = keyPad[i] ^ 0x5c; + keyPad[i] = keyPad[i] ^ 0x36; + } + + // Inner hash: H(K XOR ipad, text) + hashBuilder->begin(); + hashBuilder->add(keyPad, HMAC_BLOCK_SIZE); + hashBuilder->add(data, dataLen); + hashBuilder->calculate(); + hashBuilder->getBytes(innerHash); + + // Outer hash: H(K XOR opad, inner_hash) + hashBuilder->begin(); + hashBuilder->add(outerPad, HMAC_BLOCK_SIZE); + hashBuilder->add(innerHash, hashSize); + hashBuilder->calculate(); + hashBuilder->getBytes(output); +} + +// HashBuilder interface methods +void PBKDF2_HMACBuilder::begin() { + clearData(); +} + +void PBKDF2_HMACBuilder::add(const uint8_t *data, size_t len) { + log_w("PBKDF2_HMACBuilder::add sets only the password. Use setPassword() and setSalt() instead."); + setPassword(data, len); +} + +bool PBKDF2_HMACBuilder::addStream(Stream &stream, const size_t maxLen) { + log_e("PBKDF2_HMACBuilder does not support addStream. Use setPassword() and setSalt() instead."); + return false; +} + +void PBKDF2_HMACBuilder::calculate() { + if (password == nullptr || salt == nullptr) { + log_e("Error: Password or salt not set."); + return; + } + + // Set default output size to hash size if not specified + if (derivedKeyLen == 0) { + derivedKeyLen = hashSize; + } + + // Allocate output buffer + if (derivedKey != nullptr) { + delete[] derivedKey; + } + derivedKey = new uint8_t[derivedKeyLen]; + + // Perform PBKDF2-HMAC + pbkdf2_hmac(password, passwordLen, salt, saltLen, iterations, derivedKey, derivedKeyLen); + calculated = true; +} + +void PBKDF2_HMACBuilder::getBytes(uint8_t *output) { + if (!calculated || derivedKey == nullptr) { + log_e("Error: PBKDF2-HMAC not calculated or no output buffer provided."); + return; + } + memcpy(output, derivedKey, derivedKeyLen); +} + +void PBKDF2_HMACBuilder::getChars(char *output) { + if (!calculated || derivedKey == nullptr) { + log_e("Error: PBKDF2-HMAC not calculated or no output buffer provided."); + return; + } + for (size_t i = 0; i < derivedKeyLen; i++) { + output[i] = (char)derivedKey[i]; + } +} + +String PBKDF2_HMACBuilder::toString() { + if (!calculated || derivedKey == nullptr) { + log_e("Error: PBKDF2-HMAC not calculated or no output buffer provided."); + return ""; + } + + String result = ""; + for (size_t i = 0; i < derivedKeyLen; i++) { + if (derivedKey[i] < 0x10) { + result += "0"; + } + result += String(derivedKey[i], HEX); + } + return result; +} + +// PBKDF2 specific methods +void PBKDF2_HMACBuilder::setPassword(const uint8_t* password, size_t len) { + if (this->password != nullptr) { + delete[] this->password; + } + this->password = new uint8_t[len]; + memcpy(this->password, password, len); + this->passwordLen = len; + calculated = false; +} + +void PBKDF2_HMACBuilder::setPassword(const char* password) { + setPassword((const uint8_t*)password, strlen(password)); +} + +void PBKDF2_HMACBuilder::setPassword(String password) { + setPassword((const uint8_t*)password.c_str(), password.length()); +} + +void PBKDF2_HMACBuilder::setSalt(const uint8_t* salt, size_t len) { + if (this->salt != nullptr) { + delete[] this->salt; + } + this->salt = new uint8_t[len]; + memcpy(this->salt, salt, len); + this->saltLen = len; + calculated = false; +} + +void PBKDF2_HMACBuilder::setSalt(const char* salt) { + setSalt((const uint8_t*)salt, strlen(salt)); +} + +void PBKDF2_HMACBuilder::setSalt(String salt) { + setSalt((const uint8_t*)salt.c_str(), salt.length()); +} + +void PBKDF2_HMACBuilder::setIterations(uint32_t iterations) { + this->iterations = iterations; +} + +void PBKDF2_HMACBuilder::setHashAlgorithm(HashBuilder* hash) { + // Set the hash algorithm to use for the HMAC + // Note: We don't delete hashBuilder here as it might be owned by the caller + // The caller is responsible for managing the hashBuilder lifetime + hashBuilder = hash; + hashSize = hashBuilder->getHashSize(); +} + +void PBKDF2_HMACBuilder::pbkdf2_hmac(const uint8_t* password, size_t passwordLen, + const uint8_t* salt, size_t saltLen, + uint32_t iterations, uint8_t* output, size_t outputLen) { + uint8_t u1[64]; // Large enough for any hash + uint8_t u2[64]; + uint8_t saltWithBlock[256]; // Salt + block number + uint8_t block[64]; + + size_t blocks = (outputLen + hashSize - 1) / hashSize; + + for (size_t i = 1; i <= blocks; i++) { + // Prepare salt || INT(i) + memcpy(saltWithBlock, salt, saltLen); + saltWithBlock[saltLen] = (i >> 24) & 0xFF; + saltWithBlock[saltLen + 1] = (i >> 16) & 0xFF; + saltWithBlock[saltLen + 2] = (i >> 8) & 0xFF; + saltWithBlock[saltLen + 3] = i & 0xFF; + + // U1 = HMAC(password, salt || INT(i)) + hmac(password, passwordLen, saltWithBlock, saltLen + 4, u1); + memcpy(block, u1, hashSize); + + // U2 = HMAC(password, U1) + for (uint32_t j = 1; j < iterations; j++) { + hmac(password, passwordLen, u1, hashSize, u2); + memcpy(u1, u2, hashSize); + + // XOR with previous result + for (size_t k = 0; k < hashSize; k++) { + block[k] ^= u1[k]; + } + } + + // Copy block to output + size_t copyLen = (i == blocks) ? (outputLen - (i - 1) * hashSize) : hashSize; + memcpy(output + (i - 1) * hashSize, block, copyLen); + } +} diff --git a/libraries/Hash/src/PBKDF2_HMACBuilder.h b/libraries/Hash/src/PBKDF2_HMACBuilder.h new file mode 100644 index 00000000000..e02fc67c8bf --- /dev/null +++ b/libraries/Hash/src/PBKDF2_HMACBuilder.h @@ -0,0 +1,73 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PBKDF2_HMACBuilder_h +#define PBKDF2_HMACBuilder_h + +#include +#include +#include "HashBuilder.h" + +class PBKDF2_HMACBuilder : public HashBuilder { +private: + HashBuilder* hashBuilder; + size_t hashSize; + uint32_t iterations; + + // Password and salt storage + uint8_t* password; + size_t passwordLen; + uint8_t* salt; + size_t saltLen; + + // Output storage + uint8_t* derivedKey; + size_t derivedKeyLen; + bool calculated; + + void hmac(const uint8_t* key, size_t keyLen, const uint8_t* data, size_t dataLen, uint8_t* output); + void pbkdf2_hmac(const uint8_t* password, size_t passwordLen, + const uint8_t* salt, size_t saltLen, + uint32_t iterations, uint8_t* output, size_t outputLen); + void clearData(); + +public: + using HashBuilder::add; + + // Constructor takes a hash builder instance + PBKDF2_HMACBuilder(HashBuilder* hash, String password = "", String salt = "", uint32_t iterations = 10000); + ~PBKDF2_HMACBuilder(); + + // Standard HashBuilder interface + void begin() override; + void add(const uint8_t *data, size_t len) override; + bool addStream(Stream &stream, const size_t maxLen) override; + void calculate() override; + void getBytes(uint8_t *output) override; + void getChars(char *output) override; + String toString() override; + size_t getHashSize() const override { return derivedKeyLen; } + + // PBKDF2 specific methods + void setPassword(const uint8_t* password, size_t len); + void setPassword(const char* password); + void setPassword(String password); + void setSalt(const uint8_t* salt, size_t len); + void setSalt(const char* salt); + void setSalt(String salt); + void setIterations(uint32_t iterations); + void setHashAlgorithm(HashBuilder* hash); +}; + +#endif diff --git a/cores/esp32/SHA1Builder.cpp b/libraries/Hash/src/SHA1Builder.cpp similarity index 88% rename from cores/esp32/SHA1Builder.cpp rename to libraries/Hash/src/SHA1Builder.cpp index 6bbe3ca83e0..bc52b23a1ad 100644 --- a/cores/esp32/SHA1Builder.cpp +++ b/libraries/Hash/src/SHA1Builder.cpp @@ -1,27 +1,21 @@ -/* - * FIPS-180-1 compliant SHA-1 implementation - * - * Copyright (C) 2006-2015, ARM Limited, All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * This file is part of mbed TLS (https://tls.mbed.org) - * Modified for esp32 by Lucas Saavedra Vaz on 11 Jan 2024 - */ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Based on mbed TLS (https://tls.mbed.org) #include -#include +#include "SHA1Builder.h" // 32-bit integer manipulation macros (big endian) @@ -251,17 +245,6 @@ void SHA1Builder::add(const uint8_t *data, size_t len) { } } -void SHA1Builder::addHexString(const char *data) { - uint16_t len = strlen(data); - uint8_t *tmp = (uint8_t *)malloc(len / 2); - if (tmp == NULL) { - return; - } - hex2bytes(tmp, len / 2, data); - add(tmp, len / 2); - free(tmp); -} - bool SHA1Builder::addStream(Stream &stream, const size_t maxLen) { const int buf_size = 512; int maxLengthLeft = maxLen; diff --git a/cores/esp32/SHA1Builder.h b/libraries/Hash/src/SHA1Builder.h similarity index 94% rename from cores/esp32/SHA1Builder.h rename to libraries/Hash/src/SHA1Builder.h index b587e4fdc96..203f83a77df 100644 --- a/cores/esp32/SHA1Builder.h +++ b/libraries/Hash/src/SHA1Builder.h @@ -32,19 +32,16 @@ class SHA1Builder : public HashBuilder { void process(const uint8_t *data); public: - void begin() override; - using HashBuilder::add; - void add(const uint8_t *data, size_t len) override; - - using HashBuilder::addHexString; - void addHexString(const char *data) override; + void begin() override; + void add(const uint8_t *data, size_t len) override; bool addStream(Stream &stream, const size_t maxLen) override; void calculate() override; void getBytes(uint8_t *output) override; void getChars(char *output) override; String toString() override; + size_t getHashSize() const override { return SHA1_HASH_SIZE; } }; #endif diff --git a/libraries/Hash/src/SHA2Builder.cpp b/libraries/Hash/src/SHA2Builder.cpp new file mode 100644 index 00000000000..89725e5f920 --- /dev/null +++ b/libraries/Hash/src/SHA2Builder.cpp @@ -0,0 +1,434 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "esp32-hal-log.h" +#include "SHA2Builder.h" + +// SHA-256 constants +static const uint32_t sha256_k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +// SHA-512 constants +static const uint64_t sha512_k[80] = { + 0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, 0xe9b5dba58189dbbcULL, + 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL, + 0xd807aa98a3030242ULL, 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL, + 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, 0xc19bf174cf692694ULL, + 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL, + 0x2de92c6f592b0275ULL, 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL, + 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, 0xbf597fc7beef0ee4ULL, + 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL, + 0x27b70a8546d22ffcULL, 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL, + 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, 0x92722c851482353bULL, + 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL, + 0xd192e819d6ef5218ULL, 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL, + 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, 0x34b0bcb5e19b48a8ULL, + 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL, + 0x748f82ee5defb2fcULL, 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL, + 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, 0xc67178f2e372532bULL, + 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL, + 0x06f067aa72176fbaULL, 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL, + 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, 0x431d67c49c100d4cULL, + 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL +}; + +// Macros for bit manipulation +#define ROTR32(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define ROTR64(x, n) (((x) >> (n)) | ((x) << (64 - (n)))) +#define CH32(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define CH64(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ32(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define MAJ64(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EP0_32(x) (ROTR32(x, 2) ^ ROTR32(x, 13) ^ ROTR32(x, 22)) +#define EP0_64(x) (ROTR64(x, 28) ^ ROTR64(x, 34) ^ ROTR64(x, 39)) +#define EP1_32(x) (ROTR32(x, 6) ^ ROTR32(x, 11) ^ ROTR32(x, 25)) +#define EP1_64(x) (ROTR64(x, 14) ^ ROTR64(x, 18) ^ ROTR64(x, 41)) +#define SIG0_32(x) (ROTR32(x, 7) ^ ROTR32(x, 18) ^ ((x) >> 3)) +#define SIG0_64(x) (ROTR64(x, 1) ^ ROTR64(x, 8) ^ ((x) >> 7)) +#define SIG1_32(x) (ROTR32(x, 17) ^ ROTR32(x, 19) ^ ((x) >> 10)) +#define SIG1_64(x) (ROTR64(x, 19) ^ ROTR64(x, 61) ^ ((x) >> 6)) + +// Byte order conversion +#define BYTESWAP32(x) ((((x) & 0xFF000000) >> 24) | (((x) & 0x00FF0000) >> 8) | (((x) & 0x0000FF00) << 8) | (((x) & 0x000000FF) << 24)) +#define BYTESWAP64(x) (((uint64_t)BYTESWAP32((uint32_t)((x) >> 32))) | (((uint64_t)BYTESWAP32((uint32_t)(x))) << 32)) + +// Constructor +SHA2Builder::SHA2Builder(size_t hash_size) + : hash_size(hash_size), buffer_size(0), finalized(false), total_length(0) { + // Determine block size and algorithm family + if (hash_size == SHA2_224_HASH_SIZE || hash_size == SHA2_256_HASH_SIZE) { + block_size = SHA2_256_BLOCK_SIZE; + is_sha512 = false; + } else if (hash_size == SHA2_384_HASH_SIZE || hash_size == SHA2_512_HASH_SIZE) { + block_size = SHA2_512_BLOCK_SIZE; + is_sha512 = true; + } else { + log_e("Invalid hash size: %d", hash_size); + block_size = 0; + is_sha512 = false; + } +} + +// Initialize the hash computation +void SHA2Builder::begin() { + // Clear the state and buffer + memset(state_32, 0, sizeof(state_32)); + memset(state_64, 0, sizeof(state_64)); + memset(buffer, 0, sizeof(buffer)); + buffer_size = 0; + finalized = false; + total_length = 0; + + // Initialize state based on algorithm + if (!is_sha512) { + // SHA-224/256 initial values + if (hash_size == SHA2_224_HASH_SIZE) { + // SHA-224 initial values + state_32[0] = 0xc1059ed8; + state_32[1] = 0x367cd507; + state_32[2] = 0x3070dd17; + state_32[3] = 0xf70e5939; + state_32[4] = 0xffc00b31; + state_32[5] = 0x68581511; + state_32[6] = 0x64f98fa7; + state_32[7] = 0xbefa4fa4; + } else { + // SHA-256 initial values + state_32[0] = 0x6a09e667; + state_32[1] = 0xbb67ae85; + state_32[2] = 0x3c6ef372; + state_32[3] = 0xa54ff53a; + state_32[4] = 0x510e527f; + state_32[5] = 0x9b05688c; + state_32[6] = 0x1f83d9ab; + state_32[7] = 0x5be0cd19; + } + } else { + // SHA-384/512 initial values + if (hash_size == SHA2_384_HASH_SIZE) { + // SHA-384 initial values + state_64[0] = 0xcbbb9d5dc1059ed8ULL; + state_64[1] = 0x629a292a367cd507ULL; + state_64[2] = 0x9159015a3070dd17ULL; + state_64[3] = 0x152fecd8f70e5939ULL; + state_64[4] = 0x67332667ffc00b31ULL; + state_64[5] = 0x8eb44a8768581511ULL; + state_64[6] = 0xdb0c2e0d64f98fa7ULL; + state_64[7] = 0x47b5481dbefa4fa4ULL; + } else { + // SHA-512 initial values + state_64[0] = 0x6a09e667f3bcc908ULL; + state_64[1] = 0xbb67ae8584caa73bULL; + state_64[2] = 0x3c6ef372fe94f82bULL; + state_64[3] = 0xa54ff53a5f1d36f1ULL; + state_64[4] = 0x510e527fade682d1ULL; + state_64[5] = 0x9b05688c2b3e6c1fULL; + state_64[6] = 0x1f83d9abfb41bd6bULL; + state_64[7] = 0x5be0cd19137e2179ULL; + } + } +} + +// Process a block for SHA-256 +void SHA2Builder::process_block_sha256(const uint8_t *data) { + uint32_t w[64]; + uint32_t a, b, c, d, e, f, g, h; + uint32_t t1, t2; + + // Prepare message schedule + for (int i = 0; i < 16; i++) { + w[i] = BYTESWAP32(((uint32_t*)data)[i]); + } + for (int i = 16; i < 64; i++) { + w[i] = SIG1_32(w[i-2]) + w[i-7] + SIG0_32(w[i-15]) + w[i-16]; + } + + // Initialize working variables + a = state_32[0]; + b = state_32[1]; + c = state_32[2]; + d = state_32[3]; + e = state_32[4]; + f = state_32[5]; + g = state_32[6]; + h = state_32[7]; + + // Main loop + for (int i = 0; i < 64; i++) { + t1 = h + EP1_32(e) + CH32(e, f, g) + sha256_k[i] + w[i]; + t2 = EP0_32(a) + MAJ32(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + // Add the compressed chunk to the current hash value + state_32[0] += a; + state_32[1] += b; + state_32[2] += c; + state_32[3] += d; + state_32[4] += e; + state_32[5] += f; + state_32[6] += g; + state_32[7] += h; +} + +// Process a block for SHA-512 +void SHA2Builder::process_block_sha512(const uint8_t *data) { + uint64_t w[80]; + uint64_t a, b, c, d, e, f, g, h; + uint64_t t1, t2; + + // Prepare message schedule + for (int i = 0; i < 16; i++) { + w[i] = BYTESWAP64(((uint64_t*)data)[i]); + } + for (int i = 16; i < 80; i++) { + w[i] = SIG1_64(w[i-2]) + w[i-7] + SIG0_64(w[i-15]) + w[i-16]; + } + + // Initialize working variables + a = state_64[0]; + b = state_64[1]; + c = state_64[2]; + d = state_64[3]; + e = state_64[4]; + f = state_64[5]; + g = state_64[6]; + h = state_64[7]; + + // Main loop + for (int i = 0; i < 80; i++) { + t1 = h + EP1_64(e) + CH64(e, f, g) + sha512_k[i] + w[i]; + t2 = EP0_64(a) + MAJ64(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + // Add the compressed chunk to the current hash value + state_64[0] += a; + state_64[1] += b; + state_64[2] += c; + state_64[3] += d; + state_64[4] += e; + state_64[5] += f; + state_64[6] += g; + state_64[7] += h; +} + +// Add data to the hash computation +void SHA2Builder::add(const uint8_t *data, size_t len) { + if (finalized || len == 0) { + return; + } + + total_length += len; + size_t offset = 0; + + // Process any buffered data first + if (buffer_size > 0) { + size_t to_copy = std::min(len, block_size - buffer_size); + memcpy(buffer + buffer_size, data, to_copy); + buffer_size += to_copy; + offset += to_copy; + + if (buffer_size == block_size) { + if (is_sha512) { + process_block_sha512(buffer); + } else { + process_block_sha256(buffer); + } + buffer_size = 0; + } + } + + // Process full blocks + while (offset + block_size <= len) { + if (is_sha512) { + process_block_sha512(data + offset); + } else { + process_block_sha256(data + offset); + } + offset += block_size; + } + + // Buffer remaining data + if (offset < len) { + memcpy(buffer, data + offset, len - offset); + buffer_size = len - offset; + } +} + +// Add data from a stream +bool SHA2Builder::addStream(Stream &stream, const size_t maxLen) { + const int buf_size = 512; + int maxLengthLeft = maxLen; + uint8_t *buf = (uint8_t *)malloc(buf_size); + + if (!buf) { + return false; + } + + int bytesAvailable = stream.available(); + while ((bytesAvailable > 0) && (maxLengthLeft > 0)) { + // Determine number of bytes to read + int readBytes = bytesAvailable; + if (readBytes > maxLengthLeft) { + readBytes = maxLengthLeft; + } + if (readBytes > buf_size) { + readBytes = buf_size; + } + + // Read data and check if we got something + int numBytesRead = stream.readBytes(buf, readBytes); + if (numBytesRead < 1) { + free(buf); + return false; + } + + // Update SHA2 with buffer payload + add(buf, numBytesRead); + + // Update available number of bytes + maxLengthLeft -= numBytesRead; + bytesAvailable = stream.available(); + } + free(buf); + return true; +} + +// Pad the input according to SHA2 specification +void SHA2Builder::pad() { + // Calculate the number of bytes we have + uint64_t bit_length = total_length * 8; + + // Add the bit '1' to the message + buffer[buffer_size++] = 0x80; + + // Pad with zeros until we have enough space for the length + while (buffer_size + 8 > block_size) { + if (buffer_size < block_size) { + buffer[buffer_size++] = 0x00; + } else { + // Process the block + if (is_sha512) { + process_block_sha512(buffer); + } else { + process_block_sha256(buffer); + } + buffer_size = 0; + } + } + + // Pad with zeros to make room for the length + while (buffer_size + 8 < block_size) { + buffer[buffer_size++] = 0x00; + } + + // Add the length in bits + if (is_sha512) { + // For SHA-512, length is 128 bits (16 bytes) + // We only use the lower 64 bits for now + for (int i = 0; i < 8; i++) { + buffer[block_size - 8 + i] = (uint8_t)(bit_length >> (56 - i * 8)); + } + // Set the upper 64 bits to 0 (for SHA-384/512, length is limited to 2^128-1) + for (int i = 0; i < 8; i++) { + buffer[block_size - 16 + i] = 0x00; + } + } else { + // For SHA-256, length is 64 bits (8 bytes) + for (int i = 0; i < 8; i++) { + buffer[block_size - 8 + i] = (uint8_t)(bit_length >> (56 - i * 8)); + } + } +} + +// Finalize the hash computation +void SHA2Builder::calculate() { + if (finalized) { + return; + } + + // Pad the input + pad(); + + // Process the final block + if (is_sha512) { + process_block_sha512(buffer); + } else { + process_block_sha256(buffer); + } + + // Extract bytes from the state + if (is_sha512) { + for (size_t i = 0; i < hash_size; i++) { + hash[i] = (uint8_t)(state_64[i >> 3] >> (56 - ((i & 0x7) << 3))); + } + } else { + for (size_t i = 0; i < hash_size; i++) { + hash[i] = (uint8_t)(state_32[i >> 2] >> (24 - ((i & 0x3) << 3))); + } + } + + finalized = true; +} + +// Get the hash as bytes +void SHA2Builder::getBytes(uint8_t *output) { + memcpy(output, hash, hash_size); +} + +// Get the hash as hex string +void SHA2Builder::getChars(char *output) { + bytes2hex(output, hash_size * 2 + 1, hash, hash_size); +} + +// Get the hash as String +String SHA2Builder::toString() { + char out[(hash_size * 2) + 1]; + getChars(out); + return String(out); +} diff --git a/libraries/Hash/src/SHA2Builder.h b/libraries/Hash/src/SHA2Builder.h new file mode 100644 index 00000000000..228d58a157c --- /dev/null +++ b/libraries/Hash/src/SHA2Builder.h @@ -0,0 +1,96 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SHA2Builder_h +#define SHA2Builder_h + +#include +#include + +#include "HashBuilder.h" + +// SHA2 constants +#define SHA2_224_HASH_SIZE 28 +#define SHA2_256_HASH_SIZE 32 +#define SHA2_384_HASH_SIZE 48 +#define SHA2_512_HASH_SIZE 64 + +#define SHA2_224_BLOCK_SIZE 64 +#define SHA2_256_BLOCK_SIZE 64 +#define SHA2_384_BLOCK_SIZE 128 +#define SHA2_512_BLOCK_SIZE 128 + +// SHA2 state sizes (in 32-bit words for SHA-224/256, 64-bit words for SHA-384/512) +#define SHA2_224_STATE_SIZE 8 +#define SHA2_256_STATE_SIZE 8 +#define SHA2_384_STATE_SIZE 8 +#define SHA2_512_STATE_SIZE 8 + +class SHA2Builder : public HashBuilder { +protected: + uint32_t state_32[8]; // SHA-224/256 state (256 bits) + uint64_t state_64[8]; // SHA-384/512 state (512 bits) + uint8_t buffer[128]; // Input buffer (max block size) + size_t block_size; // Block size + size_t hash_size; // Output hash size + size_t buffer_size; // Current buffer size + bool finalized; // Whether hash has been finalized + bool is_sha512; // Whether using SHA-512 family + uint8_t hash[64]; // Hash result + uint64_t total_length; // Total length of input data + + void process_block_sha256(const uint8_t *data); + void process_block_sha512(const uint8_t *data); + void pad(); + +public: + using HashBuilder::add; + + SHA2Builder(size_t hash_size = SHA2_256_HASH_SIZE); + virtual ~SHA2Builder() {} + + void begin() override; + void add(const uint8_t *data, size_t len) override; + bool addStream(Stream &stream, const size_t maxLen) override; + void calculate() override; + void getBytes(uint8_t *output) override; + void getChars(char *output) override; + String toString() override; + + size_t getHashSize() const override { + return hash_size; + } +}; + +class SHA224Builder : public SHA2Builder { +public: + SHA224Builder() : SHA2Builder(SHA2_224_HASH_SIZE) {} +}; + +class SHA256Builder : public SHA2Builder { +public: + SHA256Builder() : SHA2Builder(SHA2_256_HASH_SIZE) {} +}; + +class SHA384Builder : public SHA2Builder { +public: + SHA384Builder() : SHA2Builder(SHA2_384_HASH_SIZE) {} +}; + +class SHA512Builder : public SHA2Builder { +public: + SHA512Builder() : SHA2Builder(SHA2_512_HASH_SIZE) {} +}; + +#endif diff --git a/libraries/Hash/src/SHA3Builder.cpp b/libraries/Hash/src/SHA3Builder.cpp new file mode 100644 index 00000000000..ef05f27fa37 --- /dev/null +++ b/libraries/Hash/src/SHA3Builder.cpp @@ -0,0 +1,264 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "esp32-hal-log.h" +#include "SHA3Builder.h" + +// Keccak round constants +static const uint64_t keccak_round_constants[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808AULL, + 0x8000000080008000ULL, 0x000000000000808BULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008AULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000AULL, + 0x000000008000808BULL, 0x800000000000008BULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800AULL, 0x800000008000000AULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL +}; + +// Rho rotation constants +static const uint32_t rho[6] = { + 0x3f022425, 0x1c143a09, 0x2c3d3615, 0x27191713, 0x312b382e, 0x3e030832 +}; + +// Pi permutation constants +static const uint32_t pi[6] = { + 0x110b070a, 0x10050312, 0x04181508, 0x0d13170f, 0x0e14020c, 0x01060916 +}; + +// Macros for bit manipulation +#define ROTR64(x, y) (((x) << (64U - (y))) | ((x) >> (y))) + +// Keccak-f permutation +void SHA3Builder::keccak_f(uint64_t state[25]) { + uint64_t lane[5]; + uint64_t *s = state; + int i; + + for (int round = 0; round < 24; round++) { + uint64_t t; + + // Theta step + for (i = 0; i < 5; i++) { + lane[i] = s[i] ^ s[i + 5] ^ s[i + 10] ^ s[i + 15] ^ s[i + 20]; + } + for (i = 0; i < 5; i++) { + t = lane[(i + 4) % 5] ^ ROTR64(lane[(i + 1) % 5], 63); + s[i] ^= t; s[i + 5] ^= t; s[i + 10] ^= t; s[i + 15] ^= t; s[i + 20] ^= t; + } + + // Rho step + for (i = 1; i < 25; i += 4) { + uint32_t r = rho[(i - 1) >> 2]; + for (int j = i; j < i + 4; j++) { + uint8_t r8 = (uint8_t) (r >> 24); + r <<= 8; + s[j] = ROTR64(s[j], r8); + } + } + + // Pi step + t = s[1]; + for (i = 0; i < 24; i += 4) { + uint32_t p = pi[i >> 2]; + for (unsigned j = 0; j < 4; j++) { + uint64_t tmp = s[p & 0xff]; + s[p & 0xff] = t; + t = tmp; + p >>= 8; + } + } + + // Chi step + for (i = 0; i <= 20; i += 5) { + lane[0] = s[i]; lane[1] = s[i + 1]; lane[2] = s[i + 2]; + lane[3] = s[i + 3]; lane[4] = s[i + 4]; + s[i + 0] ^= (~lane[1]) & lane[2]; + s[i + 1] ^= (~lane[2]) & lane[3]; + s[i + 2] ^= (~lane[3]) & lane[4]; + s[i + 3] ^= (~lane[4]) & lane[0]; + s[i + 4] ^= (~lane[0]) & lane[1]; + } + + // Iota step + s[0] ^= keccak_round_constants[round]; + } +} + +// Process a block of data +void SHA3Builder::process_block(const uint8_t *data) { + // XOR the data into the state using byte-level operations + for (size_t i = 0; i < rate; i++) { + size_t state_idx = i >> 3; // i / 8 + size_t bit_offset = (i & 0x7) << 3; // (i % 8) * 8 + uint64_t byte_val = (uint64_t)data[i] << bit_offset; + state[state_idx] ^= byte_val; + } + + // Apply Keccak-f permutation + keccak_f(state); +} + +// Pad the input according to SHA3 specification +void SHA3Builder::pad() { + // Clear the buffer first + memset(buffer + buffer_size, 0, rate - buffer_size); + + // Add the domain separator (0x06) at the current position + buffer[buffer_size] = 0x06; + + // Set the last byte to indicate the end (0x80) + buffer[rate - 1] = 0x80; +} + +// Constructor +SHA3Builder::SHA3Builder(size_t hash_size) + : hash_size(hash_size), buffer_size(0), finalized(false) { + // Calculate rate based on hash size + if (hash_size == SHA3_224_HASH_SIZE) { + rate = SHA3_224_RATE; + } else if (hash_size == SHA3_256_HASH_SIZE) { + rate = SHA3_256_RATE; + } else if (hash_size == SHA3_384_HASH_SIZE) { + rate = SHA3_384_RATE; + } else if (hash_size == SHA3_512_HASH_SIZE) { + rate = SHA3_512_RATE; + } else { + log_e("Invalid hash size: %d", hash_size); + rate = 0; // Invalid hash size + } +} + +// Initialize the hash computation +void SHA3Builder::begin() { + // Clear the state + memset(state, 0, sizeof(state)); + memset(buffer, 0, sizeof(buffer)); + buffer_size = 0; + finalized = false; +} + +// Add data to the hash computation +void SHA3Builder::add(const uint8_t *data, size_t len) { + if (finalized || len == 0) { + return; + } + + size_t offset = 0; + + // Process any buffered data first + if (buffer_size > 0) { + size_t to_copy = std::min(len, rate - buffer_size); + memcpy(buffer + buffer_size, data, to_copy); + buffer_size += to_copy; + offset += to_copy; + + if (buffer_size == rate) { + process_block(buffer); + buffer_size = 0; + } + } + + // Process full blocks + while (offset + rate <= len) { + process_block(data + offset); + offset += rate; + } + + // Buffer remaining data + if (offset < len) { + memcpy(buffer, data + offset, len - offset); + buffer_size = len - offset; + } +} + +// Add data from a stream +bool SHA3Builder::addStream(Stream &stream, const size_t maxLen) { + const int buf_size = 512; + int maxLengthLeft = maxLen; + uint8_t *buf = (uint8_t *)malloc(buf_size); + + if (!buf) { + return false; + } + + int bytesAvailable = stream.available(); + while ((bytesAvailable > 0) && (maxLengthLeft > 0)) { + // Determine number of bytes to read + int readBytes = bytesAvailable; + if (readBytes > maxLengthLeft) { + readBytes = maxLengthLeft; + } + if (readBytes > buf_size) { + readBytes = buf_size; + } + + // Read data and check if we got something + int numBytesRead = stream.readBytes(buf, readBytes); + if (numBytesRead < 1) { + free(buf); + return false; + } + + // Update SHA3 with buffer payload + add(buf, numBytesRead); + + // Update available number of bytes + maxLengthLeft -= numBytesRead; + bytesAvailable = stream.available(); + } + free(buf); + return true; +} + +// Finalize the hash computation +void SHA3Builder::calculate() { + if (finalized) { + return; + } + + // Pad the input + pad(); + + // Process the final block + process_block(buffer); + + // Extract bytes from the state + for (size_t i = 0; i < hash_size; i++) { + size_t state_idx = i >> 3; // i / 8 + size_t bit_offset = (i & 0x7) << 3; // (i % 8) * 8 + hash[i] = (uint8_t)(state[state_idx] >> bit_offset); + } + + finalized = true; +} + +// Get the hash as bytes +void SHA3Builder::getBytes(uint8_t *output) { + memcpy(output, hash, hash_size); +} + +// Get the hash as hex string +void SHA3Builder::getChars(char *output) { + bytes2hex(output, hash_size * 2 + 1, hash, hash_size); +} + +// Get the hash as String +String SHA3Builder::toString() { + char out[(hash_size * 2) + 1]; + getChars(out); + return String(out); +} diff --git a/libraries/Hash/src/SHA3Builder.h b/libraries/Hash/src/SHA3Builder.h new file mode 100644 index 00000000000..67837d05076 --- /dev/null +++ b/libraries/Hash/src/SHA3Builder.h @@ -0,0 +1,89 @@ +// Copyright 2025 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SHA3Builder_h +#define SHA3Builder_h + +#include +#include + +#include "HashBuilder.h" + +// SHA3 constants +#define SHA3_224_HASH_SIZE 28 +#define SHA3_256_HASH_SIZE 32 +#define SHA3_384_HASH_SIZE 48 +#define SHA3_512_HASH_SIZE 64 + +#define SHA3_224_RATE 144 +#define SHA3_256_RATE 136 +#define SHA3_384_RATE 104 +#define SHA3_512_RATE 72 + +#define SHA3_STATE_SIZE 200 // 1600 bits = 200 bytes + +class SHA3Builder : public HashBuilder { +protected: + uint64_t state[25]; // SHA3 state (1600 bits) + uint8_t buffer[200]; // Input buffer + size_t rate; // Rate (block size) + size_t hash_size; // Output hash size + size_t buffer_size; // Current buffer size + bool finalized; // Whether hash has been finalized + uint8_t hash[64]; // Hash result + + void keccak_f(uint64_t state[25]); + void process_block(const uint8_t *data); + void pad(); + +public: + using HashBuilder::add; + + SHA3Builder(size_t hash_size = SHA3_256_HASH_SIZE); + virtual ~SHA3Builder() {} + + void begin() override; + void add(const uint8_t *data, size_t len) override; + bool addStream(Stream &stream, const size_t maxLen) override; + void calculate() override; + void getBytes(uint8_t *output) override; + void getChars(char *output) override; + String toString() override; + + size_t getHashSize() const override { + return hash_size; + } +}; + +class SHA3_224Builder : public SHA3Builder { +public: + SHA3_224Builder() : SHA3Builder(SHA3_224_HASH_SIZE) {} +}; + +class SHA3_256Builder : public SHA3Builder { +public: + SHA3_256Builder() : SHA3Builder(SHA3_256_HASH_SIZE) {} +}; + +class SHA3_384Builder : public SHA3Builder { +public: + SHA3_384Builder() : SHA3Builder(SHA3_384_HASH_SIZE) {} +}; + +class SHA3_512Builder : public SHA3Builder { +public: + SHA3_512Builder() : SHA3Builder(SHA3_512_HASH_SIZE) {} +}; + +#endif diff --git a/libraries/WiFi/examples/WiFiUDPClient/udp_server.py b/libraries/WiFi/examples/WiFiUDPClient/udp_server.py index c70a6fe2c37..4e77c3cfeaf 100644 --- a/libraries/WiFi/examples/WiFiUDPClient/udp_server.py +++ b/libraries/WiFi/examples/WiFiUDPClient/udp_server.py @@ -2,6 +2,93 @@ # for messages from the ESP32 board and prints them import socket import sys +import subprocess +import platform + +def get_interface_ips(): + """Get all available interface IP addresses""" + interface_ips = [] + + # Try using system commands to get interface IPs + system = platform.system().lower() + + try: + if system == "darwin" or system == "linux": + # Use 'ifconfig' on macOS/Linux + result = subprocess.run(['ifconfig'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + lines = result.stdout.split('\n') + for line in lines: + if 'inet ' in line and '127.0.0.1' not in line: + # Extract IP address from ifconfig output + parts = line.strip().split() + for i, part in enumerate(parts): + if part == 'inet': + if i + 1 < len(parts): + ip = parts[i + 1] + if ip not in interface_ips and ip != '127.0.0.1': + interface_ips.append(ip) + break + elif system == "windows": + # Use 'ipconfig' on Windows + result = subprocess.run(['ipconfig'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + lines = result.stdout.split('\n') + for line in lines: + if 'IPv4 Address' in line and '127.0.0.1' not in line: + # Extract IP address from ipconfig output + if ':' in line: + ip = line.split(':')[1].strip() + if ip not in interface_ips and ip != '127.0.0.1': + interface_ips.append(ip) + except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError): + print("Error: Failed to get interface IPs using system commands") + print("Trying fallback methods...") + + # Fallback: try to get IPs using socket methods + if not interface_ips: + try: + # Get all IP addresses associated with the hostname + hostname = socket.gethostname() + ip_list = socket.gethostbyname_ex(hostname)[2] + for ip in ip_list: + if ip not in interface_ips and ip != '127.0.0.1': + interface_ips.append(ip) + except socket.gaierror: + print("Error: Failed to get interface IPs using sockets") + + # Fail if no interfaces found + if not interface_ips: + print("Error: No network interfaces found. Please check your network configuration.") + sys.exit(1) + + return interface_ips + +def select_interface(interface_ips): + """Ask user to select which interface to bind to""" + if len(interface_ips) == 1: + print(f"Using interface: {interface_ips[0]}") + return interface_ips[0] + + print("Multiple network interfaces detected:") + for i, ip in enumerate(interface_ips, 1): + print(f" {i}. {ip}") + + while True: + try: + choice = input(f"Select interface (1-{len(interface_ips)}): ").strip() + choice_idx = int(choice) - 1 + if 0 <= choice_idx < len(interface_ips): + selected_ip = interface_ips[choice_idx] + print(f"Selected interface: {selected_ip}") + return selected_ip + else: + print(f"Please enter a number between 1 and {len(interface_ips)}") + except ValueError: + print("Please enter a valid number") + except KeyboardInterrupt: + print("\nExiting...") + sys.exit(1) try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -10,15 +97,17 @@ print("Failed to create socket. Error Code : " + str(msg[0]) + " Message " + msg[1]) sys.exit() +# Get available interfaces and let user choose +interface_ips = get_interface_ips() +selected_ip = select_interface(interface_ips) + try: - s.bind(("", 3333)) + s.bind((selected_ip, 3333)) except socket.error as msg: print("Bind failed. Error: " + str(msg[0]) + ": " + msg[1]) sys.exit() -print("Server listening") - -print("Server listening") +print(f"Server listening on {selected_ip}:3333") while 1: d = s.recvfrom(1024) diff --git a/tools/espota.exe b/tools/espota.exe index 8bee0c9036f..bf1ae1175da 100644 Binary files a/tools/espota.exe and b/tools/espota.exe differ diff --git a/tools/espota.py b/tools/espota.py index fd95955a2f3..e1e773195ec 100755 --- a/tools/espota.py +++ b/tools/espota.py @@ -54,7 +54,6 @@ # Constants PROGRESS_BAR_LENGTH = 60 - # update_progress(): Displays or updates a console progress bar def update_progress(progress): if PROGRESS: @@ -94,7 +93,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, return 1 content_size = os.path.getsize(filename) - file_md5 = hashlib.md5(open(filename, "rb").read()).hexdigest() + with open(filename, "rb") as f: + file_md5 = hashlib.md5(f.read()).hexdigest() logging.info("Upload size: %d", content_size) message = "%d %d %d %s\n" % (command, local_port, content_size, file_md5) @@ -118,7 +118,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, return 1 sock2.settimeout(TIMEOUT) try: - data = sock2.recv(37).decode() + data = sock2.recv(69).decode() # "AUTH " + 64-char SHA3-256 nonce break except: # noqa: E722 sys.stderr.write(".") @@ -132,18 +132,32 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, if data != "OK": if data.startswith("AUTH"): nonce = data.split()[1] + + # Generate client nonce (cnonce) cnonce_text = "%s%u%s%s" % (filename, content_size, file_md5, remote_addr) - cnonce = hashlib.md5(cnonce_text.encode()).hexdigest() - passmd5 = hashlib.md5(password.encode()).hexdigest() - result_text = "%s:%s:%s" % (passmd5, nonce, cnonce) - result = hashlib.md5(result_text.encode()).hexdigest() + cnonce = hashlib.sha256(cnonce_text.encode()).hexdigest() + + # PBKDF2-HMAC-SHA256 challenge/response protocol + # The ESP32 stores the password as SHA256 hash, so we need to hash the password first + # 1. Hash the password with SHA256 (to match ESP32 storage) + password_hash = hashlib.sha256(password.encode()).hexdigest() + + # 2. Derive key using PBKDF2-HMAC-SHA256 with the password hash + salt = nonce + ":" + cnonce + derived_key = hashlib.pbkdf2_hmac('sha256', password_hash.encode(), salt.encode(), 10000) + derived_key_hex = derived_key.hex() + + # 3. Create challenge response + challenge = derived_key_hex + ":" + nonce + ":" + cnonce + response = hashlib.sha256(challenge.encode()).hexdigest() + sys.stderr.write("Authenticating...") sys.stderr.flush() - message = "%d %s %s\n" % (AUTH, cnonce, result) + message = "%d %s %s\n" % (AUTH, cnonce, response) sock2.sendto(message.encode(), remote_address) sock2.settimeout(10) try: - data = sock2.recv(32).decode() + data = sock2.recv(64).decode() # SHA256 produces 64 character response except: # noqa: E722 sys.stderr.write("FAIL\n") logging.error("No Answer to our Authentication") @@ -163,6 +177,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, sock2.close() logging.info("Waiting for device...") + try: sock.settimeout(10) connection, client_address = sock.accept() @@ -172,6 +187,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, logging.error("No response from device") sock.close() return 1 + try: with open(filename, "rb") as f: if PROGRESS: @@ -225,7 +241,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename, logging.error("Error response from device") connection.close() return 1 - + except Exception as e: # noqa: E722 + logging.error("Error: %s", str(e)) finally: connection.close() diff --git a/tools/get.exe b/tools/get.exe index b56f2b98384..411cc7fb0e0 100644 Binary files a/tools/get.exe and b/tools/get.exe differ diff --git a/tools/get.py b/tools/get.py index c791020b7e9..a4395e85353 100755 --- a/tools/get.py +++ b/tools/get.py @@ -50,6 +50,49 @@ dist_dir = current_dir + "/dist/" +def is_safe_archive_path(path): + # Check for absolute paths (both Unix and Windows style) + if path.startswith('/') or (len(path) > 1 and path[1] == ':' and path[2] in '\\/'): + raise ValueError(f"Absolute path not allowed: {path}") + + # Normalize the path to handle any path separators + normalized_path = os.path.normpath(path) + + # Check for directory traversal attempts using normalized path + if ".." in normalized_path.split(os.sep): + raise ValueError(f"Directory traversal not allowed: {path}") + + # Additional check for paths that would escape the target directory + if normalized_path.startswith(".."): + raise ValueError(f"Path would escape target directory: {path}") + + # Check for any remaining directory traversal patterns in the original path + # This catches cases that might not be normalized properly + path_parts = path.replace('\\', '/').split('/') + if '..' in path_parts: + raise ValueError(f"Directory traversal not allowed: {path}") + + return True + + +def safe_tar_extract(tar_file, destination): + # Validate all paths before extraction + for member in tar_file.getmembers(): + is_safe_archive_path(member.name) + + # If all paths are safe, proceed with extraction + tar_file.extractall(destination, filter="tar") + + +def safe_zip_extract(zip_file, destination): + # Validate all paths before extraction + for name in zip_file.namelist(): + is_safe_archive_path(name) + + # If all paths are safe, proceed with extraction + zip_file.extractall(destination) + + def sha256sum(filename, blocksize=65536): hash = hashlib.sha256() with open(filename, "rb") as f: @@ -212,6 +255,10 @@ def unpack(filename, destination, force_extract, checksum): # noqa: C901 print("File corrupted or incomplete!") cfile = None file_is_corrupted = True + except ValueError as e: + print(f"Security validation failed: {e}") + cfile = None + file_is_corrupted = True if file_is_corrupted: corrupted_filename = filename + ".corrupted" @@ -243,15 +290,15 @@ def unpack(filename, destination, force_extract, checksum): # noqa: C901 if filename.endswith("tar.gz"): if not cfile: cfile = tarfile.open(filename, "r:gz") - cfile.extractall(destination, filter="tar") + safe_tar_extract(cfile, destination) elif filename.endswith("tar.xz"): if not cfile: cfile = tarfile.open(filename, "r:xz") - cfile.extractall(destination, filter="tar") + safe_tar_extract(cfile, destination) elif filename.endswith("zip"): if not cfile: cfile = zipfile.ZipFile(filename) - cfile.extractall(destination) + safe_zip_extract(cfile, destination) else: raise NotImplementedError("Unsupported archive type") @@ -348,9 +395,8 @@ def get_tool(tool, force_download, force_extract): urlretrieve(url, local_path, report_progress, context=ctx) elif "Windows" in sys_name: r = requests.get(url) - f = open(local_path, "wb") - f.write(r.content) - f.close() + with open(local_path, "wb") as f: + f.write(r.content) else: is_ci = os.environ.get("GITHUB_WORKSPACE") if is_ci: @@ -374,7 +420,8 @@ def get_tool(tool, force_download, force_extract): def load_tools_list(filename, platform): - tools_info = json.load(open(filename))["packages"][0]["tools"] + with open(filename, "r") as f: + tools_info = json.load(f)["packages"][0]["tools"] tools_to_download = [] for t in tools_info: if platform == "x86_64-mingw32":