From f85f5aae4697e7125401b6f9591bae33953ed4e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Sep 2025 11:23:57 -0600 Subject: [PATCH] base it --- .../components/esphome/ota/ota_esphome.cpp | 118 +++++++----------- esphome/components/esphome/ota/ota_esphome.h | 3 +- esphome/components/md5/md5.h | 22 ++-- esphome/components/sha256/sha256.h | 21 ++-- esphome/core/hash_base.h | 33 +++++ 5 files changed, 111 insertions(+), 86 deletions(-) create mode 100644 esphome/core/hash_base.h diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 8cd4152f6e..ef8bbe78c9 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -108,24 +108,6 @@ static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; // TODO: Remove this flag and all associated code in 2026.1.0 #define ALLOW_OTA_DOWNGRADE_MD5 -template struct HashTraits; - -template<> struct HashTraits { - static constexpr int NONCE_SIZE = 8; - static constexpr int HEX_SIZE = 32; - static constexpr const char *NAME = "MD5"; - static constexpr ota::OTAResponseTypes AUTH_REQUEST = ota::OTA_RESPONSE_REQUEST_AUTH; -}; - -#ifdef USE_OTA_SHA256 -template<> struct HashTraits { - static constexpr int NONCE_SIZE = 16; - static constexpr int HEX_SIZE = 64; - static constexpr const char *NAME = "SHA256"; - static constexpr ota::OTAResponseTypes AUTH_REQUEST = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; -}; -#endif - void ESPHomeOTAComponent::handle_handshake_() { /// Handle the initial OTA handshake. /// @@ -283,10 +265,12 @@ void ESPHomeOTAComponent::handle_data_() { // This prevents users from being locked out if they need to downgrade after updating // TODO: Remove this entire ifdef block in 2026.1.0 if (client_supports_sha256) { - auth_success = this->perform_hash_auth_(this->password_); + sha256::SHA256 sha_hasher; + auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, 16, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH); } else { ESP_LOGW(TAG, "Using MD5 auth for compatibility (deprecated)"); - auth_success = this->perform_hash_auth_(this->password_); + md5::MD5Digest md5_hasher; + auth_success = this->perform_hash_auth_(&md5_hasher, this->password_, 8, ota::OTA_RESPONSE_REQUEST_AUTH); } #else // Strict mode: SHA256 required on capable platforms (future default) @@ -300,7 +284,8 @@ void ESPHomeOTAComponent::handle_data_() { #else // Platform only supports MD5 - use it as the only available option // This is not a security downgrade as the platform cannot support SHA256 - auth_success = this->perform_hash_auth_(this->password_); + md5::MD5Digest md5_hasher; + auth_success = this->perform_hash_auth_(&md5_hasher, this->password_, 8, ota::OTA_RESPONSE_REQUEST_AUTH); #endif // USE_OTA_SHA256 if (!auth_success) { @@ -527,28 +512,28 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { delay(1); } -// Template function definition - placed at end to ensure all types are complete -template bool ESPHomeOTAComponent::perform_hash_auth_(const std::string &password) { - using Traits = HashTraits; +// Non-template function definition to reduce binary size +bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string &password, size_t nonce_size, + uint8_t auth_request) { + // Get sizes from the hasher + const size_t hex_size = hasher->get_hex_size(); + const char *name = hasher->get_name(); - // Minimize stack usage by reusing buffers - // We only need 2 buffers at most at the same time - constexpr size_t hex_buffer_size = Traits::HEX_SIZE + 1; - - // These two buffers are reused throughout the function - char hex_buffer1[hex_buffer_size]; // Used for: nonce -> expected result - char hex_buffer2[hex_buffer_size]; // Used for: cnonce -> response + // Use fixed-size buffers for the maximum possible hash size (SHA256 = 64 chars) + // This avoids dynamic allocation overhead + static constexpr size_t MAX_HEX_SIZE = 65; // SHA256 hex + null terminator + char hex_buffer1[MAX_HEX_SIZE]; // Used for: nonce -> expected result + char hex_buffer2[MAX_HEX_SIZE]; // Used for: cnonce -> response // Small stack buffer for auth request and nonce seed bytes uint8_t buf[1]; uint8_t nonce_bytes[8]; // Max 8 bytes (2 x uint32_t for SHA256) // Send auth request type - buf[0] = Traits::AUTH_REQUEST; + buf[0] = auth_request; this->writeall_(buf, 1); - HashClass hasher; - hasher.init(); + hasher->init(); // Generate nonce seed bytes uint32_t r1 = random_uint32(); @@ -558,79 +543,70 @@ template bool ESPHomeOTAComponent::perform_hash_auth_(const nonce_bytes[2] = (r1 >> 8) & 0xFF; nonce_bytes[3] = r1 & 0xFF; - if (Traits::NONCE_SIZE == 8) { + if (nonce_size == 8) { // MD5: 8 chars = "%08x" format = 4 bytes from one random uint32 - hasher.add(nonce_bytes, 4); - } -#ifdef USE_OTA_SHA256 - else { + hasher->add(nonce_bytes, 4); + } else { // SHA256: 16 chars = "%08x%08x" format = 8 bytes from two random uint32s uint32_t r2 = random_uint32(); nonce_bytes[4] = (r2 >> 24) & 0xFF; nonce_bytes[5] = (r2 >> 16) & 0xFF; nonce_bytes[6] = (r2 >> 8) & 0xFF; nonce_bytes[7] = r2 & 0xFF; - hasher.add(nonce_bytes, 8); + hasher->add(nonce_bytes, 8); } -#endif - hasher.calculate(); + hasher->calculate(); // Use hex_buffer1 for nonce - hasher.get_hex(hex_buffer1); - hex_buffer1[Traits::HEX_SIZE] = '\0'; - ESP_LOGV(TAG, "Auth: %s Nonce is %s", Traits::NAME, hex_buffer1); + hasher->get_hex(hex_buffer1); + hex_buffer1[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s Nonce is %s", name, hex_buffer1); // Send nonce - if (!this->writeall_(reinterpret_cast(hex_buffer1), Traits::HEX_SIZE)) { - ESP_LOGW(TAG, "Auth: Writing %s nonce failed", Traits::NAME); + if (!this->writeall_(reinterpret_cast(hex_buffer1), hex_size)) { + ESP_LOGW(TAG, "Auth: Writing %s nonce failed", name); return false; } // Prepare challenge - hasher.init(); - hasher.add(password.c_str(), password.length()); - hasher.add(hex_buffer1, Traits::HEX_SIZE); // Add nonce + hasher->init(); + hasher->add(password.c_str(), password.length()); + hasher->add(hex_buffer1, hex_size); // Add nonce // Receive cnonce into hex_buffer2 - if (!this->readall_(reinterpret_cast(hex_buffer2), Traits::HEX_SIZE)) { - ESP_LOGW(TAG, "Auth: Reading %s cnonce failed", Traits::NAME); + if (!this->readall_(reinterpret_cast(hex_buffer2), hex_size)) { + ESP_LOGW(TAG, "Auth: Reading %s cnonce failed", name); return false; } - hex_buffer2[Traits::HEX_SIZE] = '\0'; - ESP_LOGV(TAG, "Auth: %s CNonce is %s", Traits::NAME, hex_buffer2); + hex_buffer2[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s CNonce is %s", name, hex_buffer2); // Add cnonce to hash - hasher.add(hex_buffer2, Traits::HEX_SIZE); + hasher->add(hex_buffer2, hex_size); // Calculate result - reuse hex_buffer1 for expected - hasher.calculate(); - hasher.get_hex(hex_buffer1); - hex_buffer1[Traits::HEX_SIZE] = '\0'; - ESP_LOGV(TAG, "Auth: %s Result is %s", Traits::NAME, hex_buffer1); + hasher->calculate(); + hasher->get_hex(hex_buffer1); + hex_buffer1[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s Result is %s", name, hex_buffer1); // Receive response - reuse hex_buffer2 - if (!this->readall_(reinterpret_cast(hex_buffer2), Traits::HEX_SIZE)) { - ESP_LOGW(TAG, "Auth: Reading %s response failed", Traits::NAME); + if (!this->readall_(reinterpret_cast(hex_buffer2), hex_size)) { + ESP_LOGW(TAG, "Auth: Reading %s response failed", name); return false; } - hex_buffer2[Traits::HEX_SIZE] = '\0'; - ESP_LOGV(TAG, "Auth: %s Response is %s", Traits::NAME, hex_buffer2); + hex_buffer2[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s Response is %s", name, hex_buffer2); // Compare - bool matches = memcmp(hex_buffer1, hex_buffer2, Traits::HEX_SIZE) == 0; + bool matches = memcmp(hex_buffer1, hex_buffer2, hex_size) == 0; if (!matches) { - ESP_LOGW(TAG, "Auth failed! %s passwords do not match", Traits::NAME); + ESP_LOGW(TAG, "Auth failed! %s passwords do not match", name); } return matches; } -// Explicit template instantiations -template bool ESPHomeOTAComponent::perform_hash_auth_(const std::string &); -#ifdef USE_OTA_SHA256 -template bool ESPHomeOTAComponent::perform_hash_auth_(const std::string &); -#endif - } // namespace esphome #endif diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 8bfb1658b2..598f990ebd 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -7,6 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" +#include "esphome/core/hash_base.h" namespace esphome { @@ -30,7 +31,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { protected: void handle_handshake_(); void handle_data_(); - template bool perform_hash_auth_(const std::string &password); + bool perform_hash_auth_(HashBase *hasher, const std::string &password, size_t nonce_size, uint8_t auth_request); bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); void log_socket_error_(const LogString *msg); diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index be1df40423..5c5fbc4cff 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_MD5 +#include "esphome/core/hash_base.h" + #ifdef USE_ESP32 #include "esp_rom_md5.h" #define MD5_CTX_TYPE md5_context_t @@ -26,20 +28,20 @@ namespace esphome { namespace md5 { -class MD5Digest { +class MD5Digest : public HashBase { public: MD5Digest() = default; - ~MD5Digest() = default; + ~MD5Digest() override = default; /// Initialize a new MD5 digest computation. - void init(); + void init() override; /// Add bytes of data for the digest. - void add(const uint8_t *data, size_t len); - void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + void add(const uint8_t *data, size_t len) override; + void add(const char *data, size_t len) override { this->add((const uint8_t *) data, len); } /// Compute the digest, based on the provided data. - void calculate(); + void calculate() override; /// Retrieve the MD5 digest as bytes. /// The output must be able to hold 16 bytes or more. @@ -47,7 +49,13 @@ class MD5Digest { /// Retrieve the MD5 digest as hex characters. /// The output must be able to hold 32 bytes or more. - void get_hex(char *output); + void get_hex(char *output) override; + + /// Get the size of the hex output (32 for MD5) + size_t get_hex_size() const override { return 32; } + + /// Get the algorithm name for logging + const char *get_name() const override { return "MD5"; } /// Compare the digest against a provided byte-encoded digest (16 bytes). bool equals_bytes(const uint8_t *expected); diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index 0cea4cdcef..d3e7ee60db 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -8,6 +8,7 @@ #include #include #include +#include "esphome/core/hash_base.h" #if defined(USE_ESP32) || defined(USE_LIBRETINY) #include "mbedtls/sha256.h" @@ -21,22 +22,28 @@ namespace esphome::sha256 { -class SHA256 { +class SHA256 : public esphome::HashBase { public: SHA256() = default; - ~SHA256(); + ~SHA256() override; - void init(); - void add(const uint8_t *data, size_t len); - void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + void init() override; + void add(const uint8_t *data, size_t len) override; + void add(const char *data, size_t len) override { this->add((const uint8_t *) data, len); } void add(const std::string &data) { this->add(data.c_str(), data.length()); } - void calculate(); + void calculate() override; void get_bytes(uint8_t *output); - void get_hex(char *output); + void get_hex(char *output) override; std::string get_hex_string(); + /// Get the size of the hex output (64 for SHA256) + size_t get_hex_size() const override { return 64; } + + /// Get the algorithm name for logging + const char *get_name() const override { return "SHA256"; } + bool equals_bytes(const uint8_t *expected); bool equals_hex(const char *expected); diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h new file mode 100644 index 0000000000..f4c5dc630d --- /dev/null +++ b/esphome/core/hash_base.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace esphome { + +/// Base class for hash algorithms +class HashBase { + public: + virtual ~HashBase() = default; + + /// Initialize a new hash computation + virtual void init() = 0; + + /// Add bytes of data for the hash + virtual void add(const uint8_t *data, size_t len) = 0; + virtual void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the hash based on provided data + virtual void calculate() = 0; + + /// Retrieve the hash as hex characters + virtual void get_hex(char *output) = 0; + + /// Get the size of the hex output (32 for MD5, 64 for SHA256) + virtual size_t get_hex_size() const = 0; + + /// Get the algorithm name for logging + virtual const char *get_name() const = 0; +}; + +} // namespace esphome