ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
ota_esphome.cpp
Go to the documentation of this file.
1#include "ota_esphome.h"
2#ifdef USE_OTA
3#ifdef USE_OTA_PASSWORD
5#endif
14#include "esphome/core/hal.h"
16#include "esphome/core/log.h"
17#include "esphome/core/util.h"
18#ifdef USE_LWIP_FAST_SELECT
20#endif
21
22#include <cerrno>
23#include <cstdio>
24#include <sys/time.h>
25
26namespace esphome {
27
28static const char *const TAG = "esphome.ota";
29static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
30static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
31static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
32static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
33
34// Single-instance pointer — multi-port configs are rejected in final_validate.
35// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
36static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
37
38// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ).
40 if (global_esphome_ota_component != nullptr) {
41 global_esphome_ota_component->enable_loop_soon_any_context();
42 }
43}
44
46 this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
47 if (this->server_ == nullptr) {
48 this->server_failed_(LOG_STR("creation"));
49 return;
50 }
51 int enable = 1;
52 int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
53 if (err != 0) {
54 this->log_socket_error_(LOG_STR("reuseaddr"));
55 // we can still continue
56 }
57 err = this->server_->setblocking(false);
58 if (err != 0) {
59 this->server_failed_(LOG_STR("nonblocking"));
60 return;
61 }
62
63 struct sockaddr_storage server;
64
65 socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
66 if (sl == 0) {
67 this->server_failed_(LOG_STR("set sockaddr"));
68 return;
69 }
70
71 err = this->server_->bind((struct sockaddr *) &server, sl);
72 if (err != 0) {
73 this->server_failed_(LOG_STR("bind"));
74 return;
75 }
76
77 err = this->server_->listen(1); // Only one client at a time
78 if (err != 0) {
79 this->server_failed_(LOG_STR("listen"));
80 return;
81 }
82
83 // loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
84 global_esphome_ota_component = this;
85#ifdef USE_LWIP_FAST_SELECT
86 // Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
87 // no wakes fire and loop() falls back to the self-disable safety net.
89#endif
90
91#ifdef USE_OTA_PARTITIONS
93#endif
94}
95
97 char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
98 ESP_LOGCONFIG(TAG,
99 "Over-The-Air updates:\n"
100 " Address: %s:%u\n"
101 " Version: %d",
102 network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
103#ifdef USE_OTA_PASSWORD
104 if (!this->password_.empty()) {
105 ESP_LOGCONFIG(TAG, " Password configured");
106 }
107#endif
108#ifdef USE_OTA_PARTITIONS
109 ESP_LOGCONFIG(TAG,
110 " Partition access allowed\n"
111 " Running app:\n"
112 " Partition address: 0x%" PRIX32 "\n"
113 " Used size: %zu bytes (0x%zX)",
115
116#ifdef USE_ESP32
117 ESP_LOGCONFIG(TAG,
118 " Partition table:\n"
119 " %-12s %-4s %-8s %-10s %-10s",
120 "Name", "Type", "Subtype", "Address", "Size");
121 esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, nullptr);
122 while (it != nullptr) {
123 const esp_partition_t *partition = esp_partition_get(it);
124 ESP_LOGCONFIG(TAG, " %-12s 0x%-2X 0x%-6X 0x%-8" PRIX32 " 0x%-8" PRIX32, partition->label, partition->type,
125 partition->subtype, partition->address, partition->size);
126 it = esp_partition_next(it);
127 }
128 esp_partition_iterator_release(it);
129 esp_bootloader_desc_t bootloader_desc;
130 esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
131 ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown");
132#endif // USE_ESP32
133#endif // USE_OTA_PARTITIONS
134}
135
137 // Self-disable idle loop where a wake path re-enables on listener readiness
138 // (fast-select, raw-TCP accept_fn_). Host BSD select doesn't, so stay enabled.
139 if (this->client_ == nullptr && !this->server_->ready()) {
140#ifndef USE_HOST
141 this->disable_loop();
142#endif
143 return;
144 }
145 this->handle_handshake_();
146}
147
148static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
149static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
150static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
151static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
152static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
153
160
161 if (this->client_ == nullptr) {
162 // We already checked server_->ready() in loop(), so we can accept directly
163 struct sockaddr_storage source_addr;
164 socklen_t addr_len = sizeof(source_addr);
165 int enable = 1;
166
167 this->client_ = this->server_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
168 if (this->client_ == nullptr)
169 return;
170 int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
171 if (err != 0) {
172 this->log_socket_error_(LOG_STR("nodelay"));
173 this->cleanup_connection_();
174 return;
175 }
176 err = this->client_->setblocking(false);
177 if (err != 0) {
178 this->log_socket_error_(LOG_STR("non-blocking"));
179 this->cleanup_connection_();
180 return;
181 }
182 this->log_start_(LOG_STR("handshake"));
184 this->handshake_buf_pos_ = 0; // Reset handshake buffer position
186 }
187
188 // Check for handshake timeout
190 if (now - this->client_connect_time_ > OTA_SOCKET_TIMEOUT_HANDSHAKE) {
191 ESP_LOGW(TAG, "Handshake timeout");
192 this->cleanup_connection_();
193 return;
194 }
195
196 switch (this->ota_state_) {
198 // Try to read remaining magic bytes (5 total)
199 if (!this->try_read_(5, LOG_STR("read magic"))) {
200 return;
201 }
202
203 // Validate magic bytes
204 static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
205 if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
206 ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
207 this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
209 return;
210 }
211
212 // Magic bytes valid, move to next state
215 this->handshake_buf_[1] = USE_OTA_VERSION;
216 [[fallthrough]];
217 }
218
219 case OTAState::MAGIC_ACK: {
220 // Send OK and version - 2 bytes
221 if (!this->try_write_(2, LOG_STR("ack magic"))) {
222 return;
223 }
224 // All bytes sent, create backend and move to next state
227 [[fallthrough]];
228 }
229
231 // Read features - 1 byte
232 if (!this->try_read_(1, LOG_STR("read feature"))) {
233 return;
234 }
235 this->ota_features_ = this->handshake_buf_[0];
236 ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
238
239 const bool supports_compression =
240 (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression();
241
242 // Compose the feature-ack response. When the client negotiates the extended protocol we emit
243 // a 2-byte response (marker + server feature flags); otherwise we emit the single-byte
244 // legacy response.
245 this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0;
246 if (this->extended_proto_) {
247 static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
249 this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
250#ifdef USE_OTA_PARTITIONS
251 this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
252#endif
253 } else {
254 this->handshake_buf_[0] =
256 }
257 [[fallthrough]];
258 }
259
261 static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1;
262 static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2;
263 const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE;
264 if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
265 return;
266 }
267#ifdef USE_OTA_PASSWORD
268 // If password is set, move to auth phase
269 if (!this->password_.empty()) {
271 } else
272#endif
273 {
274 // No password, move directly to data phase
276 }
277 [[fallthrough]];
278 }
279
280#ifdef USE_OTA_PASSWORD
281 case OTAState::AUTH_SEND: {
282 // Non-blocking authentication send
283 if (!this->handle_auth_send_()) {
284 return;
285 }
287 [[fallthrough]];
288 }
289
290 case OTAState::AUTH_READ: {
291 // Non-blocking authentication read & verify
292 if (!this->handle_auth_read_()) {
293 return;
294 }
296 [[fallthrough]];
297 }
298#endif
299
300 case OTAState::DATA:
301 this->handle_data_();
302 return;
303
304 default:
305 break;
306 }
307}
308
343 size_t total = 0;
344 uint32_t last_progress = 0;
345 uint32_t last_data_ms = 0;
346 uint8_t buf[OTA_BUFFER_SIZE];
347 char *sbuf = reinterpret_cast<char *>(buf);
348 size_t ota_size;
350#if USE_OTA_VERSION == 2
351 size_t size_acknowledged = 0;
352#endif
353
354 // Set socket timeouts and blocking mode (see strategy table above)
355 struct timeval tv;
356 tv.tv_sec = 2;
357 tv.tv_usec = 0;
358 this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
359 this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
360 this->client_->setblocking(true);
361
362 // Acknowledge auth OK - 1 byte
364
365 if (this->extended_proto_) {
366 // Read ota type, 1 byte
367 if (!this->readall_(buf, 1)) {
368 this->log_read_error_(LOG_STR("OTA type"));
369 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
370 }
371 ota_type = static_cast<ota::OTAType>(buf[0]);
372 }
373 ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
374
375 // Read size, 4 bytes MSB first
376 if (!this->readall_(buf, 4)) {
377 this->log_read_error_(LOG_STR("size"));
378 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
379 }
380 ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
381 (static_cast<size_t>(buf[2]) << 8) | buf[3];
382 ESP_LOGV(TAG, "Size is %zu bytes", ota_size);
383
384#ifndef USE_OTA_PARTITIONS
385 if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
387 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
388 }
389#endif
390
391 // Now that we've passed authentication and are actually
392 // starting the update, set the warning status and notify
393 // listeners. This ensures that port scanners do not
394 // accidentally trigger the update process.
395 this->log_start_(LOG_STR("update"));
396 this->status_set_warning();
397#ifdef USE_OTA_STATE_LISTENER
398 this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
399#endif
400
401 // begin() may block for a few seconds while it locks flash.
402 error_code = this->backend_->begin(ota_size, ota_type);
403 if (error_code != ota::OTA_RESPONSE_OK)
404 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
405
406 // Acknowledge prepare OK - 1 byte
408
409 // Read binary MD5, 32 bytes
410 if (!this->readall_(buf, 32)) {
411 this->log_read_error_(LOG_STR("MD5 checksum"));
412 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
413 }
414 sbuf[32] = '\0';
415 ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf);
416 this->backend_->set_update_md5(sbuf);
417
418 // Acknowledge MD5 OK - 1 byte
420
421 // Track when we last received data so a silently-vanished peer (no FIN/RST
422 // delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
423 // can't wedge the device indefinitely. Without this, the loop only exits
424 // on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
425 // TCP keepalive isn't enabled here.
426 last_data_ms = millis();
427 while (total < ota_size) {
428 if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
429 ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
431 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
432 }
433 size_t remaining = ota_size - total;
434 size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
435 ssize_t read = this->client_->read(buf, requested);
436 if (read == -1) {
437 const int err = errno;
438 if (this->would_block_(err)) {
439 // read() already waited up to SO_RCVTIMEO for data, just feed WDT
440 App.feed_wdt();
441 continue;
442 }
443 ESP_LOGW(TAG, "Read err %d", err);
444 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
445 } else if (read == 0) {
446 ESP_LOGW(TAG, "Remote closed");
447 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
448 }
449
450 last_data_ms = millis();
451 error_code = this->backend_->write(buf, read);
452 if (error_code != ota::OTA_RESPONSE_OK) {
453 ESP_LOGW(TAG, "Flash write err %d", error_code);
454 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
455 }
456 total += read;
457#if USE_OTA_VERSION == 2
458 while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
460 size_acknowledged += OTA_BLOCK_SIZE;
461 }
462#endif
463
464 uint32_t now = millis();
465 if (now - last_progress > 1000) {
466 last_progress = now;
467 float percentage = (total * 100.0f) / ota_size;
468 ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
469#ifdef USE_OTA_STATE_LISTENER
470 this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
471#endif
472 // feed watchdog and give other tasks a chance to run
474 }
475 }
476
477 // Acknowledge receive OK - 1 byte
479
480 error_code = this->backend_->end();
481 if (error_code != ota::OTA_RESPONSE_OK) {
482 ESP_LOGW(TAG, "End update err %d", error_code);
483 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
484 }
485
486 // Acknowledge Update end OK - 1 byte
488
489 // Read ACK
490 if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
491 this->log_read_error_(LOG_STR("ack"));
492 // do not go to error, this is not fatal
493 }
494
495 this->cleanup_connection_();
496 delay(10);
497 ESP_LOGI(TAG, "Update complete");
498 this->status_clear_warning();
499#ifdef USE_OTA_STATE_LISTENER
500 this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0);
501#endif
502 delay(100); // NOLINT
503#ifdef USE_OTA_PARTITIONS
504 if (ota_type == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
505 // Skip on_safe_shutdown: nvs_flash_deinit() has already invalidated open NVS handles, so
506 // preferences flush would emit ESP_ERR_NVS_INVALID_HANDLE for every entry. Reboot directly.
507 App.reboot();
508 }
509#endif
511
512error:
513 this->write_byte_(static_cast<uint8_t>(error_code));
514
515 // Abort backend before cleanup - cleanup_connection_() destroys the backend.
516 // Always call abort() unconditionally: backends register external partitions before
517 // esp_ota_begin (partition table / bootloader paths), and abort() is responsible for
518 // releasing those even if begin() failed before an OTA handle was opened. The IDF
519 // backend's esp_ota_abort(0) is documented as harmless.
520 if (this->backend_ != nullptr) {
521 this->backend_->abort();
522 }
523
524 this->cleanup_connection_();
525
526 this->status_momentary_error("err", 5000);
527#ifdef USE_OTA_STATE_LISTENER
528 this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
529#endif
530}
531
532bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
533 uint32_t start = millis();
534 uint32_t at = 0;
535 while (len - at > 0) {
536 uint32_t now = millis();
537 if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
538 ESP_LOGW(TAG, "Timeout reading %zu bytes", len);
539 return false;
540 }
541
542 ssize_t read = this->client_->read(buf + at, len - at);
543 if (read == -1) {
544 const int err = errno;
545 if (!this->would_block_(err)) {
546 ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err);
547 return false;
548 }
549 } else if (read == 0) {
550 ESP_LOGW(TAG, "Remote closed");
551 return false;
552 } else {
553 at += read;
554 }
555 // read() already waited via SO_RCVTIMEO, just yield without 1ms stall
556 App.feed_wdt();
557 delay(0);
558 }
559
560 return true;
561}
562bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
563 uint32_t start = millis();
564 uint32_t at = 0;
565 while (len - at > 0) {
566 uint32_t now = millis();
567 if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
568 ESP_LOGW(TAG, "Timeout writing %zu bytes", len);
569 return false;
570 }
571
572 ssize_t written = this->client_->write(buf + at, len - at);
573 if (written == -1) {
574 const int err = errno;
575 if (!this->would_block_(err)) {
576 ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err);
577 return false;
578 }
579 // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning
581 } else {
582 at += written;
583 // write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT
584 App.feed_wdt();
585 }
586 }
587 return true;
588}
589
591uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
592void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
593
594void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
595 ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
596}
597
598void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); }
599
600void ESPHomeOTAComponent::log_start_(const LogString *phase) {
601 char peername[socket::SOCKADDR_STR_LEN];
602 this->client_->getpeername_to(peername);
603 ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername);
604}
605
606void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) {
607 ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during));
608}
609
610void ESPHomeOTAComponent::server_failed_(const LogString *msg) {
611 this->log_socket_error_(msg);
612 // No explicit close() needed — listen sockets have no active connections on
613 // failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
614 delete this->server_;
615 this->server_ = nullptr;
616 this->mark_failed();
617}
618
619bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) {
620 if (read == -1 && this->would_block_(errno)) {
621 return false; // No data yet, try again next loop
622 }
623
624 if (read <= 0) {
625 read == 0 ? this->log_remote_closed_(desc) : this->log_socket_error_(desc);
626 this->cleanup_connection_();
627 return false;
628 }
629 return true;
630}
631
633 if (written == -1) {
634 if (this->would_block_(errno)) {
635 return false; // Try again next loop
636 }
637 this->log_socket_error_(desc);
638 this->cleanup_connection_();
639 return false;
640 }
641 return true;
642}
643
644bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *desc) {
645 // Read bytes into handshake buffer, starting at handshake_buf_pos_
646 size_t bytes_to_read = to_read - this->handshake_buf_pos_;
647 ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read);
648
649 if (!this->handle_read_error_(read, desc)) {
650 return false;
651 }
652
653 this->handshake_buf_pos_ += read;
654 // Return true only if we have all the requested bytes
655 return this->handshake_buf_pos_ >= to_read;
656}
657
658bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) {
659 // Write bytes from handshake buffer, starting at handshake_buf_pos_
660 size_t bytes_to_write = to_write - this->handshake_buf_pos_;
661 ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write);
662
663 if (!this->handle_write_error_(written, desc)) {
664 return false;
665 }
666
667 this->handshake_buf_pos_ += written;
668 // Return true only if we have written all the requested bytes
669 return this->handshake_buf_pos_ >= to_write;
670}
671
673 this->client_->close();
674 this->client_ = nullptr;
675 this->client_connect_time_ = 0;
676 this->handshake_buf_pos_ = 0;
678 this->ota_features_ = 0;
679 this->backend_ = nullptr;
680#ifdef USE_OTA_PASSWORD
681 this->cleanup_auth_();
682#endif
683 // Intentionally no disable_loop() — letting loop() run one more iteration catches
684 // any connection that queued on the listener mid-session (otherwise the wake flag,
685 // set while we were in LOOP state, would be lost to enable_pending_loops_()).
686}
687
692
693#ifdef USE_OTA_PASSWORD
694void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
695
697 bool client_supports_sha256 = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_SHA256_AUTH) != 0;
698
699 // Require SHA256
700 if (!client_supports_sha256) {
701 this->log_auth_warning_(LOG_STR("SHA256 required"));
703 return false;
704 }
706 return true;
707}
708
710 // Initialize auth buffer if not already done
711 if (!this->auth_buf_) {
712 // Select auth type based on client capabilities and configuration
713 if (!this->select_auth_type_()) {
714 return false;
715 }
716
717 // Generate nonce - hasher must be created and used in same stack frame
718 // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
719 // 1. Hash objects must NEVER be passed to another function (different stack frame)
720 // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
721 // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
722 // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
723 //
724 // Buffer layout after AUTH_READ completes:
725 // [0]: auth_type (1 byte)
726 // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
727 // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
728 // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
729
730 // CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
731 // (no passing to other functions). All hash operations must happen in this function.
732 sha256::SHA256 hasher;
733
734 const size_t hex_size = hasher.get_size() * 2;
735 const size_t nonce_len = hasher.get_size() / 4;
736 const size_t auth_buf_size = 1 + 3 * hex_size;
737 this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
738 this->auth_buf_pos_ = 0;
739
740 char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
741 if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
742 this->log_auth_warning_(LOG_STR("Random failed"));
744 return false;
745 }
746
747 hasher.init();
748 hasher.add(buf, nonce_len);
749 hasher.calculate();
750 this->auth_buf_[0] = this->auth_type_;
751 hasher.get_hex(buf);
752
753 ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf);
754 }
755
756 // Try to write auth_type + nonce
757 constexpr size_t hex_size = SHA256_HEX_SIZE;
758 const size_t to_write = 1 + hex_size;
759 size_t remaining = to_write - this->auth_buf_pos_;
760
761 ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
762 if (!this->handle_write_error_(written, LOG_STR("ack auth"))) {
763 return false;
764 }
765
766 this->auth_buf_pos_ += written;
767
768 // Check if we still have more to write
769 if (this->auth_buf_pos_ < to_write) {
770 return false; // More to write, try again next loop
771 }
772
773 // All written, prepare for reading phase
774 this->auth_buf_pos_ = 0;
775 return true;
776}
777
779 constexpr size_t hex_size = SHA256_HEX_SIZE;
780 const size_t to_read = hex_size * 2; // CNonce + Response
781
782 // Try to read remaining bytes (CNonce + Response)
783 // We read cnonce+response starting at offset 1+hex_size (after auth_type and our nonce)
784 size_t cnonce_offset = 1 + hex_size; // Offset where cnonce should be stored in buffer
785 size_t remaining = to_read - this->auth_buf_pos_;
786 ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining);
787
788 if (!this->handle_read_error_(read, LOG_STR("read auth"))) {
789 return false;
790 }
791
792 this->auth_buf_pos_ += read;
793
794 // Check if we still need more data
795 if (this->auth_buf_pos_ < to_read) {
796 return false; // More to read, try again next loop
797 }
798
799 // We have all the data, verify it
800 const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
801 const char *cnonce = nonce + hex_size;
802 const char *response = cnonce + hex_size;
803
804 // CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
805 // (no passing to other functions). All hash operations must happen in this function.
806 sha256::SHA256 hasher;
807
808 hasher.init();
809 hasher.add(this->password_.c_str(), this->password_.length());
810 hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
811 hasher.calculate();
812
813 ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce);
814#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
815 char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator)
816 hasher.get_hex(computed_hash);
817 ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash);
818#endif
819 ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response);
820
821 // Compare response
822 bool matches = hasher.equals_hex(response);
823
824 if (!matches) {
825 this->log_auth_warning_(LOG_STR("Password mismatch"));
827 return false;
828 }
829
830 // Authentication successful - clean up auth state
831 this->cleanup_auth_();
832
833 return true;
834}
835
837 this->auth_buf_ = nullptr;
838 this->auth_buf_pos_ = 0;
839 this->auth_type_ = 0;
840}
841#endif // USE_OTA_PASSWORD
842
843} // namespace esphome
844#endif
void feed_wdt()
Feed the task watchdog.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:289
bool would_block_(int error_code) const
Definition ota_esphome.h:69
static constexpr size_t SHA256_HEX_SIZE
Definition ota_esphome.h:55
bool writeall_(const uint8_t *buf, size_t len)
bool try_read_(size_t to_read, const LogString *desc)
ota::OTABackendPtr backend_
Definition ota_esphome.h:97
bool try_write_(size_t to_write, const LogString *desc)
std::unique_ptr< uint8_t[]> auth_buf_
Definition ota_esphome.h:92
bool handle_write_error_(ssize_t written, const LogString *desc)
void log_auth_warning_(const LogString *msg)
float get_setup_priority() const override
void send_error_and_cleanup_(ota::OTAResponseTypes error)
Definition ota_esphome.h:83
bool handle_read_error_(ssize_t read, const LogString *desc)
void log_read_error_(const LogString *what)
bool readall_(uint8_t *buf, size_t len)
void set_port(uint16_t port)
Manually set the port OTA should listen on.
bool write_byte_(uint8_t byte)
Definition ota_esphome.h:64
uint8_t handshake_buf_[HANDSHAKE_BUF_SIZE]
static constexpr size_t HANDSHAKE_BUF_SIZE
void server_failed_(const LogString *msg)
void transition_ota_state_(OTAState next_state)
Definition ota_esphome.h:72
socket::ListenSocket * server_
Definition ota_esphome.h:95
void log_remote_closed_(const LogString *during)
std::unique_ptr< socket::Socket > client_
Definition ota_esphome.h:96
void log_start_(const LogString *phase)
void log_socket_error_(const LogString *msg)
void get_hex(char *output)
Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes.
Definition hash_base.h:29
bool equals_hex(const char *expected)
Compare the hash against a provided hex-encoded hash.
Definition hash_base.h:35
void notify_state_(OTAState state, float progress, uint8_t error)
SHA256 hash implementation.
Definition sha256.h:51
void calculate() override
Definition sha256.cpp:27
size_t get_size() const override
Get the size of the hash in bytes (32 for SHA256)
Definition sha256.h:64
void add(const uint8_t *data, size_t len) override
Definition sha256.cpp:25
void init() override
Definition sha256.cpp:19
bool ready() const
Check if the socket has buffered data ready to read.
Definition socket.h:85
int bind(const struct sockaddr *addr, socklen_t addrlen)
int setsockopt(int level, int optname, const void *optval, socklen_t optlen)
std::unique_ptr< BSDSocketImpl > accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen)
uint16_t addr_len
uint32_t socklen_t
Definition headers.h:99
__int64 ssize_t
Definition httplib.h:178
struct lwip_sock * esphome_lwip_get_sock(int fd)
Look up a LwIP socket struct from a file descriptor.
void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock)
Set the listener netconn that the fast-select callback filters OTA wakes against.
const char * get_use_address_to(std::span< char, USE_ADDRESS_BUFFER_SIZE > buf)
Get the active network address for logging.
Definition util.cpp:25
@ OTA_TYPE_UPDATE_PARTITION_TABLE
Definition ota_backend.h:77
void get_running_app_position(uint32_t &offset, size_t &size)
@ OTA_RESPONSE_UPDATE_PREPARE_OK
Definition ota_backend.h:22
@ OTA_RESPONSE_SUPPORTS_COMPRESSION
Definition ota_backend.h:26
@ OTA_RESPONSE_BIN_MD5_OK
Definition ota_backend.h:23
@ OTA_RESPONSE_UPDATE_END_OK
Definition ota_backend.h:25
@ OTA_RESPONSE_RECEIVE_OK
Definition ota_backend.h:24
@ OTA_RESPONSE_CHUNK_OK
Definition ota_backend.h:27
@ OTA_RESPONSE_FEATURE_FLAGS
Definition ota_backend.h:28
@ OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE
Definition ota_backend.h:44
@ OTA_RESPONSE_ERROR_AUTH_INVALID
Definition ota_backend.h:32
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:50
@ OTA_RESPONSE_REQUEST_SHA256_AUTH
Definition ota_backend.h:18
@ OTA_RESPONSE_ERROR_MAGIC
Definition ota_backend.h:30
@ OTA_RESPONSE_HEADER_OK
Definition ota_backend.h:20
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port)
Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
Definition socket.cpp:194
std::unique_ptr< ListenSocket > socket_ip_loop_monitored(int type, int protocol)
Definition socket.cpp:130
bool random_bytes(uint8_t *data, size_t len)
Generate len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG).
Definition helpers.cpp:20
void esphome_wake_ota_component_any_context()
const void size_t len
Definition hal.h:64
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
int written
Definition helpers.h:1059
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t