1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-28 08:02:23 +01:00

Merge branch 'ota_password_block' into memory_api

This commit is contained in:
J. Nick Koston
2025-09-27 09:29:15 -05:00
3 changed files with 462 additions and 219 deletions

View File

@@ -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"
@@ -26,9 +28,19 @@ 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
#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);
@@ -69,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();
@@ -112,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
@@ -141,7 +153,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 +165,99 @@ 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);
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");
switch (this->ota_state_) {
case OTAState::MAGIC_READ: {
// Try to read remaining magic bytes (5 total)
if (!this->try_read_(5, LOG_STR("read magic"))) {
return;
}
this->cleanup_connection_();
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]);
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
return;
}
// 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]];
}
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<uint8_t>(ota::OTA_RESPONSE_ERROR_MAGIC);
this->client_->write(&error, 1);
this->cleanup_connection_();
return;
case OTAState::MAGIC_ACK: {
// Send OK and version - 2 bytes
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);
[[fallthrough]];
}
// All 5 magic bytes are valid, continue with data handling
this->handle_data_();
case OTAState::FEATURE_READ: {
// Read features - 1 byte
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
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()) {
this->transition_ota_state_(OTAState::AUTH_SEND);
} 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_();
return;
default:
break;
}
}
@@ -199,114 +265,21 @@ 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;
uint32_t last_progress = 0;
uint8_t buf[1024];
uint8_t buf[OTA_BUFFER_SIZE];
char *sbuf = reinterpret_cast<char *>(buf);
size_t ota_size;
uint8_t ota_features;
std::unique_ptr<ota::OTABackend> 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);
#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 = (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
// Acknowledge auth OK - 1 byte
buf[0] = ota::OTA_RESPONSE_AUTH_OK;
this->writeall_(buf, 1);
@@ -334,7 +307,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 +323,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;
@@ -358,26 +331,24 @@ 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;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
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;
}
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 = 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);
ESP_LOGW(TAG, "Flash write err %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
total += read;
@@ -406,9 +377,9 @@ 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);
ESP_LOGW(TAG, "End update err %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -437,8 +408,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);
@@ -459,12 +430,12 @@ 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) {
ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno);
if (!this->would_block_(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;
@@ -486,8 +457,8 @@ 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) {
ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno);
if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Write err %d bytes, errno %d", len, errno);
return false;
}
} else {
@@ -512,11 +483,74 @@ 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 at %s", LOG_STR_ARG(during));
}
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_(desc) : this->log_socket_error_(desc);
this->cleanup_connection_();
return false;
}
return true;
}
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_(desc);
this->cleanup_connection_();
return false;
}
return true;
}
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, desc)) {
return false;
}
this->handshake_buf_pos_ += read;
// Return true only if we have all the requested bytes
return this->handshake_buf_pos_ >= to_read;
}
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, desc)) {
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::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;
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
}
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
@@ -525,82 +559,247 @@ 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::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 deprecated MD5"));
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
return true;
#else
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
#else // !ALLOW_OTA_DOWNGRADE_MD5
// Require SHA256
if (!client_supports_sha256) {
this->log_auth_warning_(LOG_STR("SHA256 required"));
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
}
// 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_send_() {
// Initialize auth buffer if not already done
if (!this->auth_buf_) {
// Select auth type based on client capabilities and configuration
if (!this->select_auth_type_()) {
return false;
}
// Use the provided buffer for all hex operations
// 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
// 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
if (!random_bytes(nonce_bytes, nonce_len)) {
this->log_auth_warning_(LOG_STR("Random bytes generation failed"), name);
return false;
if (!success) {
return false;
}
}
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);
// Try to write auth_type + nonce
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_;
if (!this->writeall_(reinterpret_cast<uint8_t *>(buf), hex_size)) {
this->log_auth_warning_(LOG_STR("Writing nonce"), name);
ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
if (!this->handle_write_error_(written, LOG_STR("ack auth"))) {
return false;
}
// Start challenge: password + nonce
hasher->init();
hasher->add(password.c_str(), password.length());
hasher->add(buf, hex_size);
this->auth_buf_pos_ += written;
// Read cnonce and add to hash
if (!this->readall_(reinterpret_cast<uint8_t *>(buf), hex_size)) {
this->log_auth_warning_(LOG_STR("Reading cnonce"), name);
// 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;
return true;
}
bool ESPHomeOTAComponent::handle_auth_read_() {
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)
// 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() + cnonce_offset + this->auth_buf_pos_, remaining);
if (!this->handle_read_error_(read, LOG_STR("read auth"))) {
return false;
}
buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s CNonce is %s", LOG_STR_ARG(name), buf);
hasher->add(buf, hex_size);
hasher->calculate();
this->auth_buf_pos_ += read;
// 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);
// Read response into the buffer
if (!this->readall_(reinterpret_cast<uint8_t *>(buf), hex_size)) {
this->log_auth_warning_(LOG_STR("Reading 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
}
buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: %s Response is %s", LOG_STR_ARG(name), buf);
// Compare response directly with digest in hasher
bool matches = hasher->equals_hex(buf);
// We have all the data, verify it
bool matches = false;
#ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
sha256::SHA256 sha_hasher;
matches = this->verify_hash_auth_(&sha_hasher, hex_size);
}
#endif
#ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
md5::MD5Digest md5_hasher;
matches = this->verify_hash_auth_(&md5_hasher, hex_size);
}
#endif
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;
}
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 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<uint8_t[]>(auth_buf_size);
this->auth_buf_pos_ = 0;
// Generate nonce
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
this->log_auth_warning_(LOG_STR("Random failed"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
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 in the buffer (see prepare_auth_nonce_ for buffer layout)
const char *nonce = reinterpret_cast<char *>(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 * 2); // Add both nonce and cnonce (contiguous in buffer)
hasher->calculate();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
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);
// 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
return hasher->equals_hex(response);
}
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
#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;
#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
}
void ESPHomeOTAComponent::cleanup_auth_() {
this->auth_buf_ = nullptr;
this->auth_buf_pos_ = 0;
this->auth_type_ = 0;
}
#endif // USE_OTA_PASSWORD

View File

@@ -14,6 +14,18 @@ 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
#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; }
#endif // USE_OTA_PASSWORD
@@ -32,16 +44,39 @@ 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_();
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;
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);
bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc);
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_();
inline void send_error_and_cleanup_(ota::OTAResponseTypes error) {
uint8_t error_byte = static_cast<uint8_t>(error);
this->client_->write(&error_byte, 1); // Best effort, non-blocking
this->cleanup_connection_();
}
void yield_and_feed_watchdog_();
#ifdef USE_OTA_PASSWORD
@@ -50,11 +85,19 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
std::unique_ptr<socket::Socket> server_;
std::unique_ptr<socket::Socket> client_;
std::unique_ptr<ota::OTABackend> backend_;
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];
OTAState ota_state_{OTAState::IDLE};
uint8_t handshake_buf_pos_{0};
uint8_t ota_features_{0};
#ifdef USE_OTA_PASSWORD
std::unique_ptr<uint8_t[]> auth_buf_;
uint8_t auth_buf_pos_{0};
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
#endif // USE_OTA_PASSWORD
};
} // namespace esphome

View File

@@ -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