mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'sha256_ota' into integration
This commit is contained in:
		| @@ -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 | // TODO: Remove this flag and all associated code in 2026.1.0 | ||||||
| #define ALLOW_OTA_DOWNGRADE_MD5 | #define ALLOW_OTA_DOWNGRADE_MD5 | ||||||
|  |  | ||||||
| template<typename HashClass> struct HashTraits; |  | ||||||
|  |  | ||||||
| template<> struct HashTraits<md5::MD5Digest> { |  | ||||||
|   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<sha256::SHA256> { |  | ||||||
|   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_() { | void ESPHomeOTAComponent::handle_handshake_() { | ||||||
|   /// Handle the initial OTA 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 |     // This prevents users from being locked out if they need to downgrade after updating | ||||||
|     // TODO: Remove this entire ifdef block in 2026.1.0 |     // TODO: Remove this entire ifdef block in 2026.1.0 | ||||||
|     if (client_supports_sha256) { |     if (client_supports_sha256) { | ||||||
|       auth_success = this->perform_hash_auth_<sha256::SHA256>(this->password_); |       sha256::SHA256 sha_hasher; | ||||||
|  |       auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, 16, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH); | ||||||
|     } else { |     } else { | ||||||
|       ESP_LOGW(TAG, "Using MD5 auth for compatibility (deprecated)"); |       ESP_LOGW(TAG, "Using MD5 auth for compatibility (deprecated)"); | ||||||
|       auth_success = this->perform_hash_auth_<md5::MD5Digest>(this->password_); |       md5::MD5Digest md5_hasher; | ||||||
|  |       auth_success = this->perform_hash_auth_(&md5_hasher, this->password_, 8, ota::OTA_RESPONSE_REQUEST_AUTH); | ||||||
|     } |     } | ||||||
| #else | #else | ||||||
|     // Strict mode: SHA256 required on capable platforms (future default) |     // Strict mode: SHA256 required on capable platforms (future default) | ||||||
| @@ -300,7 +284,8 @@ void ESPHomeOTAComponent::handle_data_() { | |||||||
| #else | #else | ||||||
|     // Platform only supports MD5 - use it as the only available option |     // Platform only supports MD5 - use it as the only available option | ||||||
|     // This is not a security downgrade as the platform cannot support SHA256 |     // This is not a security downgrade as the platform cannot support SHA256 | ||||||
|     auth_success = this->perform_hash_auth_<md5::MD5Digest>(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 | #endif  // USE_OTA_SHA256 | ||||||
|  |  | ||||||
|     if (!auth_success) { |     if (!auth_success) { | ||||||
| @@ -527,28 +512,28 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { | |||||||
|   delay(1); |   delay(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Template function definition - placed at end to ensure all types are complete | // Non-template function definition to reduce binary size | ||||||
| template<typename HashClass> bool ESPHomeOTAComponent::perform_hash_auth_(const std::string &password) { | bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string &password, size_t nonce_size, | ||||||
|   using Traits = HashTraits<HashClass>; |                                              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 |   // Use fixed-size buffers for the maximum possible hash size (SHA256 = 64 chars) | ||||||
|   // We only need 2 buffers at most at the same time |   // This avoids dynamic allocation overhead | ||||||
|   constexpr size_t hex_buffer_size = Traits::HEX_SIZE + 1; |   static constexpr size_t MAX_HEX_SIZE = 65;  // SHA256 hex + null terminator | ||||||
|  |   char hex_buffer1[MAX_HEX_SIZE];             // Used for: nonce -> expected result | ||||||
|   // These two buffers are reused throughout the function |   char hex_buffer2[MAX_HEX_SIZE];             // Used for: cnonce -> response | ||||||
|   char hex_buffer1[hex_buffer_size];  // Used for: nonce -> expected result |  | ||||||
|   char hex_buffer2[hex_buffer_size];  // Used for: cnonce -> response |  | ||||||
|  |  | ||||||
|   // Small stack buffer for auth request and nonce seed bytes |   // Small stack buffer for auth request and nonce seed bytes | ||||||
|   uint8_t buf[1]; |   uint8_t buf[1]; | ||||||
|   uint8_t nonce_bytes[8];  // Max 8 bytes (2 x uint32_t for SHA256) |   uint8_t nonce_bytes[8];  // Max 8 bytes (2 x uint32_t for SHA256) | ||||||
|  |  | ||||||
|   // Send auth request type |   // Send auth request type | ||||||
|   buf[0] = Traits::AUTH_REQUEST; |   buf[0] = auth_request; | ||||||
|   this->writeall_(buf, 1); |   this->writeall_(buf, 1); | ||||||
|  |  | ||||||
|   HashClass hasher; |   hasher->init(); | ||||||
|   hasher.init(); |  | ||||||
|  |  | ||||||
|   // Generate nonce seed bytes |   // Generate nonce seed bytes | ||||||
|   uint32_t r1 = random_uint32(); |   uint32_t r1 = random_uint32(); | ||||||
| @@ -558,79 +543,70 @@ template<typename HashClass> bool ESPHomeOTAComponent::perform_hash_auth_(const | |||||||
|   nonce_bytes[2] = (r1 >> 8) & 0xFF; |   nonce_bytes[2] = (r1 >> 8) & 0xFF; | ||||||
|   nonce_bytes[3] = r1 & 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 |     // MD5: 8 chars = "%08x" format = 4 bytes from one random uint32 | ||||||
|     hasher.add(nonce_bytes, 4); |     hasher->add(nonce_bytes, 4); | ||||||
|   } |   } else { | ||||||
| #ifdef USE_OTA_SHA256 |  | ||||||
|   else { |  | ||||||
|     // SHA256: 16 chars = "%08x%08x" format = 8 bytes from two random uint32s |     // SHA256: 16 chars = "%08x%08x" format = 8 bytes from two random uint32s | ||||||
|     uint32_t r2 = random_uint32(); |     uint32_t r2 = random_uint32(); | ||||||
|     nonce_bytes[4] = (r2 >> 24) & 0xFF; |     nonce_bytes[4] = (r2 >> 24) & 0xFF; | ||||||
|     nonce_bytes[5] = (r2 >> 16) & 0xFF; |     nonce_bytes[5] = (r2 >> 16) & 0xFF; | ||||||
|     nonce_bytes[6] = (r2 >> 8) & 0xFF; |     nonce_bytes[6] = (r2 >> 8) & 0xFF; | ||||||
|     nonce_bytes[7] = r2 & 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 |   // Use hex_buffer1 for nonce | ||||||
|   hasher.get_hex(hex_buffer1); |   hasher->get_hex(hex_buffer1); | ||||||
|   hex_buffer1[Traits::HEX_SIZE] = '\0'; |   hex_buffer1[hex_size] = '\0'; | ||||||
|   ESP_LOGV(TAG, "Auth: %s Nonce is %s", Traits::NAME, hex_buffer1); |   ESP_LOGV(TAG, "Auth: %s Nonce is %s", name, hex_buffer1); | ||||||
|  |  | ||||||
|   // Send nonce |   // Send nonce | ||||||
|   if (!this->writeall_(reinterpret_cast<uint8_t *>(hex_buffer1), Traits::HEX_SIZE)) { |   if (!this->writeall_(reinterpret_cast<uint8_t *>(hex_buffer1), hex_size)) { | ||||||
|     ESP_LOGW(TAG, "Auth: Writing %s nonce failed", Traits::NAME); |     ESP_LOGW(TAG, "Auth: Writing %s nonce failed", name); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Prepare challenge |   // Prepare challenge | ||||||
|   hasher.init(); |   hasher->init(); | ||||||
|   hasher.add(password.c_str(), password.length()); |   hasher->add(password.c_str(), password.length()); | ||||||
|   hasher.add(hex_buffer1, Traits::HEX_SIZE);  // Add nonce |   hasher->add(hex_buffer1, hex_size);  // Add nonce | ||||||
|  |  | ||||||
|   // Receive cnonce into hex_buffer2 |   // Receive cnonce into hex_buffer2 | ||||||
|   if (!this->readall_(reinterpret_cast<uint8_t *>(hex_buffer2), Traits::HEX_SIZE)) { |   if (!this->readall_(reinterpret_cast<uint8_t *>(hex_buffer2), hex_size)) { | ||||||
|     ESP_LOGW(TAG, "Auth: Reading %s cnonce failed", Traits::NAME); |     ESP_LOGW(TAG, "Auth: Reading %s cnonce failed", name); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   hex_buffer2[Traits::HEX_SIZE] = '\0'; |   hex_buffer2[hex_size] = '\0'; | ||||||
|   ESP_LOGV(TAG, "Auth: %s CNonce is %s", Traits::NAME, hex_buffer2); |   ESP_LOGV(TAG, "Auth: %s CNonce is %s", name, hex_buffer2); | ||||||
|  |  | ||||||
|   // Add cnonce to hash |   // Add cnonce to hash | ||||||
|   hasher.add(hex_buffer2, Traits::HEX_SIZE); |   hasher->add(hex_buffer2, hex_size); | ||||||
|  |  | ||||||
|   // Calculate result - reuse hex_buffer1 for expected |   // Calculate result - reuse hex_buffer1 for expected | ||||||
|   hasher.calculate(); |   hasher->calculate(); | ||||||
|   hasher.get_hex(hex_buffer1); |   hasher->get_hex(hex_buffer1); | ||||||
|   hex_buffer1[Traits::HEX_SIZE] = '\0'; |   hex_buffer1[hex_size] = '\0'; | ||||||
|   ESP_LOGV(TAG, "Auth: %s Result is %s", Traits::NAME, hex_buffer1); |   ESP_LOGV(TAG, "Auth: %s Result is %s", name, hex_buffer1); | ||||||
|  |  | ||||||
|   // Receive response - reuse hex_buffer2 |   // Receive response - reuse hex_buffer2 | ||||||
|   if (!this->readall_(reinterpret_cast<uint8_t *>(hex_buffer2), Traits::HEX_SIZE)) { |   if (!this->readall_(reinterpret_cast<uint8_t *>(hex_buffer2), hex_size)) { | ||||||
|     ESP_LOGW(TAG, "Auth: Reading %s response failed", Traits::NAME); |     ESP_LOGW(TAG, "Auth: Reading %s response failed", name); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   hex_buffer2[Traits::HEX_SIZE] = '\0'; |   hex_buffer2[hex_size] = '\0'; | ||||||
|   ESP_LOGV(TAG, "Auth: %s Response is %s", Traits::NAME, hex_buffer2); |   ESP_LOGV(TAG, "Auth: %s Response is %s", name, hex_buffer2); | ||||||
|  |  | ||||||
|   // Compare |   // Compare | ||||||
|   bool matches = memcmp(hex_buffer1, hex_buffer2, Traits::HEX_SIZE) == 0; |   bool matches = memcmp(hex_buffer1, hex_buffer2, hex_size) == 0; | ||||||
|  |  | ||||||
|   if (!matches) { |   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; |   return matches; | ||||||
| } | } | ||||||
|  |  | ||||||
| // Explicit template instantiations |  | ||||||
| template bool ESPHomeOTAComponent::perform_hash_auth_<md5::MD5Digest>(const std::string &); |  | ||||||
| #ifdef USE_OTA_SHA256 |  | ||||||
| template bool ESPHomeOTAComponent::perform_hash_auth_<sha256::SHA256>(const std::string &); |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/preferences.h" | #include "esphome/core/preferences.h" | ||||||
|  | #include "esphome/core/hash_base.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| @@ -30,7 +31,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { | |||||||
|  protected: |  protected: | ||||||
|   void handle_handshake_(); |   void handle_handshake_(); | ||||||
|   void handle_data_(); |   void handle_data_(); | ||||||
|   template<typename HashClass> 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 readall_(uint8_t *buf, size_t len); | ||||||
|   bool writeall_(const uint8_t *buf, size_t len); |   bool writeall_(const uint8_t *buf, size_t len); | ||||||
|   void log_socket_error_(const LogString *msg); |   void log_socket_error_(const LogString *msg); | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #ifdef USE_MD5 | #ifdef USE_MD5 | ||||||
|  |  | ||||||
|  | #include "esphome/core/hash_base.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| #include "esp_rom_md5.h" | #include "esp_rom_md5.h" | ||||||
| #define MD5_CTX_TYPE md5_context_t | #define MD5_CTX_TYPE md5_context_t | ||||||
| @@ -26,20 +28,20 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace md5 { | namespace md5 { | ||||||
|  |  | ||||||
| class MD5Digest { | class MD5Digest : public HashBase { | ||||||
|  public: |  public: | ||||||
|   MD5Digest() = default; |   MD5Digest() = default; | ||||||
|   ~MD5Digest() = default; |   ~MD5Digest() override = default; | ||||||
|  |  | ||||||
|   /// Initialize a new MD5 digest computation. |   /// Initialize a new MD5 digest computation. | ||||||
|   void init(); |   void init() override; | ||||||
|  |  | ||||||
|   /// Add bytes of data for the digest. |   /// Add bytes of data for the digest. | ||||||
|   void add(const uint8_t *data, size_t len); |   void add(const uint8_t *data, size_t len) override; | ||||||
|   void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } |   void add(const char *data, size_t len) override { this->add((const uint8_t *) data, len); } | ||||||
|  |  | ||||||
|   /// Compute the digest, based on the provided data. |   /// Compute the digest, based on the provided data. | ||||||
|   void calculate(); |   void calculate() override; | ||||||
|  |  | ||||||
|   /// Retrieve the MD5 digest as bytes. |   /// Retrieve the MD5 digest as bytes. | ||||||
|   /// The output must be able to hold 16 bytes or more. |   /// The output must be able to hold 16 bytes or more. | ||||||
| @@ -47,7 +49,13 @@ class MD5Digest { | |||||||
|  |  | ||||||
|   /// Retrieve the MD5 digest as hex characters. |   /// Retrieve the MD5 digest as hex characters. | ||||||
|   /// The output must be able to hold 32 bytes or more. |   /// 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). |   /// Compare the digest against a provided byte-encoded digest (16 bytes). | ||||||
|   bool equals_bytes(const uint8_t *expected); |   bool equals_bytes(const uint8_t *expected); | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| #include <cstdint> | #include <cstdint> | ||||||
| #include <string> | #include <string> | ||||||
| #include <memory> | #include <memory> | ||||||
|  | #include "esphome/core/hash_base.h" | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) || defined(USE_LIBRETINY) | ||||||
| #include "mbedtls/sha256.h" | #include "mbedtls/sha256.h" | ||||||
| @@ -21,22 +22,28 @@ | |||||||
|  |  | ||||||
| namespace esphome::sha256 { | namespace esphome::sha256 { | ||||||
|  |  | ||||||
| class SHA256 { | class SHA256 : public esphome::HashBase { | ||||||
|  public: |  public: | ||||||
|   SHA256() = default; |   SHA256() = default; | ||||||
|   ~SHA256(); |   ~SHA256() override; | ||||||
|  |  | ||||||
|   void init(); |   void init() override; | ||||||
|   void add(const uint8_t *data, size_t len); |   void add(const uint8_t *data, size_t len) override; | ||||||
|   void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } |   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 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_bytes(uint8_t *output); | ||||||
|   void get_hex(char *output); |   void get_hex(char *output) override; | ||||||
|   std::string get_hex_string(); |   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_bytes(const uint8_t *expected); | ||||||
|   bool equals_hex(const char *expected); |   bool equals_hex(const char *expected); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								esphome/core/hash_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/core/hash_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  | #include <cstddef> | ||||||
|  |  | ||||||
|  | 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 | ||||||
		Reference in New Issue
	
	Block a user