From 5c047ba485f45b88f1c4add822bd556e489bd68b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Sep 2026 11:38:55 +0200 Subject: [PATCH] Keep the OTA key in flash and derive the extended protocol flag --- esphome/components/esphome/ota/__init__.py | 10 ++++++++-- esphome/components/esphome/ota/ota_esphome.cpp | 16 +++------------- esphome/components/esphome/ota/ota_esphome.h | 16 +++++++++++++--- .../components/esphome/ota/ota_esphome_noise.cpp | 12 +++++++++--- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index dd084853c3..6559000229 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -25,7 +25,7 @@ from esphome.const import ( CONF_VERSION, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, ID, coroutine_with_priority from esphome.coroutine import CoroPriority import esphome.final_validate as fv from esphome.types import ConfigType @@ -309,7 +309,13 @@ async def to_code(config: ConfigType) -> None: key = _api_static_key(CORE.config.get(CONF_API) or {}) if key is not None: cg.add_define("USE_OTA_ENCRYPTION") - cg.add(var.set_noise_psk(list(decode_encryption_key(key)))) + # The key stays in flash; it is copied into the handshake only while + # an encrypted session is open + psk = cg.progmem_array( + ID(f"{config[CONF_ID].id}_psk", is_declaration=True, type=cg.uint8), + list(decode_encryption_key(key)), + ) + cg.add(var.set_noise_psk(psk)) # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index daf5f9cd24..f7f17df9a6 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -151,18 +151,9 @@ void ESPHomeOTAComponent::loop() { this->handle_handshake_(); } -static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; -static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; -static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; -static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04; -#ifdef USE_OTA_ENCRYPTION -// Noise needs the extended protocol: the prologue binds the 2-byte feature ack -static constexpr uint8_t CLIENT_NOISE_FEATURES = - CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; -#endif void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. @@ -265,8 +256,7 @@ void ESPHomeOTAComponent::handle_handshake_() { // Compose the feature-ack response. When the client negotiates the extended protocol we emit // a 2-byte response (marker + server feature flags); otherwise we emit the single-byte // legacy response. - this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; - if (this->extended_proto_) { + if (this->extended_proto_()) { static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack"); this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS; this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0); @@ -286,7 +276,7 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::FEATURE_ACK: { static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1; static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2; - const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; + const size_t ack_size = this->extended_proto_() ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; if (!this->try_write_(ack_size, LOG_STR("ack feature"))) { return; } @@ -414,7 +404,7 @@ void ESPHomeOTAComponent::handle_data_() { // Acknowledge auth OK - 1 byte this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK); - if (this->extended_proto_) { + if (this->extended_proto_()) { // Read ota type, 1 byte if (!this->data_readall_(buf, 1)) { this->log_read_error_(LOG_STR("OTA type")); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 6c75980525..9f7f6ae649 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -45,7 +45,8 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { #endif // USE_OTA_PASSWORD #ifdef USE_OTA_ENCRYPTION - void set_noise_psk(const noise::psk_t &psk) { this->noise_ctx_.set_psk(psk); } + /// psk points at 32 bytes that live in flash for the life of the program + void set_noise_psk(const uint8_t *psk) { this->noise_psk_ = psk; } #endif /// Manually set the port OTA should listen on @@ -145,7 +146,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD #ifdef USE_OTA_ENCRYPTION - noise::NoiseContext noise_ctx_; + const uint8_t *noise_psk_{nullptr}; std::unique_ptr noise_; #endif // USE_OTA_ENCRYPTION @@ -167,6 +168,16 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { "OTA_BUFFER_SIZE must fit a full encrypted data frame"); #endif static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; + static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; + static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; + static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08; + // Noise needs the extended protocol: the prologue binds the 2-byte feature ack + static constexpr uint8_t CLIENT_NOISE_FEATURES = + CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; + // Derived from the feature byte rather than stored, which keeps the + // trailing byte members at a multiple of four + inline bool extended_proto_() const { return (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; } #ifdef USE_OTA_PARTITIONS uint32_t running_app_offset_{0}; size_t running_app_size_{0}; @@ -180,7 +191,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD - bool extended_proto_{false}; }; } // namespace esphome diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp index 7dcc0ec5fd..4e2abe4416 100644 --- a/esphome/components/esphome/ota/ota_esphome_noise.cpp +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -65,9 +65,15 @@ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { *p++ = ota::OTA_RESPONSE_FEATURE_FLAGS; *p++ = server_feature_flags; - int err = this->noise_ == nullptr - ? NOISE_ERROR_NO_MEMORY - : this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue)); + // noise-c keeps its own copy of the key, so the flash copy is only read here + noise::psk_t psk; +#ifdef USE_ESP8266 + memcpy_P(psk.data(), this->noise_psk_, psk.size()); +#else + std::memcpy(psk.data(), this->noise_psk_, psk.size()); +#endif + int err = + this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY : this->noise_->handshake.init(psk, prologue, sizeof(prologue)); if (err != 0) { ESP_LOGW(TAG, "Session init: %d", err); this->cleanup_connection_();