mirror of
https://github.com/esphome/esphome.git
synced 2025-09-28 16:12:24 +01:00
Merge branch 'ota_password_block' into memory_api
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user