From 78333ef795f298f58347fc4cfb4990fc484492c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 21:14:56 -0500 Subject: [PATCH 01/34] safe a write --- .../components/esphome/ota/ota_esphome.cpp | 23 ++++++++++--------- esphome/core/defines.h | 1 + 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 6ffeeedb1a..ef86131e66 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -541,9 +541,6 @@ bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string // Small stack buffer for nonce seed bytes uint8_t nonce_bytes[8]; // Max 8 bytes (2 x uint32_t for SHA256) - // Send auth request type - this->writeall_(&auth_request, 1); - hasher->init(); // Generate nonce seed bytes using random_bytes @@ -554,20 +551,24 @@ bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string hasher->add(nonce_bytes, nonce_len); hasher->calculate(); - // Generate and send nonce - hasher->get_hex(buf); - buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s Nonce is %s", LOG_STR_ARG(name), buf); + // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) + buf[0] = auth_request; + hasher->get_hex(buf + 1); - if (!this->writeall_(reinterpret_cast(buf), hex_size)) { - this->log_auth_warning_(LOG_STR("Writing nonce"), name); + // Log nonce for debugging + buf[1 + hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s Nonce is %s", LOG_STR_ARG(name), buf + 1); + + // Send auth_type + nonce in a single write + if (!this->writeall_(reinterpret_cast(buf), 1 + hex_size)) { + this->log_auth_warning_(LOG_STR("Writing auth type and nonce"), name); return false; } - // Start challenge: password + nonce + // Start challenge: password + nonce (nonce is at buf + 1) hasher->init(); hasher->add(password.c_str(), password.length()); - hasher->add(buf, hex_size); + hasher->add(buf + 1, hex_size); // Read cnonce and add to hash if (!this->readall_(reinterpret_cast(buf), hex_size)) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 067ef4a4d0..261b6863ca 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -126,6 +126,7 @@ #define USE_OTA_MD5 #define USE_OTA_PASSWORD #define USE_OTA_SHA256 +#define ALLOW_OTA_DOWNGRADE_MD5 #define USE_OTA_STATE_CALLBACK #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE From a12283ba35c8f5caeb16da61dffc34452f6cd633 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 21:27:40 -0500 Subject: [PATCH 02/34] optimize --- .../components/esphome/ota/ota_esphome.cpp | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index ef86131e66..11795aaf2f 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -536,19 +536,16 @@ bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string const size_t hex_size = hasher->get_size() * 2; // Hex is twice the byte size const size_t nonce_len = hasher->get_size() / 4; // Nonce is 1/4 of hash size in bytes - // Use the provided buffer for all hex operations - - // Small stack buffer for nonce seed bytes - uint8_t nonce_bytes[8]; // Max 8 bytes (2 x uint32_t for SHA256) - - hasher->init(); + // Use the provided buffer for all operations // Generate nonce seed bytes using random_bytes - if (!random_bytes(nonce_bytes, nonce_len)) { + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { this->log_auth_warning_(LOG_STR("Random bytes generation failed"), name); return false; } - hasher->add(nonce_bytes, nonce_len); + + hasher->init(); + hasher->add(buf, nonce_len); hasher->calculate(); // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) @@ -571,31 +568,37 @@ bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string hasher->add(buf + 1, hex_size); // Read cnonce and add to hash - if (!this->readall_(reinterpret_cast(buf), hex_size)) { - this->log_auth_warning_(LOG_STR("Reading cnonce"), name); + if (!this->readall_(reinterpret_cast(buf), hex_size * 2)) { + this->log_auth_warning_(LOG_STR("Reading cnonce response"), name); return false; } - buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s CNonce is %s", LOG_STR_ARG(name), buf); - hasher->add(buf, hex_size); + // Response is located after CNonce in the buffer + const char *response = buf + hex_size; + + hasher->add(buf, hex_size); // add CNonce in binary hasher->calculate(); - // Log expected result (digest is already in hasher) - hasher->get_hex(buf); - buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s Result is %s", LOG_STR_ARG(name), buf); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[hex_size + 1]; + // Log CNonce for debugging + memcpy(log_buf, buf, hex_size); // Save CNonce for logging + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s CNonce is %s", LOG_STR_ARG(name), log_buf); - // Read response into the buffer - if (!this->readall_(reinterpret_cast(buf), hex_size)) { - this->log_auth_warning_(LOG_STR("Reading response"), name); - return false; - } - buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s Response is %s", LOG_STR_ARG(name), buf); + // Log computed hash for debugging + hasher->get_hex(log_buf); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s Result is %s", LOG_STR_ARG(name), log_buf); + + // Log received response + memcpy(log_buf, response, hex_size); // Save response for logging + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: %s Response is %s", LOG_STR_ARG(name), log_buf); +#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Compare response directly with digest in hasher - bool matches = hasher->equals_hex(buf); + bool matches = hasher->equals_hex(response); if (!matches) { this->log_auth_warning_(LOG_STR("Password mismatch"), name); From cc4c059429e5e2e5884915aeb149319af7ec970e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 21:52:00 -0500 Subject: [PATCH 03/34] optimize --- .../components/esphome/ota/ota_esphome.cpp | 221 ++++++++++++------ esphome/components/esphome/ota/ota_esphome.h | 15 +- 2 files changed, 164 insertions(+), 72 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 11795aaf2f..445167f13e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -141,7 +141,8 @@ void ESPHomeOTAComponent::handle_handshake_() { } this->log_start_(LOG_STR("handshake")); this->client_connect_time_ = App.get_loop_component_start_time(); - this->magic_buf_pos_ = 0; // Reset magic buffer position + this->handshake_buf_pos_ = 0; // Reset handshake buffer position + this->ota_state_ = OTAState::MAGIC_READ; } // Check for handshake timeout @@ -152,46 +153,143 @@ void ESPHomeOTAComponent::handle_handshake_() { return; } - // Try to read remaining magic bytes - if (this->magic_buf_pos_ < 5) { - // Read as many bytes as available - uint8_t bytes_to_read = 5 - this->magic_buf_pos_; - ssize_t read = this->client_->read(this->magic_buf_ + this->magic_buf_pos_, bytes_to_read); + while (true) { + switch (this->ota_state_) { + case OTAState::MAGIC_READ: { + // Try to read remaining magic bytes + if (this->handshake_buf_pos_ < 5) { + // Read as many bytes as available + uint8_t bytes_to_read = 5 - this->handshake_buf_pos_; + ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - return; // No data yet, try again next loop - } + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop + } - if (read <= 0) { - // Error or connection closed - if (read == -1) { - this->log_socket_error_(LOG_STR("reading magic bytes")); - } else { - ESP_LOGW(TAG, "Remote closed during handshake"); + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_(LOG_STR("reading magic bytes")); + } else { + ESP_LOGW(TAG, "Remote closed during handshake"); + } + this->cleanup_connection_(); + return; + } + + this->handshake_buf_pos_ += read; + } + + // Check if we have all 5 magic bytes + if (this->handshake_buf_pos_ != 5) { + break; + } + + // Validate magic bytes + static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0], + this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]); + // Send error response (non-blocking, best effort) + uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); + this->client_->write(&error, 1); + this->cleanup_connection_(); + return; + } + + // Magic bytes valid, move to next state + this->ota_state_ = OTAState::MAGIC_ACK; + this->handshake_buf_pos_ = 0; // Reset for reuse + continue; } - this->cleanup_connection_(); - return; + + case OTAState::MAGIC_ACK: { + // Send OK and version - 2 bytes + // Prepare response in handshake buffer if not already done + if (this->handshake_buf_pos_ == 0) { + this->handshake_buf_[0] = ota::OTA_RESPONSE_OK; + this->handshake_buf_[1] = USE_OTA_VERSION; + } + + // Write remaining bytes (2 total) + size_t bytes_to_write = 2 - this->handshake_buf_pos_; + ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write); + + if (written == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; // Try again next loop + } + this->log_socket_error_(LOG_STR("writing magic ack")); + this->cleanup_connection_(); + return; + } + + this->handshake_buf_pos_ += written; + if (this->handshake_buf_pos_ != 2) { + return; + } + // All bytes sent, create backend and move to next state + this->backend_ = ota::make_ota_backend(); + this->ota_state_ = OTAState::FEATURE_READ; + this->handshake_buf_pos_ = 0; // Reset for reuse + continue; + } + + case OTAState::FEATURE_READ: { + // Read features - 1 byte + ssize_t read = this->client_->read(this->handshake_buf_, 1); + + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop + } + + if (read <= 0) { + if (read == -1) { + this->log_socket_error_(LOG_STR("reading features")); + } else { + ESP_LOGW(TAG, "Remote closed during feature read"); + } + this->cleanup_connection_(); + return; + } + + this->ota_features_ = this->handshake_buf_[0]; + ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); + this->ota_state_ = OTAState::FEATURE_ACK; + this->handshake_buf_pos_ = 0; // Reset for reuse + continue; + } + + case OTAState::FEATURE_ACK: { + // Acknowledge header - 1 byte + uint8_t ack = + ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) + ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION + : ota::OTA_RESPONSE_HEADER_OK; + + ssize_t written = this->client_->write(&ack, 1); + if (written == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; // Try again next loop + } + this->log_socket_error_(LOG_STR("writing feature ack")); + this->cleanup_connection_(); + return; + } + + // Handshake complete, move to data phase + this->ota_state_ = OTAState::DATA; + continue; + } + + case OTAState::DATA: + this->handle_data_(); + return; + + case OTAState::IDLE: + // This shouldn't happen + return; } - - this->magic_buf_pos_ += read; - } - - // Check if we have all 5 magic bytes - if (this->magic_buf_pos_ == 5) { - // Validate magic bytes - static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; - if (memcmp(this->magic_buf_, MAGIC_BYTES, 5) != 0) { - ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->magic_buf_[0], - this->magic_buf_[1], this->magic_buf_[2], this->magic_buf_[3], this->magic_buf_[4]); - // Send error response (non-blocking, best effort) - uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); - this->client_->write(&error, 1); - this->cleanup_connection_(); - return; - } - - // All 5 magic bytes are valid, continue with data handling - this->handle_data_(); } } @@ -208,35 +306,15 @@ void ESPHomeOTAComponent::handle_data_() { uint8_t buf[1024]; char *sbuf = reinterpret_cast(buf); size_t ota_size; - uint8_t ota_features; - std::unique_ptr backend; - (void) ota_features; #if USE_OTA_VERSION == 2 size_t size_acknowledged = 0; #endif - // Send OK and version - 2 bytes - buf[0] = ota::OTA_RESPONSE_OK; - buf[1] = USE_OTA_VERSION; - this->writeall_(buf, 2); - - backend = ota::make_ota_backend(); - - // Read features - 1 byte - if (!this->readall_(buf, 1)) { - this->log_read_error_(LOG_STR("features")); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - ota_features = buf[0]; // NOLINT - ESP_LOGV(TAG, "Features: 0x%02X", ota_features); - - // Acknowledge header - 1 byte - buf[0] = ota::OTA_RESPONSE_HEADER_OK; - if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { - buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION; - } - - this->writeall_(buf, 1); + // The handshake has already been completed in handle_handshake_() + // We already have: + // - this->backend_ created + // - this->ota_features_ set + // - Feature acknowledgment sent #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { @@ -261,7 +339,7 @@ void ESPHomeOTAComponent::handle_data_() { // Devices that don't support SHA256 (due to platform limitations) will // continue to use MD5 as their only option (see #else branch below). - bool client_supports_sha256 = (ota_features & FEATURE_SUPPORTS_SHA256_AUTH) != 0; + bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; #ifdef ALLOW_OTA_DOWNGRADE_MD5 // Temporary compatibility mode: Allow MD5 for ~3 versions to enable OTA downgrades @@ -334,7 +412,7 @@ void ESPHomeOTAComponent::handle_data_() { #endif // This will block for a few seconds as it locks flash - error_code = backend->begin(ota_size); + error_code = this->backend_->begin(ota_size); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) update_started = true; @@ -350,7 +428,7 @@ void ESPHomeOTAComponent::handle_data_() { } sbuf[32] = '\0'; ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf); - backend->set_update_md5(sbuf); + this->backend_->set_update_md5(sbuf); // Acknowledge MD5 OK - 1 byte buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; @@ -375,7 +453,7 @@ void ESPHomeOTAComponent::handle_data_() { goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - error_code = backend->write(buf, read); + error_code = this->backend_->write(buf, read); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Flash write error, code: %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) @@ -406,7 +484,7 @@ void ESPHomeOTAComponent::handle_data_() { buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; this->writeall_(buf, 1); - error_code = backend->end(); + error_code = this->backend_->end(); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Error ending update! code: %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) @@ -437,8 +515,8 @@ error: this->writeall_(buf, 1); this->cleanup_connection_(); - if (backend != nullptr && update_started) { - backend->abort(); + if (this->backend_ != nullptr && update_started) { + this->backend_->abort(); } this->status_momentary_error("onerror", 5000); @@ -516,7 +594,10 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; this->client_connect_time_ = 0; - this->magic_buf_pos_ = 0; + this->handshake_buf_pos_ = 0; + this->ota_state_ = OTAState::IDLE; + this->ota_features_ = 0; + this->backend_ = nullptr; } void ESPHomeOTAComponent::yield_and_feed_watchdog_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 5bacb60706..02e759c2ba 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -14,6 +14,14 @@ namespace esphome { /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. class ESPHomeOTAComponent : public ota::OTAComponent { public: + enum class OTAState : uint8_t { + IDLE, + MAGIC_READ, // Reading magic bytes + MAGIC_ACK, // Sending OK and version after magic bytes + FEATURE_READ, // Reading feature flags from client + FEATURE_ACK, // Sending feature acknowledgment + DATA, // Processing OTA data (authentication, update, etc.) + }; #ifdef USE_OTA_PASSWORD void set_auth_password(const std::string &password) { password_ = password; } #endif // USE_OTA_PASSWORD @@ -51,10 +59,13 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr server_; std::unique_ptr client_; + OTAState ota_state_{OTAState::IDLE}; uint32_t client_connect_time_{0}; uint16_t port_; - uint8_t magic_buf_[5]; - uint8_t magic_buf_pos_{0}; + uint8_t handshake_buf_[5]; + uint8_t handshake_buf_pos_{0}; + uint8_t ota_features_{0}; + std::unique_ptr backend_; }; } // namespace esphome From e7b9f17bbed304f1a6a93681cc6dcfe921bcf9ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 21:54:58 -0500 Subject: [PATCH 04/34] optimize --- esphome/components/esphome/ota/ota_esphome.cpp | 8 ++++++-- esphome/components/esphome/ota/ota_esphome.h | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 445167f13e..8edcba99c9 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -171,7 +171,7 @@ void ESPHomeOTAComponent::handle_handshake_() { if (read == -1) { this->log_socket_error_(LOG_STR("reading magic bytes")); } else { - ESP_LOGW(TAG, "Remote closed during handshake"); + this->log_remote_closed_(LOG_STR("handshake")); } this->cleanup_connection_(); return; @@ -247,7 +247,7 @@ void ESPHomeOTAComponent::handle_handshake_() { if (read == -1) { this->log_socket_error_(LOG_STR("reading features")); } else { - ESP_LOGW(TAG, "Remote closed during feature read"); + this->log_remote_closed_(LOG_STR("feature read")); } this->cleanup_connection_(); return; @@ -590,6 +590,10 @@ void ESPHomeOTAComponent::log_start_(const LogString *phase) { ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); } +void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { + ESP_LOGW(TAG, "Remote closed during %s", LOG_STR_ARG(during)); +} + void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 02e759c2ba..f50444c6ce 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -49,6 +49,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); + void log_remote_closed_(const LogString *during); void cleanup_connection_(); void yield_and_feed_watchdog_(); From 10c5a19503bec70af4825bfc466dc921c7d3ebf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 22:01:44 -0500 Subject: [PATCH 05/34] optimize --- .../components/esphome/ota/ota_esphome.cpp | 69 ++++++++----------- esphome/components/esphome/ota/ota_esphome.h | 3 + 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 8edcba99c9..e231cf336e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -156,33 +156,9 @@ void ESPHomeOTAComponent::handle_handshake_() { while (true) { switch (this->ota_state_) { case OTAState::MAGIC_READ: { - // Try to read remaining magic bytes - if (this->handshake_buf_pos_ < 5) { - // Read as many bytes as available - uint8_t bytes_to_read = 5 - this->handshake_buf_pos_; - ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); - - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - return; // No data yet, try again next loop - } - - if (read <= 0) { - // Error or connection closed - if (read == -1) { - this->log_socket_error_(LOG_STR("reading magic bytes")); - } else { - this->log_remote_closed_(LOG_STR("handshake")); - } - this->cleanup_connection_(); - return; - } - - this->handshake_buf_pos_ += read; - } - - // Check if we have all 5 magic bytes - if (this->handshake_buf_pos_ != 5) { - break; + // Try to read remaining magic bytes (5 total) + if (!this->try_read_(5, LOG_STR("reading magic bytes"), LOG_STR("handshake"))) { + return; } // Validate magic bytes @@ -237,19 +213,7 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::FEATURE_READ: { // Read features - 1 byte - ssize_t read = this->client_->read(this->handshake_buf_, 1); - - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - return; // No data yet, try again next loop - } - - if (read <= 0) { - if (read == -1) { - this->log_socket_error_(LOG_STR("reading features")); - } else { - this->log_remote_closed_(LOG_STR("feature read")); - } - this->cleanup_connection_(); + if (!this->try_read_(1, LOG_STR("reading features"), LOG_STR("feature read"))) { return; } @@ -594,6 +558,31 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed during %s", LOG_STR_ARG(during)); } +bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc) { + // Read bytes into handshake buffer, starting at handshake_buf_pos_ + size_t bytes_to_read = to_read - this->handshake_buf_pos_; + ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); + + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return false; // No data yet, try again next loop + } + + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_(error_desc); + } else { + this->log_remote_closed_(close_desc); + } + this->cleanup_connection_(); + return false; + } + + this->handshake_buf_pos_ += read; + // Return true only if we have all the requested bytes + return this->handshake_buf_pos_ >= to_read; +} + void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index f50444c6ce..c73fe7e732 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -46,6 +46,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #endif // USE_OTA_PASSWORD bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); + + bool try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc); + void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); From a08a99e3f4f06e65600542650f193754141b2888 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 22:05:06 -0500 Subject: [PATCH 06/34] optimize --- .../components/esphome/ota/ota_esphome.cpp | 69 ++++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 2 + 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index e231cf336e..1ab15983a0 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -174,8 +174,7 @@ void ESPHomeOTAComponent::handle_handshake_() { } // Magic bytes valid, move to next state - this->ota_state_ = OTAState::MAGIC_ACK; - this->handshake_buf_pos_ = 0; // Reset for reuse + this->transition_ota_state_(OTAState::MAGIC_ACK); continue; } @@ -187,27 +186,13 @@ void ESPHomeOTAComponent::handle_handshake_() { this->handshake_buf_[1] = USE_OTA_VERSION; } - // Write remaining bytes (2 total) - size_t bytes_to_write = 2 - this->handshake_buf_pos_; - ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write); - - if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return; // Try again next loop - } - this->log_socket_error_(LOG_STR("writing magic ack")); - this->cleanup_connection_(); + if (!this->try_write_(2, LOG_STR("writing magic ack"))) { return; } - this->handshake_buf_pos_ += written; - if (this->handshake_buf_pos_ != 2) { - return; - } // All bytes sent, create backend and move to next state this->backend_ = ota::make_ota_backend(); - this->ota_state_ = OTAState::FEATURE_READ; - this->handshake_buf_pos_ = 0; // Reset for reuse + this->transition_ota_state_(OTAState::FEATURE_READ); continue; } @@ -219,30 +204,26 @@ void ESPHomeOTAComponent::handle_handshake_() { this->ota_features_ = this->handshake_buf_[0]; ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); - this->ota_state_ = OTAState::FEATURE_ACK; - this->handshake_buf_pos_ = 0; // Reset for reuse + this->transition_ota_state_(OTAState::FEATURE_ACK); continue; } case OTAState::FEATURE_ACK: { // Acknowledge header - 1 byte - uint8_t ack = - ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) - ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION - : ota::OTA_RESPONSE_HEADER_OK; + // Prepare response in handshake buffer if not already done + if (this->handshake_buf_pos_ == 0) { + this->handshake_buf_[0] = + ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) + ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION + : ota::OTA_RESPONSE_HEADER_OK; + } - ssize_t written = this->client_->write(&ack, 1); - if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return; // Try again next loop - } - this->log_socket_error_(LOG_STR("writing feature ack")); - this->cleanup_connection_(); + if (!this->try_write_(1, LOG_STR("writing feature ack"))) { return; } // Handshake complete, move to data phase - this->ota_state_ = OTAState::DATA; + this->transition_ota_state_(OTAState::DATA); continue; } @@ -583,6 +564,30 @@ bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, return this->handshake_buf_pos_ >= to_read; } +bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *error_desc) { + // Write bytes from handshake buffer, starting at handshake_buf_pos_ + size_t bytes_to_write = to_write - this->handshake_buf_pos_; + ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write); + + if (written == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return false; // Try again next loop + } + this->log_socket_error_(error_desc); + this->cleanup_connection_(); + return false; + } + + this->handshake_buf_pos_ += written; + // Return true only if we have written all the requested bytes + return this->handshake_buf_pos_ >= to_write; +} + +void ESPHomeOTAComponent::transition_ota_state_(OTAState next_state) { + this->ota_state_ = next_state; + this->handshake_buf_pos_ = 0; // Reset buffer position for next state +} + void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c73fe7e732..b7491df752 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -48,6 +48,8 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool writeall_(const uint8_t *buf, size_t len); bool try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc); + bool try_write_(size_t to_write, const LogString *error_desc); + void transition_ota_state_(OTAState next_state); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); From e0f99e059602d0616e4e5a0d9e280032c1fe9a06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 22:09:44 -0500 Subject: [PATCH 07/34] optimize --- .../components/esphome/ota/ota_esphome.cpp | 154 +++++++++--------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 1ab15983a0..70e3693cb7 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -153,88 +153,86 @@ void ESPHomeOTAComponent::handle_handshake_() { return; } - while (true) { - switch (this->ota_state_) { - case OTAState::MAGIC_READ: { - // Try to read remaining magic bytes (5 total) - if (!this->try_read_(5, LOG_STR("reading magic bytes"), LOG_STR("handshake"))) { - return; - } - - // Validate magic bytes - static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; - if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) { - ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0], - this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]); - // Send error response (non-blocking, best effort) - uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); - this->client_->write(&error, 1); - this->cleanup_connection_(); - return; - } - - // Magic bytes valid, move to next state - this->transition_ota_state_(OTAState::MAGIC_ACK); - continue; - } - - case OTAState::MAGIC_ACK: { - // Send OK and version - 2 bytes - // Prepare response in handshake buffer if not already done - if (this->handshake_buf_pos_ == 0) { - this->handshake_buf_[0] = ota::OTA_RESPONSE_OK; - this->handshake_buf_[1] = USE_OTA_VERSION; - } - - if (!this->try_write_(2, LOG_STR("writing magic ack"))) { - return; - } - - // All bytes sent, create backend and move to next state - this->backend_ = ota::make_ota_backend(); - this->transition_ota_state_(OTAState::FEATURE_READ); - continue; - } - - case OTAState::FEATURE_READ: { - // Read features - 1 byte - if (!this->try_read_(1, LOG_STR("reading features"), LOG_STR("feature read"))) { - return; - } - - this->ota_features_ = this->handshake_buf_[0]; - ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); - this->transition_ota_state_(OTAState::FEATURE_ACK); - continue; - } - - case OTAState::FEATURE_ACK: { - // Acknowledge header - 1 byte - // Prepare response in handshake buffer if not already done - if (this->handshake_buf_pos_ == 0) { - this->handshake_buf_[0] = - ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) - ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION - : ota::OTA_RESPONSE_HEADER_OK; - } - - if (!this->try_write_(1, LOG_STR("writing feature ack"))) { - return; - } - - // Handshake complete, move to data phase - this->transition_ota_state_(OTAState::DATA); - continue; - } - - case OTAState::DATA: - this->handle_data_(); + switch (this->ota_state_) { + case OTAState::MAGIC_READ: { + // Try to read remaining magic bytes (5 total) + if (!this->try_read_(5, LOG_STR("reading magic bytes"), LOG_STR("handshake"))) { return; + } - case OTAState::IDLE: - // This shouldn't happen + // Validate magic bytes + static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0], + this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]); + // Send error response (non-blocking, best effort) + uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); + this->client_->write(&error, 1); + this->cleanup_connection_(); return; + } + + // Magic bytes valid, move to next state + this->transition_ota_state_(OTAState::MAGIC_ACK); + [[fallthrough]]; } + + case OTAState::MAGIC_ACK: { + // Send OK and version - 2 bytes + // Prepare response in handshake buffer if not already done + if (this->handshake_buf_pos_ == 0) { + this->handshake_buf_[0] = ota::OTA_RESPONSE_OK; + this->handshake_buf_[1] = USE_OTA_VERSION; + } + + if (!this->try_write_(2, LOG_STR("writing magic ack"))) { + return; + } + + // All bytes sent, create backend and move to next state + this->backend_ = ota::make_ota_backend(); + this->transition_ota_state_(OTAState::FEATURE_READ); + [[fallthrough]]; + } + + case OTAState::FEATURE_READ: { + // Read features - 1 byte + if (!this->try_read_(1, LOG_STR("reading features"), LOG_STR("feature read"))) { + return; + } + + this->ota_features_ = this->handshake_buf_[0]; + ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); + this->transition_ota_state_(OTAState::FEATURE_ACK); + [[fallthrough]]; + } + + case OTAState::FEATURE_ACK: { + // Acknowledge header - 1 byte + // Prepare response in handshake buffer if not already done + if (this->handshake_buf_pos_ == 0) { + this->handshake_buf_[0] = + ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) + ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION + : ota::OTA_RESPONSE_HEADER_OK; + } + + if (!this->try_write_(1, LOG_STR("writing feature ack"))) { + return; + } + + // Handshake complete, move to data phase + this->transition_ota_state_(OTAState::DATA); + [[fallthrough]]; + } + + case OTAState::DATA: + this->handle_data_(); + return; + + case OTAState::IDLE: + // This shouldn't happen + return; } } From 3bec6efdc379f25ac698ed911f4e27d6d2398cb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 22:10:41 -0500 Subject: [PATCH 08/34] optimize --- esphome/components/esphome/ota/ota_esphome.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index b7491df752..7210d78fa9 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -64,14 +64,14 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr server_; std::unique_ptr client_; + std::unique_ptr backend_; - OTAState ota_state_{OTAState::IDLE}; uint32_t client_connect_time_{0}; uint16_t port_; uint8_t handshake_buf_[5]; + OTAState ota_state_{OTAState::IDLE}; uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; - std::unique_ptr backend_; }; } // namespace esphome From abcc2d483b184332c93e98fd383e62787bdfbfd9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 22:33:41 -0500 Subject: [PATCH 09/34] optimize --- .../components/esphome/ota/ota_esphome.cpp | 343 +++++++++++------- esphome/components/esphome/ota/ota_esphome.h | 20 +- 2 files changed, 231 insertions(+), 132 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 70e3693cb7..cabc14f1c1 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,11 +1,13 @@ #include "ota_esphome.h" #ifdef USE_OTA +#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_MD5 #include "esphome/components/md5/md5.h" #endif #ifdef USE_OTA_SHA256 #include "esphome/components/sha256/sha256.h" #endif +#endif #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_arduino_esp32.h" @@ -165,10 +167,7 @@ void ESPHomeOTAComponent::handle_handshake_() { if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) { ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0], this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]); - // Send error response (non-blocking, best effort) - uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); - this->client_->write(&error, 1); - this->cleanup_connection_(); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC); return; } @@ -221,10 +220,39 @@ void ESPHomeOTAComponent::handle_handshake_() { return; } - // Handshake complete, move to data phase +#ifdef USE_OTA_PASSWORD + // If password is set, move to auth phase + if (!this->password_.empty()) { + this->transition_ota_state_(OTAState::AUTH_SEND); + [[fallthrough]]; + } else +#endif + { + // No password, move directly to data phase + this->transition_ota_state_(OTAState::DATA); + [[fallthrough]]; + } + } + +#ifdef USE_OTA_PASSWORD + case OTAState::AUTH_SEND: { + // Non-blocking authentication send + if (!this->handle_auth_send_()) { + return; + } + this->transition_ota_state_(OTAState::AUTH_READ); + [[fallthrough]]; + } + + case OTAState::AUTH_READ: { + // Non-blocking authentication read & verify + if (!this->handle_auth_read_()) { + return; + } this->transition_ota_state_(OTAState::DATA); [[fallthrough]]; } +#endif case OTAState::DATA: this->handle_data_(); @@ -240,8 +268,10 @@ void ESPHomeOTAComponent::handle_data_() { /// Handle the OTA data transfer and update process. /// /// This method is blocking and will not return until the OTA update completes, - /// fails, or times out. It handles authentication, receives the firmware data, - /// writes it to flash, and reboots on success. + /// fails, or times out. It receives the firmware data, writes it to flash, + /// and reboots on success. + /// + /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ. ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; size_t total = 0; @@ -253,80 +283,12 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif - // The handshake has already been completed in handle_handshake_() + // The handshake and auth have already been completed // We already have: // - this->backend_ created // - this->ota_features_ set // - Feature acknowledgment sent - -#ifdef USE_OTA_PASSWORD - if (!this->password_.empty()) { - bool auth_success = false; - -#ifdef USE_OTA_SHA256 - // SECURITY HARDENING: Prefer SHA256 authentication on platforms that support it. - // - // This is a hardening measure to prevent future downgrade attacks where an attacker - // could force the use of MD5 authentication by manipulating the feature flags. - // - // While MD5 is currently still acceptable for our OTA authentication use case - // (where the password is a shared secret and we're only authenticating, not - // encrypting), at some point in the future MD5 will likely become so weak that - // it could be practically attacked. - // - // We enforce SHA256 now on capable platforms because: - // 1. We can't retroactively update device firmware in the field - // 2. Clients (like esphome CLI) can always be updated to support SHA256 - // 3. This prevents any possibility of downgrade attacks in the future - // - // Devices that don't support SHA256 (due to platform limitations) will - // continue to use MD5 as their only option (see #else branch below). - - bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; - -#ifdef ALLOW_OTA_DOWNGRADE_MD5 - // Temporary compatibility mode: Allow MD5 for ~3 versions to enable OTA downgrades - // 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) { - sha256::SHA256 sha_hasher; - auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH, - LOG_STR("SHA256"), sbuf); - } else { -#ifdef USE_OTA_MD5 - ESP_LOGW(TAG, "Using MD5 auth for compatibility (deprecated)"); - md5::MD5Digest md5_hasher; - auth_success = - this->perform_hash_auth_(&md5_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_AUTH, LOG_STR("MD5"), sbuf); -#endif // USE_OTA_MD5 - } -#else - // Strict mode: SHA256 required on capable platforms (future default) - if (!client_supports_sha256) { - ESP_LOGW(TAG, "Client requires SHA256"); - error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - sha256::SHA256 sha_hasher; - auth_success = this->perform_hash_auth_(&sha_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_SHA256_AUTH, - LOG_STR("SHA256"), sbuf); -#endif // ALLOW_OTA_DOWNGRADE_MD5 -#else - // Platform only supports MD5 - use it as the only available option - // This is not a security downgrade as the platform cannot support SHA256 -#ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; - auth_success = - this->perform_hash_auth_(&md5_hasher, this->password_, ota::OTA_RESPONSE_REQUEST_AUTH, LOG_STR("MD5"), sbuf); -#endif // USE_OTA_MD5 -#endif // USE_OTA_SHA256 - - if (!auth_success) { - error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - } -#endif // USE_OTA_PASSWORD + // - Authentication completed (if password was set) // Acknowledge auth OK - 1 byte buf[0] = ota::OTA_RESPONSE_AUTH_OK; @@ -594,6 +556,15 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->ota_state_ = OTAState::IDLE; this->ota_features_ = 0; this->backend_ = nullptr; +#ifdef USE_OTA_PASSWORD + this->cleanup_auth_(); +#endif +} + +void ESPHomeOTAComponent::send_error_and_cleanup_(ota::OTAResponseTypes error) { + uint8_t error_byte = static_cast(error); + this->client_->write(&error_byte, 1); // Best effort, non-blocking + this->cleanup_connection_(); } void ESPHomeOTAComponent::yield_and_feed_watchdog_() { @@ -602,86 +573,202 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { } #ifdef USE_OTA_PASSWORD -void ESPHomeOTAComponent::log_auth_warning_(const LogString *action, const LogString *hash_name) { - ESP_LOGW(TAG, "Auth: %s %s failed", LOG_STR_ARG(action), LOG_STR_ARG(hash_name)); +void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } + +bool ESPHomeOTAComponent::handle_auth_send_() { + // Determine which auth type to use based on platform capabilities and client support + uint8_t auth_type; + +#ifdef USE_OTA_SHA256 + bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; + +#ifdef ALLOW_OTA_DOWNGRADE_MD5 + if (client_supports_sha256) { + auth_type = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + if (!this->auth_hasher_) { + this->auth_hasher_ = std::make_unique(); + } + } else { +#ifdef USE_OTA_MD5 + this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); + auth_type = ota::OTA_RESPONSE_REQUEST_AUTH; + if (!this->auth_hasher_) { + this->auth_hasher_ = std::make_unique(); + } +#else + this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; +#endif // USE_OTA_MD5 + } +#else // !ALLOW_OTA_DOWNGRADE_MD5 + if (!client_supports_sha256) { + this->log_auth_warning_(LOG_STR("Client requires SHA256")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + auth_type = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + if (!this->auth_hasher_) { + this->auth_hasher_ = std::make_unique(); + } +#endif // ALLOW_OTA_DOWNGRADE_MD5 +#else // !USE_OTA_SHA256 +#ifdef USE_OTA_MD5 + auth_type = ota::OTA_RESPONSE_REQUEST_AUTH; + if (!this->auth_hasher_) { + this->auth_hasher_ = std::make_unique(); + } +#else + this->log_auth_warning_(LOG_STR("No auth methods available")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; +#endif // USE_OTA_MD5 +#endif // USE_OTA_SHA256 + + // Initialize auth buffer if not already done + if (!this->auth_buf_) { + // Calculate required buffer size + const size_t hex_size = this->auth_hasher_->get_size() * 2; + const size_t nonce_len = this->auth_hasher_->get_size() / 4; + // Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response) + this->auth_buf_size_ = 1 + hex_size + hex_size * 2; + this->auth_buf_ = std::make_unique(this->auth_buf_size_); + this->auth_buf_pos_ = 0; + + // Generate nonce + char *buf = reinterpret_cast(this->auth_buf_.get() + 1); + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { + this->log_auth_warning_(LOG_STR("Random bytes generation failed")); + this->cleanup_connection_(); + return false; + } + + this->auth_hasher_->init(); + this->auth_hasher_->add(buf, nonce_len); + this->auth_hasher_->calculate(); + + // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) + this->auth_buf_[0] = auth_type; + this->auth_hasher_->get_hex(buf); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + // Log nonce for debugging + char log_buf[hex_size + 1]; + memcpy(log_buf, buf, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); +#endif + } + + // Try to write auth_type + nonce + const size_t hex_size = this->auth_hasher_->get_size() * 2; + const size_t to_write = 1 + hex_size; + size_t remaining = to_write - this->auth_buf_pos_; + + ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining); + if (written == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return false; // Try again next loop + } + this->log_auth_warning_(LOG_STR("Writing auth type and nonce failed")); + this->cleanup_connection_(); + return false; + } + + this->auth_buf_pos_ += written; + + // Check if we still have more to write + if (this->auth_buf_pos_ < to_write) { + return false; // More to write, try again next loop + } + + // All written, prepare for reading phase + this->auth_buf_pos_ = 0; + + // Start challenge hash: password + nonce + this->auth_hasher_->init(); + this->auth_hasher_->add(this->password_.c_str(), this->password_.length()); + this->auth_hasher_->add(reinterpret_cast(this->auth_buf_.get() + 1), hex_size); + + return true; } -// Non-template function definition to reduce binary size -bool ESPHomeOTAComponent::perform_hash_auth_(HashBase *hasher, const std::string &password, uint8_t auth_request, - const LogString *name, char *buf) { - // Get sizes from the hasher - const size_t hex_size = hasher->get_size() * 2; // Hex is twice the byte size - const size_t nonce_len = hasher->get_size() / 4; // Nonce is 1/4 of hash size in bytes +bool ESPHomeOTAComponent::handle_auth_read_() { + const size_t hex_size = this->auth_hasher_->get_size() * 2; + const size_t to_read = hex_size * 2; // CNonce + Response - // Use the provided buffer for all operations + // Try to read remaining bytes + size_t remaining = to_read - this->auth_buf_pos_; + ssize_t read = this->client_->read(this->auth_buf_.get() + this->auth_buf_pos_, remaining); - // Generate nonce seed bytes using random_bytes - if (!random_bytes(reinterpret_cast(buf), nonce_len)) { - this->log_auth_warning_(LOG_STR("Random bytes generation failed"), name); + if (read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return false; // Try again next loop + } + this->log_auth_warning_(LOG_STR("Reading cnonce response failed")); + this->cleanup_connection_(); return false; } - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); - - // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) - buf[0] = auth_request; - hasher->get_hex(buf + 1); - - // Log nonce for debugging - buf[1 + hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s Nonce is %s", LOG_STR_ARG(name), buf + 1); - - // Send auth_type + nonce in a single write - if (!this->writeall_(reinterpret_cast(buf), 1 + hex_size)) { - this->log_auth_warning_(LOG_STR("Writing auth type and nonce"), name); + if (read == 0) { + this->log_auth_warning_(LOG_STR("Remote closed during auth read")); + this->cleanup_connection_(); return false; } - // Start challenge: password + nonce (nonce is at buf + 1) - hasher->init(); - hasher->add(password.c_str(), password.length()); - hasher->add(buf + 1, hex_size); + this->auth_buf_pos_ += read; - // Read cnonce and add to hash - if (!this->readall_(reinterpret_cast(buf), hex_size * 2)) { - this->log_auth_warning_(LOG_STR("Reading cnonce response"), name); - return false; + // Check if we still need more data + if (this->auth_buf_pos_ < to_read) { + return false; // More to read, try again next loop } - // Response is located after CNonce in the buffer + // We have all the data, verify it + char *buf = reinterpret_cast(this->auth_buf_.get()); const char *response = buf + hex_size; - hasher->add(buf, hex_size); // add CNonce in binary - hasher->calculate(); + // Add CNonce to hash + this->auth_hasher_->add(buf, hex_size); + this->auth_hasher_->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char log_buf[hex_size + 1]; - // Log CNonce for debugging - memcpy(log_buf, buf, hex_size); // Save CNonce for logging + // Log CNonce + memcpy(log_buf, buf, hex_size); log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s CNonce is %s", LOG_STR_ARG(name), log_buf); + ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); - // Log computed hash for debugging - hasher->get_hex(log_buf); + // Log computed hash + this->auth_hasher_->get_hex(log_buf); log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s Result is %s", LOG_STR_ARG(name), log_buf); + ESP_LOGV(TAG, "Auth: Result is %s", log_buf); // Log received response - memcpy(log_buf, response, hex_size); // Save response for logging + memcpy(log_buf, response, hex_size); log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: %s Response is %s", LOG_STR_ARG(name), log_buf); -#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Auth: Response is %s", log_buf); +#endif - // Compare response directly with digest in hasher - bool matches = hasher->equals_hex(response); + // Compare response + bool matches = this->auth_hasher_->equals_hex(response); if (!matches) { - this->log_auth_warning_(LOG_STR("Password mismatch"), name); + this->log_auth_warning_(LOG_STR("Password mismatch")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; } - return matches; + // Authentication successful - clean up auth state + this->cleanup_auth_(); + + return true; +} + +void ESPHomeOTAComponent::cleanup_auth_() { + this->auth_hasher_ = nullptr; + this->auth_buf_ = nullptr; + this->auth_buf_size_ = 0; + this->auth_buf_pos_ = 0; } #endif // USE_OTA_PASSWORD diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 7210d78fa9..55ae34d3af 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -20,7 +20,11 @@ class ESPHomeOTAComponent : public ota::OTAComponent { MAGIC_ACK, // Sending OK and version after magic bytes FEATURE_READ, // Reading feature flags from client FEATURE_ACK, // Sending feature acknowledgment - DATA, // Processing OTA data (authentication, update, etc.) +#ifdef USE_OTA_PASSWORD + AUTH_SEND, // Sending authentication request + AUTH_READ, // Reading authentication data +#endif // USE_OTA_PASSWORD + DATA, // BLOCKING! Processing OTA data (update, etc.) }; #ifdef USE_OTA_PASSWORD void set_auth_password(const std::string &password) { password_ = password; } @@ -40,9 +44,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void handle_handshake_(); void handle_data_(); #ifdef USE_OTA_PASSWORD - bool perform_hash_auth_(HashBase *hasher, const std::string &password, uint8_t auth_request, const LogString *name, - char *buf); - void log_auth_warning_(const LogString *action, const LogString *hash_name); + bool handle_auth_send_(); + bool handle_auth_read_(); + void cleanup_auth_(); + void log_auth_warning_(const LogString *msg); #endif // USE_OTA_PASSWORD bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); @@ -56,6 +61,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void log_start_(const LogString *phase); void log_remote_closed_(const LogString *during); void cleanup_connection_(); + void send_error_and_cleanup_(ota::OTAResponseTypes error); void yield_and_feed_watchdog_(); #ifdef USE_OTA_PASSWORD @@ -72,6 +78,12 @@ class ESPHomeOTAComponent : public ota::OTAComponent { OTAState ota_state_{OTAState::IDLE}; uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; +#ifdef USE_OTA_PASSWORD + std::unique_ptr auth_hasher_; + std::unique_ptr auth_buf_; + size_t auth_buf_size_{0}; + size_t auth_buf_pos_{0}; +#endif // USE_OTA_PASSWORD }; } // namespace esphome From 7251f7edec2fc0ec14ec3ae0a403a907fccf5b28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:13:21 -0500 Subject: [PATCH 10/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 221 ++++++++++++------ esphome/components/esphome/ota/ota_esphome.h | 4 +- 2 files changed, 151 insertions(+), 74 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index cabc14f1c1..e2425e0181 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -576,60 +576,59 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } bool ESPHomeOTAComponent::handle_auth_send_() { - // Determine which auth type to use based on platform capabilities and client support - uint8_t auth_type; + // Initialize auth buffer if not already done + if (!this->auth_buf_) { + // Determine which auth type to use and create hasher on stack + HashBase *hasher = nullptr; #ifdef USE_OTA_SHA256 - bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; + sha256::SHA256 sha256_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + +#ifdef USE_OTA_SHA256 + bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; #ifdef ALLOW_OTA_DOWNGRADE_MD5 - if (client_supports_sha256) { - auth_type = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - if (!this->auth_hasher_) { - this->auth_hasher_ = std::make_unique(); - } - } else { + if (client_supports_sha256) { + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + hasher = &sha256_hasher; + } else { #ifdef USE_OTA_MD5 - this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); - auth_type = ota::OTA_RESPONSE_REQUEST_AUTH; - if (!this->auth_hasher_) { - this->auth_hasher_ = std::make_unique(); - } + this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; + hasher = &md5_hasher; #else - this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; + this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; #endif // USE_OTA_MD5 - } + } #else // !ALLOW_OTA_DOWNGRADE_MD5 - if (!client_supports_sha256) { - this->log_auth_warning_(LOG_STR("Client requires SHA256")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; - } - auth_type = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - if (!this->auth_hasher_) { - this->auth_hasher_ = std::make_unique(); - } + if (!client_supports_sha256) { + this->log_auth_warning_(LOG_STR("Client requires SHA256")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + hasher = &sha256_hasher; #endif // ALLOW_OTA_DOWNGRADE_MD5 #else // !USE_OTA_SHA256 #ifdef USE_OTA_MD5 - auth_type = ota::OTA_RESPONSE_REQUEST_AUTH; - if (!this->auth_hasher_) { - this->auth_hasher_ = std::make_unique(); - } + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; + hasher = &md5_hasher; #else - this->log_auth_warning_(LOG_STR("No auth methods available")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; + this->log_auth_warning_(LOG_STR("No auth methods available")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; #endif // USE_OTA_MD5 #endif // USE_OTA_SHA256 - // Initialize auth buffer if not already done - if (!this->auth_buf_) { - // Calculate required buffer size - const size_t hex_size = this->auth_hasher_->get_size() * 2; - const size_t nonce_len = this->auth_hasher_->get_size() / 4; + // Calculate required buffer size using the hasher + const size_t hex_size = hasher->get_size() * 2; + const size_t nonce_len = hasher->get_size() / 4; // Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response) this->auth_buf_size_ = 1 + hex_size + hex_size * 2; this->auth_buf_ = std::make_unique(this->auth_buf_size_); @@ -643,13 +642,13 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } - this->auth_hasher_->init(); - this->auth_hasher_->add(buf, nonce_len); - this->auth_hasher_->calculate(); + hasher->init(); + hasher->add(buf, nonce_len); + hasher->calculate(); // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) - this->auth_buf_[0] = auth_type; - this->auth_hasher_->get_hex(buf); + this->auth_buf_[0] = this->auth_type_; + hasher->get_hex(buf); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Log nonce for debugging @@ -661,7 +660,19 @@ bool ESPHomeOTAComponent::handle_auth_send_() { } // Try to write auth_type + nonce - const size_t hex_size = this->auth_hasher_->get_size() * 2; + // Calculate hex_size based on auth_type + size_t hex_size; +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + hex_size = 64; // SHA256 = 32 bytes * 2 + } else +#endif + { +#ifdef USE_OTA_MD5 + hex_size = 32; // MD5 = 16 bytes * 2 +#endif + } + const size_t to_write = 1 + hex_size; size_t remaining = to_write - this->auth_buf_pos_; @@ -685,21 +696,41 @@ bool ESPHomeOTAComponent::handle_auth_send_() { // All written, prepare for reading phase this->auth_buf_pos_ = 0; - // Start challenge hash: password + nonce - this->auth_hasher_->init(); - this->auth_hasher_->add(this->password_.c_str(), this->password_.length()); - this->auth_hasher_->add(reinterpret_cast(this->auth_buf_.get() + 1), hex_size); + // We'll start the challenge hash in handle_auth_read_ when we have the cnonce return true; } bool ESPHomeOTAComponent::handle_auth_read_() { - const size_t hex_size = this->auth_hasher_->get_size() * 2; + // Calculate hex_size based on auth_type + size_t hex_size; +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + hex_size = 64; // SHA256 = 32 bytes * 2 + } else +#endif + { +#ifdef USE_OTA_MD5 + hex_size = 32; // MD5 = 16 bytes * 2 +#endif + } + const size_t to_read = hex_size * 2; // CNonce + Response - // Try to read remaining bytes + // Initialize buffer if not already done + if (!this->auth_buf_) { + // Note: we're reusing the buffer from handle_auth_send_ which should have the nonce + // But if we reach here without it, something went wrong + this->log_auth_warning_(LOG_STR("Auth buffer not initialized")); + this->cleanup_connection_(); + return false; + } + + // Try to read remaining bytes (CNonce + Response) + // We need to read into the buffer starting after the auth_type (1 byte) and nonce (hex_size bytes) + size_t offset = 1 + hex_size; size_t remaining = to_read - this->auth_buf_pos_; - ssize_t read = this->client_->read(this->auth_buf_.get() + this->auth_buf_pos_, remaining); + ssize_t read = this->client_->read(this->auth_buf_.get() + offset + this->auth_buf_pos_, remaining); if (read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { @@ -724,33 +755,79 @@ bool ESPHomeOTAComponent::handle_auth_read_() { } // We have all the data, verify it - char *buf = reinterpret_cast(this->auth_buf_.get()); - const char *response = buf + hex_size; + // Create hasher on stack based on auth_type + HashBase *hasher = nullptr; - // Add CNonce to hash - this->auth_hasher_->add(buf, hex_size); - this->auth_hasher_->calculate(); +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha256_hasher; + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + hasher = &sha256_hasher; + } +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { + hasher = &md5_hasher; + } +#endif + + if (!hasher) { + this->log_auth_warning_(LOG_STR("Invalid auth type")); + this->cleanup_connection_(); + return false; + } + + // Get pointers to the data + char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte + char *cnonce = reinterpret_cast(this->auth_buf_.get() + offset); + const char *response = cnonce + hex_size; + + // Calculate expected hash: password + nonce + cnonce + hasher->init(); + hasher->add(this->password_.c_str(), this->password_.length()); + hasher->add(nonce, hex_size); + hasher->add(cnonce, hex_size); + hasher->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[hex_size + 1]; - // Log CNonce - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); +#ifdef USE_OTA_SHA256 + char log_buf_sha[65]; // 64 hex chars + null terminator for SHA256 +#endif +#ifdef USE_OTA_MD5 + char log_buf_md5[33]; // 32 hex chars + null terminator for MD5 +#endif + char *log_buf = nullptr; +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + log_buf = log_buf_sha; + } +#endif +#ifdef USE_OTA_MD5 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { + log_buf = log_buf_md5; + } +#endif - // Log computed hash - this->auth_hasher_->get_hex(log_buf); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Result is %s", log_buf); + if (log_buf) { + // Log CNonce + memcpy(log_buf, cnonce, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); - // Log received response - memcpy(log_buf, response, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Response is %s", log_buf); + // Log computed hash + hasher->get_hex(log_buf); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Result is %s", log_buf); + + // Log received response + memcpy(log_buf, response, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Response is %s", log_buf); + } #endif // Compare response - bool matches = this->auth_hasher_->equals_hex(response); + bool matches = hasher->equals_hex(response); if (!matches) { this->log_auth_warning_(LOG_STR("Password mismatch")); @@ -765,10 +842,10 @@ bool ESPHomeOTAComponent::handle_auth_read_() { } void ESPHomeOTAComponent::cleanup_auth_() { - this->auth_hasher_ = nullptr; this->auth_buf_ = nullptr; this->auth_buf_size_ = 0; this->auth_buf_pos_ = 0; + this->auth_type_ = 0; } #endif // USE_OTA_PASSWORD diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 55ae34d3af..8b18ee8d46 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -79,11 +79,11 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; #ifdef USE_OTA_PASSWORD - std::unique_ptr auth_hasher_; std::unique_ptr auth_buf_; size_t auth_buf_size_{0}; size_t auth_buf_pos_{0}; -#endif // USE_OTA_PASSWORD + uint8_t auth_type_{0}; // Store auth type to know which hasher to use +#endif // USE_OTA_PASSWORD }; } // namespace esphome From 4b003389b8282d7c12029ba5fa0ec3d683da795e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:14:28 -0500 Subject: [PATCH 11/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index e2425e0181..fccf4106db 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -717,15 +717,6 @@ bool ESPHomeOTAComponent::handle_auth_read_() { const size_t to_read = hex_size * 2; // CNonce + Response - // Initialize buffer if not already done - if (!this->auth_buf_) { - // Note: we're reusing the buffer from handle_auth_send_ which should have the nonce - // But if we reach here without it, something went wrong - this->log_auth_warning_(LOG_STR("Auth buffer not initialized")); - this->cleanup_connection_(); - return false; - } - // Try to read remaining bytes (CNonce + Response) // We need to read into the buffer starting after the auth_type (1 byte) and nonce (hex_size bytes) size_t offset = 1 + hex_size; From 3b92c6630dcf2aaea28e77718e4c2326cf51b313 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:26:16 -0500 Subject: [PATCH 12/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 167 ++++++++---------- esphome/components/esphome/ota/ota_esphome.h | 3 + 2 files changed, 78 insertions(+), 92 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fccf4106db..b7dbed4e19 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -578,28 +578,17 @@ void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG bool ESPHomeOTAComponent::handle_auth_send_() { // Initialize auth buffer if not already done if (!this->auth_buf_) { - // Determine which auth type to use and create hasher on stack - HashBase *hasher = nullptr; - -#ifdef USE_OTA_SHA256 - sha256::SHA256 sha256_hasher; -#endif -#ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; -#endif - + // Determine which auth type to use #ifdef USE_OTA_SHA256 bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; #ifdef ALLOW_OTA_DOWNGRADE_MD5 if (client_supports_sha256) { this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - hasher = &sha256_hasher; } else { #ifdef USE_OTA_MD5 this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; - hasher = &md5_hasher; #else this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); @@ -613,12 +602,10 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - hasher = &sha256_hasher; #endif // ALLOW_OTA_DOWNGRADE_MD5 #else // !USE_OTA_SHA256 #ifdef USE_OTA_MD5 this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; - hasher = &md5_hasher; #else this->log_auth_warning_(LOG_STR("No auth methods available")); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); @@ -626,53 +613,28 @@ bool ESPHomeOTAComponent::handle_auth_send_() { #endif // USE_OTA_MD5 #endif // USE_OTA_SHA256 - // Calculate required buffer size using the hasher - const size_t hex_size = hasher->get_size() * 2; - const size_t nonce_len = hasher->get_size() / 4; - // Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response) - this->auth_buf_size_ = 1 + hex_size + hex_size * 2; - this->auth_buf_ = std::make_unique(this->auth_buf_size_); - this->auth_buf_pos_ = 0; + // Generate nonce with appropriate hasher + bool success = false; +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + sha256::SHA256 sha_hasher; + success = this->prepare_auth_nonce_(&sha_hasher); + } +#endif +#ifdef USE_OTA_MD5 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { + md5::MD5Digest md5_hasher; + success = this->prepare_auth_nonce_(&md5_hasher); + } +#endif - // Generate nonce - char *buf = reinterpret_cast(this->auth_buf_.get() + 1); - if (!random_bytes(reinterpret_cast(buf), nonce_len)) { - this->log_auth_warning_(LOG_STR("Random bytes generation failed")); - this->cleanup_connection_(); + if (!success) { return false; } - - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); - - // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) - this->auth_buf_[0] = this->auth_type_; - hasher->get_hex(buf); - -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - // Log nonce for debugging - char log_buf[hex_size + 1]; - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); -#endif } // Try to write auth_type + nonce - // Calculate hex_size based on auth_type - size_t hex_size; -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hex_size = 64; // SHA256 = 32 bytes * 2 - } else -#endif - { -#ifdef USE_OTA_MD5 - hex_size = 32; // MD5 = 16 bytes * 2 -#endif - } - + size_t hex_size = this->get_auth_hex_size_(); const size_t to_write = 1 + hex_size; size_t remaining = to_write - this->auth_buf_pos_; @@ -695,26 +657,11 @@ bool ESPHomeOTAComponent::handle_auth_send_() { // All written, prepare for reading phase this->auth_buf_pos_ = 0; - - // We'll start the challenge hash in handle_auth_read_ when we have the cnonce - return true; } bool ESPHomeOTAComponent::handle_auth_read_() { - // Calculate hex_size based on auth_type - size_t hex_size; -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hex_size = 64; // SHA256 = 32 bytes * 2 - } else -#endif - { -#ifdef USE_OTA_MD5 - hex_size = 32; // MD5 = 16 bytes * 2 -#endif - } - + size_t hex_size = this->get_auth_hex_size_(); const size_t to_read = hex_size * 2; // CNonce + Response // Try to read remaining bytes (CNonce + Response) @@ -746,29 +693,72 @@ bool ESPHomeOTAComponent::handle_auth_read_() { } // We have all the data, verify it - // Create hasher on stack based on auth_type - HashBase *hasher = nullptr; + bool matches = false; #ifdef USE_OTA_SHA256 - sha256::SHA256 sha256_hasher; if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hasher = &sha256_hasher; + sha256::SHA256 sha_hasher; + matches = this->verify_hash_auth_(&sha_hasher, hex_size); } #endif #ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - hasher = &md5_hasher; + md5::MD5Digest md5_hasher; + matches = this->verify_hash_auth_(&md5_hasher, hex_size); } #endif - if (!hasher) { - this->log_auth_warning_(LOG_STR("Invalid auth type")); + if (!matches) { + this->log_auth_warning_(LOG_STR("Password mismatch")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + + // Authentication successful - clean up auth state + this->cleanup_auth_(); + + return true; +} + +bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { + // Calculate required buffer size using the hasher + const size_t hex_size = hasher->get_size() * 2; + const size_t nonce_len = hasher->get_size() / 4; + // Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response) + this->auth_buf_size_ = 1 + hex_size + hex_size * 2; + this->auth_buf_ = std::make_unique(this->auth_buf_size_); + this->auth_buf_pos_ = 0; + + // Generate nonce + char *buf = reinterpret_cast(this->auth_buf_.get() + 1); + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { + this->log_auth_warning_(LOG_STR("Random bytes generation failed")); this->cleanup_connection_(); return false; } + hasher->init(); + hasher->add(buf, nonce_len); + hasher->calculate(); + + // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes) + this->auth_buf_[0] = this->auth_type_; + hasher->get_hex(buf); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[hex_size + 1]; + // Log nonce for debugging + memcpy(log_buf, buf, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); +#endif + + return true; +} + +bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { // Get pointers to the data + size_t offset = 1 + hex_size; // Skip auth_type byte and nonce char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte char *cnonce = reinterpret_cast(this->auth_buf_.get() + offset); const char *response = cnonce + hex_size; @@ -789,12 +779,12 @@ bool ESPHomeOTAComponent::handle_auth_read_() { #endif char *log_buf = nullptr; #ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + if (hex_size == 64) { log_buf = log_buf_sha; } #endif #ifdef USE_OTA_MD5 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { + if (hex_size == 32) { log_buf = log_buf_md5; } #endif @@ -818,18 +808,11 @@ bool ESPHomeOTAComponent::handle_auth_read_() { #endif // Compare response - bool matches = hasher->equals_hex(response); + return hasher->equals_hex(response); +} - if (!matches) { - this->log_auth_warning_(LOG_STR("Password mismatch")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; - } - - // Authentication successful - clean up auth state - this->cleanup_auth_(); - - return true; +size_t ESPHomeOTAComponent::get_auth_hex_size_() const { + return this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH ? 64 : 32; } void ESPHomeOTAComponent::cleanup_auth_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 8b18ee8d46..15d1aac914 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -46,6 +46,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #ifdef USE_OTA_PASSWORD bool handle_auth_send_(); bool handle_auth_read_(); + bool prepare_auth_nonce_(HashBase *hasher); + bool verify_hash_auth_(HashBase *hasher, size_t hex_size); + size_t get_auth_hex_size_() const; void cleanup_auth_(); void log_auth_warning_(const LogString *msg); #endif // USE_OTA_PASSWORD From e2c637cf489e902bec6b084618fe7e847ab3848a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:26:54 -0500 Subject: [PATCH 13/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 4 ++-- esphome/components/esphome/ota/ota_esphome.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b7dbed4e19..b39ab5222a 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -725,8 +725,8 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { const size_t hex_size = hasher->get_size() * 2; const size_t nonce_len = hasher->get_size() / 4; // Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response) - this->auth_buf_size_ = 1 + hex_size + hex_size * 2; - this->auth_buf_ = std::make_unique(this->auth_buf_size_); + const size_t auth_buf_size = 1 + hex_size + hex_size * 2; + this->auth_buf_ = std::make_unique(auth_buf_size); this->auth_buf_pos_ = 0; // Generate nonce diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 15d1aac914..ce19d52253 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -83,7 +83,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint8_t ota_features_{0}; #ifdef USE_OTA_PASSWORD std::unique_ptr auth_buf_; - size_t auth_buf_size_{0}; size_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD From 0fb3d7550e15f76a77cd9185c3dae4a78d760bf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:27:04 -0500 Subject: [PATCH 14/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b39ab5222a..e2ccbf0c94 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -817,7 +817,6 @@ size_t ESPHomeOTAComponent::get_auth_hex_size_() const { void ESPHomeOTAComponent::cleanup_auth_() { this->auth_buf_ = nullptr; - this->auth_buf_size_ = 0; this->auth_buf_pos_ = 0; this->auth_type_ = 0; } From d1d8efd5a2ce80fe4d7924ce34c0111329506379 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:28:03 -0500 Subject: [PATCH 15/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index e2ccbf0c94..90b08ad826 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -771,40 +771,21 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { hasher->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE -#ifdef USE_OTA_SHA256 - char log_buf_sha[65]; // 64 hex chars + null terminator for SHA256 -#endif -#ifdef USE_OTA_MD5 - char log_buf_md5[33]; // 32 hex chars + null terminator for MD5 -#endif - char *log_buf = nullptr; -#ifdef USE_OTA_SHA256 - if (hex_size == 64) { - log_buf = log_buf_sha; - } -#endif -#ifdef USE_OTA_MD5 - if (hex_size == 32) { - log_buf = log_buf_md5; - } -#endif + char log_buf[hex_size + 1]; + // Log CNonce + memcpy(log_buf, cnonce, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); - if (log_buf) { - // Log CNonce - memcpy(log_buf, cnonce, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); + // Log computed hash + hasher->get_hex(log_buf); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Result is %s", log_buf); - // Log computed hash - hasher->get_hex(log_buf); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Result is %s", log_buf); - - // Log received response - memcpy(log_buf, response, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Response is %s", log_buf); - } + // Log received response + memcpy(log_buf, response, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Response is %s", log_buf); #endif // Compare response From a2d3e81c4ec79eb2f73eab9e9b8bc5b4651e43ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:29:09 -0500 Subject: [PATCH 16/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 90b08ad826..fbc14a0747 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -724,8 +724,11 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { // Calculate required buffer size using the hasher const size_t hex_size = hasher->get_size() * 2; const size_t nonce_len = hasher->get_size() / 4; - // Buffer needs: 1 (auth_type) + hex_size (nonce) + hex_size*2 (cnonce+response) - const size_t auth_buf_size = 1 + hex_size + hex_size * 2; + // Buffer needs to hold max of: + // - During send: auth_type (1) + nonce (hex_size) + // - During read: cnonce (hex_size) + response (hex_size) + // So max is hex_size * 2 + const size_t auth_buf_size = hex_size * 2; this->auth_buf_ = std::make_unique(auth_buf_size); this->auth_buf_pos_ = 0; From 9f421ca60c270619408e001f62786d6dafb54e89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:32:02 -0500 Subject: [PATCH 17/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fbc14a0747..b55a7ee191 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -724,11 +724,11 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { // Calculate required buffer size using the hasher const size_t hex_size = hasher->get_size() * 2; const size_t nonce_len = hasher->get_size() / 4; - // Buffer needs to hold max of: - // - During send: auth_type (1) + nonce (hex_size) - // - During read: cnonce (hex_size) + response (hex_size) - // So max is hex_size * 2 - const size_t auth_buf_size = hex_size * 2; + // Buffer layout: + // - auth_type (1 byte) + nonce (hex_size) - sent in AUTH_SEND + // - cnonce (hex_size) + response (hex_size) - read in AUTH_READ at offset 1+hex_size + // Total: 1 + hex_size + (hex_size * 2) + const size_t auth_buf_size = 1 + hex_size + hex_size * 2; this->auth_buf_ = std::make_unique(auth_buf_size); this->auth_buf_pos_ = 0; From 6430ae80cf51eebe713bc4be7e0d4c30d6e39b89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:38:13 -0500 Subject: [PATCH 18/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b55a7ee191..1e8c61778f 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -665,10 +665,10 @@ bool ESPHomeOTAComponent::handle_auth_read_() { const size_t to_read = hex_size * 2; // CNonce + Response // Try to read remaining bytes (CNonce + Response) - // We need to read into the buffer starting after the auth_type (1 byte) and nonce (hex_size bytes) - size_t offset = 1 + hex_size; + // We read cnonce+response starting at offset 1+hex_size (after auth_type and our nonce) + size_t cnonce_offset = 1 + hex_size; // Offset where cnonce should be stored in buffer size_t remaining = to_read - this->auth_buf_pos_; - ssize_t read = this->client_->read(this->auth_buf_.get() + offset + this->auth_buf_pos_, remaining); + ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining); if (read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { @@ -760,11 +760,17 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { } bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { - // Get pointers to the data - size_t offset = 1 + hex_size; // Skip auth_type byte and nonce + // Buffer layout after AUTH_READ completes: + // [0]: auth_type (1 byte) + // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND + // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce + // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash + + // Get pointers to the data in the buffer char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte - char *cnonce = reinterpret_cast(this->auth_buf_.get() + offset); - const char *response = cnonce + hex_size; + size_t cnonce_offset = 1 + hex_size; // Offset where cnonce starts in buffer + char *cnonce = reinterpret_cast(this->auth_buf_.get() + cnonce_offset); + const char *response = cnonce + hex_size; // Response immediately follows cnonce // Calculate expected hash: password + nonce + cnonce hasher->init(); From e5868a79a2ae3d383fdc4ea59fb8796ef9e41948 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:40:50 -0500 Subject: [PATCH 19/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 79 +++++++++++-------- esphome/components/esphome/ota/ota_esphome.h | 1 + 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 1e8c61778f..52e996f660 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -575,43 +575,58 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { #ifdef USE_OTA_PASSWORD void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } +bool ESPHomeOTAComponent::select_auth_type_() { +#ifdef USE_OTA_SHA256 + bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; + +#ifdef ALLOW_OTA_DOWNGRADE_MD5 + // Allow fallback to MD5 if client doesn't support SHA256 + if (client_supports_sha256) { + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + return true; + } +#ifdef USE_OTA_MD5 + this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; + return true; +#else + this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; +#endif // USE_OTA_MD5 + +#else // !ALLOW_OTA_DOWNGRADE_MD5 + // Require SHA256 + if (!client_supports_sha256) { + this->log_auth_warning_(LOG_STR("Client requires SHA256")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + return true; +#endif // ALLOW_OTA_DOWNGRADE_MD5 + +#else // !USE_OTA_SHA256 +#ifdef USE_OTA_MD5 + // Only MD5 available + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; + return true; +#else + // No auth methods available + this->log_auth_warning_(LOG_STR("No auth methods available")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; +#endif // USE_OTA_MD5 +#endif // USE_OTA_SHA256 +} + bool ESPHomeOTAComponent::handle_auth_send_() { // Initialize auth buffer if not already done if (!this->auth_buf_) { - // Determine which auth type to use -#ifdef USE_OTA_SHA256 - bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; - -#ifdef ALLOW_OTA_DOWNGRADE_MD5 - if (client_supports_sha256) { - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - } else { -#ifdef USE_OTA_MD5 - this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; -#else - this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; -#endif // USE_OTA_MD5 - } -#else // !ALLOW_OTA_DOWNGRADE_MD5 - if (!client_supports_sha256) { - this->log_auth_warning_(LOG_STR("Client requires SHA256")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + // Select auth type based on client capabilities and configuration + if (!this->select_auth_type_()) { return false; } - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; -#endif // ALLOW_OTA_DOWNGRADE_MD5 -#else // !USE_OTA_SHA256 -#ifdef USE_OTA_MD5 - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; -#else - this->log_auth_warning_(LOG_STR("No auth methods available")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; -#endif // USE_OTA_MD5 -#endif // USE_OTA_SHA256 // Generate nonce with appropriate hasher bool success = false; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index ce19d52253..680c5788b9 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -46,6 +46,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #ifdef USE_OTA_PASSWORD bool handle_auth_send_(); bool handle_auth_read_(); + bool select_auth_type_(); bool prepare_auth_nonce_(HashBase *hasher); bool verify_hash_auth_(HashBase *hasher, size_t hex_size); size_t get_auth_hex_size_() const; From 2d6669068fa95a5fd7de61796eeaaee0d662f3ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:41:40 -0500 Subject: [PATCH 20/34] stack it --- esphome/components/esphome/ota/ota_esphome.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 680c5788b9..cd4a5d7a40 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -84,7 +84,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint8_t ota_features_{0}; #ifdef USE_OTA_PASSWORD std::unique_ptr auth_buf_; - size_t auth_buf_pos_{0}; + uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD }; From 5fb99e901304092e1ef2c0836f5e0072ac22fe8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:42:58 -0500 Subject: [PATCH 21/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 52e996f660..6016160159 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -510,11 +510,7 @@ bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, if (read <= 0) { // Error or connection closed - if (read == -1) { - this->log_socket_error_(error_desc); - } else { - this->log_remote_closed_(close_desc); - } + read == -1 ? this->log_socket_error_(error_desc) : this->log_remote_closed_(close_desc); this->cleanup_connection_(); return false; } From 7e8de7c92cea8ad9bde9c5c519c3328cd0aaeb0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:44:24 -0500 Subject: [PATCH 22/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 6016160159..7272f1d208 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -509,8 +509,7 @@ bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, } if (read <= 0) { - // Error or connection closed - read == -1 ? this->log_socket_error_(error_desc) : this->log_remote_closed_(close_desc); + read == 0 ? this->log_remote_closed_(close_desc) : this->log_socket_error_(error_desc); this->cleanup_connection_(); return false; } From 20cbc48ad4639734dd68f063a3e3a881a97ae2dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:45:56 -0500 Subject: [PATCH 23/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 7272f1d208..872a8be8f4 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -734,11 +734,14 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { // Calculate required buffer size using the hasher const size_t hex_size = hasher->get_size() * 2; const size_t nonce_len = hasher->get_size() / 4; - // Buffer layout: - // - auth_type (1 byte) + nonce (hex_size) - sent in AUTH_SEND - // - cnonce (hex_size) + response (hex_size) - read in AUTH_READ at offset 1+hex_size - // Total: 1 + hex_size + (hex_size * 2) - const size_t auth_buf_size = 1 + hex_size + hex_size * 2; + + // Buffer layout after AUTH_READ completes: + // [0]: auth_type (1 byte) + // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND + // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce + // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash + // Total: 1 + 3*hex_size + const size_t auth_buf_size = 1 + 3 * hex_size; this->auth_buf_ = std::make_unique(auth_buf_size); this->auth_buf_pos_ = 0; @@ -770,13 +773,7 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { } bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { - // Buffer layout after AUTH_READ completes: - // [0]: auth_type (1 byte) - // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND - // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce - // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - - // Get pointers to the data in the buffer + // Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout) char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte size_t cnonce_offset = 1 + hex_size; // Offset where cnonce starts in buffer char *cnonce = reinterpret_cast(this->auth_buf_.get() + cnonce_offset); From dba680a748555ec22f7137edcb5ad2848ce72417 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:52:48 -0500 Subject: [PATCH 24/34] stack it --- .../components/esphome/ota/ota_esphome.cpp | 65 +++++++++---------- esphome/components/esphome/ota/ota_esphome.h | 4 ++ 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 872a8be8f4..5d2da9d93d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -344,7 +344,7 @@ void ESPHomeOTAComponent::handle_data_() { size_t requested = std::min(sizeof(buf), ota_size - total); ssize_t read = this->client_->read(buf, requested); if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (this->would_block_(errno)) { this->yield_and_feed_watchdog_(); continue; } @@ -442,7 +442,7 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (!this->would_block_(errno)) { ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno); return false; } @@ -469,7 +469,7 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { + if (!this->would_block_(errno)) { ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno); return false; } @@ -499,12 +499,8 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed during %s", LOG_STR_ARG(during)); } -bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc) { - // Read bytes into handshake buffer, starting at handshake_buf_pos_ - size_t bytes_to_read = to_read - this->handshake_buf_pos_; - ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); - - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { +bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *error_desc, const LogString *close_desc) { + if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop } @@ -513,6 +509,29 @@ bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, this->cleanup_connection_(); return false; } + return true; +} + +bool ESPHomeOTAComponent::handle_write_error_(ssize_t written, const LogString *error_desc) { + if (written == -1) { + if (this->would_block_(errno)) { + return false; // Try again next loop + } + this->log_socket_error_(error_desc); + this->cleanup_connection_(); + return false; + } + return true; +} + +bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc) { + // Read bytes into handshake buffer, starting at handshake_buf_pos_ + size_t bytes_to_read = to_read - this->handshake_buf_pos_; + ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); + + if (!this->handle_read_error_(read, error_desc, close_desc)) { + return false; + } this->handshake_buf_pos_ += read; // Return true only if we have all the requested bytes @@ -524,12 +543,7 @@ bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *error_des size_t bytes_to_write = to_write - this->handshake_buf_pos_; ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write); - if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return false; // Try again next loop - } - this->log_socket_error_(error_desc); - this->cleanup_connection_(); + if (!this->handle_write_error_(written, error_desc)) { return false; } @@ -649,12 +663,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { size_t remaining = to_write - this->auth_buf_pos_; ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining); - if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return false; // Try again next loop - } - this->log_auth_warning_(LOG_STR("Writing auth type and nonce failed")); - this->cleanup_connection_(); + if (!this->handle_write_error_(written, LOG_STR("auth write"))) { return false; } @@ -680,18 +689,8 @@ bool ESPHomeOTAComponent::handle_auth_read_() { size_t remaining = to_read - this->auth_buf_pos_; ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining); - if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return false; // Try again next loop - } - this->log_auth_warning_(LOG_STR("Reading cnonce response failed")); - this->cleanup_connection_(); - return false; - } - - if (read == 0) { - this->log_auth_warning_(LOG_STR("Remote closed during auth read")); - this->cleanup_connection_(); + auto *auth_read_desc = LOG_STR("auth read"); + if (!this->handle_read_error_(read, auth_read_desc, auth_read_desc)) { return false; } diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index cd4a5d7a40..6f7bef550a 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -58,6 +58,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc); bool try_write_(size_t to_write, const LogString *error_desc); + + bool would_block_(int error_code) const { return error_code == EAGAIN || error_code == EWOULDBLOCK; } + bool handle_read_error_(ssize_t read, const LogString *error_desc, const LogString *close_desc); + bool handle_write_error_(ssize_t written, const LogString *error_desc); void transition_ota_state_(OTAState next_state); void log_socket_error_(const LogString *msg); From c789fbf9f340cb14a5b1963adaf77a30e86d9d5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:54:21 -0500 Subject: [PATCH 25/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 5d2da9d93d..c63eb929d3 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -341,7 +341,9 @@ void ESPHomeOTAComponent::handle_data_() { while (total < ota_size) { // TODO: timeout check - size_t requested = std::min(sizeof(buf), ota_size - total); + size_t remaining = ota_size - total; + const size_t buf_size = sizeof(buf); + size_t requested = remaining < buf_size ? remaining : buf_size; ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (this->would_block_(errno)) { From 9875e96b13602cc1b153b951fc5b224a6930daee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Sep 2025 23:56:11 -0500 Subject: [PATCH 26/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index c63eb929d3..9c698822ba 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -28,6 +28,7 @@ namespace esphome { static const char *const TAG = "esphome.ota"; static constexpr uint16_t OTA_BLOCK_SIZE = 8192; +static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer @@ -276,7 +277,8 @@ void ESPHomeOTAComponent::handle_data_() { bool update_started = false; size_t total = 0; uint32_t last_progress = 0; - uint8_t buf[1024]; + uint8_t buf[OTA_BUFFER_SIZE]; + const size_t buf_size = sizeof(buf); char *sbuf = reinterpret_cast(buf); size_t ota_size; #if USE_OTA_VERSION == 2 @@ -342,7 +344,6 @@ void ESPHomeOTAComponent::handle_data_() { while (total < ota_size) { // TODO: timeout check size_t remaining = ota_size - total; - const size_t buf_size = sizeof(buf); size_t requested = remaining < buf_size ? remaining : buf_size; ssize_t read = this->client_->read(buf, requested); if (read == -1) { From 93ca48d9aa9d0fd42c1c5157f33f41243c42402a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 00:00:00 -0500 Subject: [PATCH 27/34] stack it --- esphome/components/esphome/ota/ota_esphome.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 9c698822ba..d213f872e0 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -32,6 +32,15 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer +#ifdef USE_OTA_PASSWORD +#ifdef USE_OTA_MD5 +static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2) +#endif +#ifdef USE_OTA_SHA256 +static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2) +#endif +#endif // USE_OTA_PASSWORD + void ESPHomeOTAComponent::setup() { #ifdef USE_OTA_STATE_CALLBACK ota::register_ota_platform(this); @@ -811,7 +820,14 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { } size_t ESPHomeOTAComponent::get_auth_hex_size_() const { - return this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH ? 64 : 32; +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + return SHA256_HEX_SIZE; + } +#endif +#ifdef USE_OTA_MD5 + return MD5_HEX_SIZE; +#endif } void ESPHomeOTAComponent::cleanup_auth_() { From 9cdd4bc555f4581409eb90ffb66d89e838f24793 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 00:15:57 -0500 Subject: [PATCH 28/34] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index d213f872e0..ae6e7c576f 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -607,11 +607,11 @@ bool ESPHomeOTAComponent::select_auth_type_() { return true; } #ifdef USE_OTA_MD5 - this->log_auth_warning_(LOG_STR("Using MD5 for compatibility (deprecated)")); + this->log_auth_warning_(LOG_STR("Using deprecated MD5")); this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; return true; #else - this->log_auth_warning_(LOG_STR("Client doesn't support SHA256 and MD5 is disabled")); + this->log_auth_warning_(LOG_STR("SHA256 required")); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); return false; #endif // USE_OTA_MD5 @@ -619,7 +619,7 @@ bool ESPHomeOTAComponent::select_auth_type_() { #else // !ALLOW_OTA_DOWNGRADE_MD5 // Require SHA256 if (!client_supports_sha256) { - this->log_auth_warning_(LOG_STR("Client requires SHA256")); + this->log_auth_warning_(LOG_STR("SHA256 required")); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); return false; } @@ -759,7 +759,7 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { // Generate nonce char *buf = reinterpret_cast(this->auth_buf_.get() + 1); if (!random_bytes(reinterpret_cast(buf), nonce_len)) { - this->log_auth_warning_(LOG_STR("Random bytes generation failed")); + this->log_auth_warning_(LOG_STR("Random failed")); this->cleanup_connection_(); return false; } From 5abde23432da07ea5a0c55278e16261cfda3f205 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 01:08:10 -0500 Subject: [PATCH 29/34] merge --- .../components/esphome/ota/ota_esphome.cpp | 107 ++++++------------ esphome/components/esphome/ota/ota_esphome.h | 21 ++-- 2 files changed, 50 insertions(+), 78 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index ae6e7c576f..fe2625f15e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -124,11 +124,11 @@ static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; #define ALLOW_OTA_DOWNGRADE_MD5 void ESPHomeOTAComponent::handle_handshake_() { - /// Handle the initial OTA handshake. + /// Handle the OTA handshake and authentication. /// /// This method is non-blocking and will return immediately if no data is available. - /// It reads all 5 magic bytes (0x6C, 0x26, 0xF7, 0x5C, 0x45) non-blocking - /// before proceeding to handle_data_(). A 10-second timeout is enforced from initial connection. + /// It manages the state machine through connection, magic bytes validation, feature + /// negotiation, and authentication before entering the blocking data transfer phase. if (this->client_ == nullptr) { // We already checked server_->ready() in loop(), so we can accept directly @@ -168,7 +168,7 @@ void ESPHomeOTAComponent::handle_handshake_() { switch (this->ota_state_) { case OTAState::MAGIC_READ: { // Try to read remaining magic bytes (5 total) - if (!this->try_read_(5, LOG_STR("reading magic bytes"), LOG_STR("handshake"))) { + if (!this->try_read_(5, LOG_STR("read magic"))) { return; } @@ -183,21 +183,16 @@ void ESPHomeOTAComponent::handle_handshake_() { // Magic bytes valid, move to next state this->transition_ota_state_(OTAState::MAGIC_ACK); + this->handshake_buf_[0] = ota::OTA_RESPONSE_OK; + this->handshake_buf_[1] = USE_OTA_VERSION; [[fallthrough]]; } case OTAState::MAGIC_ACK: { // Send OK and version - 2 bytes - // Prepare response in handshake buffer if not already done - if (this->handshake_buf_pos_ == 0) { - this->handshake_buf_[0] = ota::OTA_RESPONSE_OK; - this->handshake_buf_[1] = USE_OTA_VERSION; - } - - if (!this->try_write_(2, LOG_STR("writing magic ack"))) { + if (!this->try_write_(2, LOG_STR("ack magic"))) { return; } - // All bytes sent, create backend and move to next state this->backend_ = ota::make_ota_backend(); this->transition_ota_state_(OTAState::FEATURE_READ); @@ -206,30 +201,24 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::FEATURE_READ: { // Read features - 1 byte - if (!this->try_read_(1, LOG_STR("reading features"), LOG_STR("feature read"))) { + if (!this->try_read_(1, LOG_STR("read feature"))) { return; } - this->ota_features_ = this->handshake_buf_[0]; ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); this->transition_ota_state_(OTAState::FEATURE_ACK); + this->handshake_buf_[0] = + ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) + ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION + : ota::OTA_RESPONSE_HEADER_OK; [[fallthrough]]; } case OTAState::FEATURE_ACK: { // Acknowledge header - 1 byte - // Prepare response in handshake buffer if not already done - if (this->handshake_buf_pos_ == 0) { - this->handshake_buf_[0] = - ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) - ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION - : ota::OTA_RESPONSE_HEADER_OK; - } - - if (!this->try_write_(1, LOG_STR("writing feature ack"))) { + if (!this->try_write_(1, LOG_STR("ack feature"))) { return; } - #ifdef USE_OTA_PASSWORD // If password is set, move to auth phase if (!this->password_.empty()) { @@ -266,11 +255,10 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::DATA: this->handle_data_(); - return; + [[fallthrough]]; - case OTAState::IDLE: - // This shouldn't happen - return; + default: + break; } } @@ -287,20 +275,12 @@ void ESPHomeOTAComponent::handle_data_() { size_t total = 0; uint32_t last_progress = 0; uint8_t buf[OTA_BUFFER_SIZE]; - const size_t buf_size = sizeof(buf); char *sbuf = reinterpret_cast(buf); size_t ota_size; #if USE_OTA_VERSION == 2 size_t size_acknowledged = 0; #endif - // The handshake and auth have already been completed - // We already have: - // - this->backend_ created - // - this->ota_features_ set - // - Feature acknowledgment sent - // - Authentication completed (if password was set) - // Acknowledge auth OK - 1 byte buf[0] = ota::OTA_RESPONSE_AUTH_OK; this->writeall_(buf, 1); @@ -353,26 +333,23 @@ void ESPHomeOTAComponent::handle_data_() { while (total < ota_size) { // TODO: timeout check size_t remaining = ota_size - total; - size_t requested = remaining < buf_size ? remaining : buf_size; + size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE; ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (this->would_block_(errno)) { this->yield_and_feed_watchdog_(); continue; } - ESP_LOGW(TAG, "Read error, errno %d", errno); + ESP_LOGW(TAG, "Read err %d", errno); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } else if (read == 0) { - // $ man recv - // "When a stream socket peer has performed an orderly shutdown, the return value will - // be 0 (the traditional "end-of-file" return)." - ESP_LOGW(TAG, "Remote closed connection"); + ESP_LOGW(TAG, "Remote closed"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } error_code = this->backend_->write(buf, read); if (error_code != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Flash write error, code: %d", error_code); + ESP_LOGW(TAG, "Flash write err %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } total += read; @@ -403,7 +380,7 @@ void ESPHomeOTAComponent::handle_data_() { error_code = this->backend_->end(); if (error_code != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error ending update! code: %d", error_code); + ESP_LOGW(TAG, "End update err %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -455,11 +432,11 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno); + ESP_LOGW(TAG, "Read err %d bytes, errno %d", len, errno); return false; } } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed connection"); + ESP_LOGW(TAG, "Remote closed"); return false; } else { at += read; @@ -482,7 +459,7 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno); + ESP_LOGW(TAG, "Write err %d bytes, errno %d", len, errno); return false; } } else { @@ -508,40 +485,40 @@ void ESPHomeOTAComponent::log_start_(const LogString *phase) { } void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { - ESP_LOGW(TAG, "Remote closed during %s", LOG_STR_ARG(during)); + ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); } -bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *error_desc, const LogString *close_desc) { +bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop } if (read <= 0) { - read == 0 ? this->log_remote_closed_(close_desc) : this->log_socket_error_(error_desc); + read == 0 ? this->log_remote_closed_(desc) : this->log_socket_error_(desc); this->cleanup_connection_(); return false; } return true; } -bool ESPHomeOTAComponent::handle_write_error_(ssize_t written, const LogString *error_desc) { +bool ESPHomeOTAComponent::handle_write_error_(ssize_t written, const LogString *desc) { if (written == -1) { if (this->would_block_(errno)) { return false; // Try again next loop } - this->log_socket_error_(error_desc); + this->log_socket_error_(desc); this->cleanup_connection_(); return false; } return true; } -bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc) { +bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *desc) { // Read bytes into handshake buffer, starting at handshake_buf_pos_ size_t bytes_to_read = to_read - this->handshake_buf_pos_; ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); - if (!this->handle_read_error_(read, error_desc, close_desc)) { + if (!this->handle_read_error_(read, desc)) { return false; } @@ -550,12 +527,12 @@ bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *error_desc, return this->handshake_buf_pos_ >= to_read; } -bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *error_desc) { +bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) { // Write bytes from handshake buffer, starting at handshake_buf_pos_ size_t bytes_to_write = to_write - this->handshake_buf_pos_; ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write); - if (!this->handle_write_error_(written, error_desc)) { + if (!this->handle_write_error_(written, desc)) { return false; } @@ -564,11 +541,6 @@ bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *error_des return this->handshake_buf_pos_ >= to_write; } -void ESPHomeOTAComponent::transition_ota_state_(OTAState next_state) { - this->ota_state_ = next_state; - this->handshake_buf_pos_ = 0; // Reset buffer position for next state -} - void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; @@ -582,12 +554,6 @@ void ESPHomeOTAComponent::cleanup_connection_() { #endif } -void ESPHomeOTAComponent::send_error_and_cleanup_(ota::OTAResponseTypes error) { - uint8_t error_byte = static_cast(error); - this->client_->write(&error_byte, 1); // Best effort, non-blocking - this->cleanup_connection_(); -} - void ESPHomeOTAComponent::yield_and_feed_watchdog_() { App.feed_wdt(); delay(1); @@ -675,7 +641,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { size_t remaining = to_write - this->auth_buf_pos_; ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining); - if (!this->handle_write_error_(written, LOG_STR("auth write"))) { + if (!this->handle_write_error_(written, LOG_STR("ack auth"))) { return false; } @@ -701,8 +667,7 @@ bool ESPHomeOTAComponent::handle_auth_read_() { size_t remaining = to_read - this->auth_buf_pos_; ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining); - auto *auth_read_desc = LOG_STR("auth read"); - if (!this->handle_read_error_(read, auth_read_desc, auth_read_desc)) { + if (!this->handle_read_error_(read, LOG_STR("read auth"))) { return false; } @@ -760,7 +725,7 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { char *buf = reinterpret_cast(this->auth_buf_.get() + 1); if (!random_bytes(reinterpret_cast(buf), nonce_len)) { this->log_auth_warning_(LOG_STR("Random failed")); - this->cleanup_connection_(); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); return false; } diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 6f7bef550a..1e26494fd0 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -56,20 +56,27 @@ class ESPHomeOTAComponent : public ota::OTAComponent { bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); - bool try_read_(size_t to_read, const LogString *error_desc, const LogString *close_desc); - bool try_write_(size_t to_write, const LogString *error_desc); + bool try_read_(size_t to_read, const LogString *desc); + bool try_write_(size_t to_write, const LogString *desc); - bool would_block_(int error_code) const { return error_code == EAGAIN || error_code == EWOULDBLOCK; } - bool handle_read_error_(ssize_t read, const LogString *error_desc, const LogString *close_desc); - bool handle_write_error_(ssize_t written, const LogString *error_desc); - void transition_ota_state_(OTAState next_state); + inline bool would_block_(int error_code) const { return error_code == EAGAIN || error_code == EWOULDBLOCK; } + bool handle_read_error_(ssize_t read, const LogString *desc); + bool handle_write_error_(ssize_t written, const LogString *desc); + inline void transition_ota_state_(OTAState next_state) { + this->ota_state_ = next_state; + this->handshake_buf_pos_ = 0; // Reset buffer position for next state + } void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); void log_remote_closed_(const LogString *during); void cleanup_connection_(); - void send_error_and_cleanup_(ota::OTAResponseTypes error); + inline void send_error_and_cleanup_(ota::OTAResponseTypes error) { + uint8_t error_byte = static_cast(error); + this->client_->write(&error_byte, 1); // Best effort, non-blocking + this->cleanup_connection_(); + } void yield_and_feed_watchdog_(); #ifdef USE_OTA_PASSWORD From 603bde05e7c97cba08b76cee82c2c5ba8dbf74e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 01:11:30 -0500 Subject: [PATCH 30/34] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fe2625f15e..007ff9af97 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -223,14 +223,13 @@ void ESPHomeOTAComponent::handle_handshake_() { // If password is set, move to auth phase if (!this->password_.empty()) { this->transition_ota_state_(OTAState::AUTH_SEND); - [[fallthrough]]; } else #endif { // No password, move directly to data phase this->transition_ota_state_(OTAState::DATA); - [[fallthrough]]; } + [[fallthrough]]; } #ifdef USE_OTA_PASSWORD From 91adbc2466517e5cab26a2f21c0fa01cc357e5e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 01:39:56 -0500 Subject: [PATCH 31/34] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 007ff9af97..664c05f3b1 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -749,16 +749,14 @@ bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) { bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) { // Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout) - char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte - size_t cnonce_offset = 1 + hex_size; // Offset where cnonce starts in buffer - char *cnonce = reinterpret_cast(this->auth_buf_.get() + cnonce_offset); - const char *response = cnonce + hex_size; // Response immediately follows cnonce + const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); // Skip auth_type byte + const char *cnonce = nonce + hex_size; // CNonce immediately follows nonce + const char *response = cnonce + hex_size; // Response immediately follows cnonce // Calculate expected hash: password + nonce + cnonce hasher->init(); hasher->add(this->password_.c_str(), this->password_.length()); - hasher->add(nonce, hex_size); - hasher->add(cnonce, hex_size); + hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher->calculate(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE From 8b98ed16e959b172a6364525cbd285786c5dff8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 01:52:17 -0500 Subject: [PATCH 32/34] error --- esphome/components/esphome/ota/ota_esphome.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 664c05f3b1..caa526ff91 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -789,6 +789,10 @@ size_t ESPHomeOTAComponent::get_auth_hex_size_() const { #endif #ifdef USE_OTA_MD5 return MD5_HEX_SIZE; +#else +#ifndef USE_OTA_SHA256 +#error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled" +#endif #endif } From ceb1dcba408148b16b0ae47511b507cc635bca3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 02:04:18 -0500 Subject: [PATCH 33/34] fix --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index caa526ff91..f73837f8fa 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -81,7 +81,7 @@ void ESPHomeOTAComponent::setup() { return; } - err = this->server_->listen(4); + err = this->server_->listen(1); // Only one client at a time if (err != 0) { this->log_socket_error_(LOG_STR("listen")); this->mark_failed(); From e4460bc8024523843bbdb48541facef837fbfc9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 09:25:32 -0500 Subject: [PATCH 34/34] preen --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f73837f8fa..f1506f066c 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -254,7 +254,7 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::DATA: this->handle_data_(); - [[fallthrough]]; + return; default: break;