mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	[ota] Add SHA256 password authentication with backward compatibility (#10809)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -16,7 +16,7 @@ from esphome.const import ( | ||||
|     CONF_SAFE_MODE, | ||||
|     CONF_VERSION, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.coroutine import CoroPriority | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| @@ -24,9 +24,22 @@ _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| AUTO_LOAD = ["md5", "socket"] | ||||
| DEPENDENCIES = ["network"] | ||||
|  | ||||
|  | ||||
| def supports_sha256() -> bool: | ||||
|     """Check if the current platform supports SHA256 for OTA authentication.""" | ||||
|     return bool(CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny) | ||||
|  | ||||
|  | ||||
| def AUTO_LOAD() -> list[str]: | ||||
|     """Conditionally auto-load sha256 only on platforms that support it.""" | ||||
|     base_components = ["md5", "socket"] | ||||
|     if supports_sha256(): | ||||
|         return base_components + ["sha256"] | ||||
|     return base_components | ||||
|  | ||||
|  | ||||
| esphome = cg.esphome_ns.namespace("esphome") | ||||
| ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", OTAComponent) | ||||
|  | ||||
| @@ -126,9 +139,15 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_port(config[CONF_PORT])) | ||||
|  | ||||
|     if CONF_PASSWORD in config: | ||||
|         cg.add(var.set_auth_password(config[CONF_PASSWORD])) | ||||
|         cg.add_define("USE_OTA_PASSWORD") | ||||
|         # Only include hash algorithms when password is configured | ||||
|         cg.add_define("USE_OTA_MD5") | ||||
|         # Only include SHA256 support on platforms that have it | ||||
|         if supports_sha256(): | ||||
|             cg.add_define("USE_OTA_SHA256") | ||||
|     cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -1,6 +1,11 @@ | ||||
| #include "ota_esphome.h" | ||||
| #ifdef USE_OTA | ||||
| #ifdef USE_OTA_MD5 | ||||
| #include "esphome/components/md5/md5.h" | ||||
| #endif | ||||
| #ifdef USE_OTA_SHA256 | ||||
| #include "esphome/components/sha256/sha256.h" | ||||
| #endif | ||||
| #include "esphome/components/network/util.h" | ||||
| #include "esphome/components/ota/ota_backend.h" | ||||
| #include "esphome/components/ota/ota_backend_arduino_esp32.h" | ||||
| @@ -10,6 +15,7 @@ | ||||
| #include "esphome/components/ota/ota_backend_esp_idf.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
|  | ||||
| @@ -95,6 +101,15 @@ void ESPHomeOTAComponent::loop() { | ||||
| } | ||||
|  | ||||
| static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; | ||||
| #ifdef USE_OTA_SHA256 | ||||
| static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; | ||||
| #endif | ||||
|  | ||||
| // Temporary flag to allow MD5 downgrade for ~3 versions (until 2026.1.0) | ||||
| // This allows users to downgrade via OTA if they encounter issues after updating. | ||||
| // Without this, users would need to do a serial flash to downgrade. | ||||
| // TODO: Remove this flag and all associated code in 2026.1.0 | ||||
| #define ALLOW_OTA_DOWNGRADE_MD5 | ||||
|  | ||||
| void ESPHomeOTAComponent::handle_handshake_() { | ||||
|   /// Handle the initial OTA handshake. | ||||
| @@ -225,57 +240,67 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|  | ||||
| #ifdef USE_OTA_PASSWORD | ||||
|   if (!this->password_.empty()) { | ||||
|     buf[0] = ota::OTA_RESPONSE_REQUEST_AUTH; | ||||
|     this->writeall_(buf, 1); | ||||
|     md5::MD5Digest md5{}; | ||||
|     md5.init(); | ||||
|     sprintf(sbuf, "%08" PRIx32, random_uint32()); | ||||
|     md5.add(sbuf, 8); | ||||
|     md5.calculate(); | ||||
|     md5.get_hex(sbuf); | ||||
|     ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); | ||||
|     bool auth_success = false; | ||||
|  | ||||
|     // Send nonce, 32 bytes hex MD5 | ||||
|     if (!this->writeall_(reinterpret_cast<uint8_t *>(sbuf), 32)) { | ||||
|       ESP_LOGW(TAG, "Auth: Writing nonce failed"); | ||||
| #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 | ||||
|  | ||||
|     // prepare challenge | ||||
|     md5.init(); | ||||
|     md5.add(this->password_.c_str(), this->password_.length()); | ||||
|     // add nonce | ||||
|     md5.add(sbuf, 32); | ||||
|  | ||||
|     // Receive cnonce, 32 bytes hex MD5 | ||||
|     if (!this->readall_(buf, 32)) { | ||||
|       ESP_LOGW(TAG, "Auth: Reading cnonce failed"); | ||||
|       goto error;  // NOLINT(cppcoreguidelines-avoid-goto) | ||||
|     } | ||||
|     sbuf[32] = '\0'; | ||||
|     ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf); | ||||
|     // add cnonce | ||||
|     md5.add(sbuf, 32); | ||||
|  | ||||
|     // calculate result | ||||
|     md5.calculate(); | ||||
|     md5.get_hex(sbuf); | ||||
|     ESP_LOGV(TAG, "Auth: Result is %s", sbuf); | ||||
|  | ||||
|     // Receive result, 32 bytes hex MD5 | ||||
|     if (!this->readall_(buf + 64, 32)) { | ||||
|       ESP_LOGW(TAG, "Auth: Reading response failed"); | ||||
|       goto error;  // NOLINT(cppcoreguidelines-avoid-goto) | ||||
|     } | ||||
|     sbuf[64 + 32] = '\0'; | ||||
|     ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64); | ||||
|  | ||||
|     bool matches = true; | ||||
|     for (uint8_t i = 0; i < 32; i++) | ||||
|       matches = matches && buf[i] == buf[64 + i]; | ||||
|  | ||||
|     if (!matches) { | ||||
|       ESP_LOGW(TAG, "Auth failed! Passwords do not match"); | ||||
|     if (!auth_success) { | ||||
|       error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID; | ||||
|       goto error;  // NOLINT(cppcoreguidelines-avoid-goto) | ||||
|     } | ||||
| @@ -499,5 +524,85 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { | ||||
|   delay(1); | ||||
| } | ||||
|  | ||||
| #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)); | ||||
| } | ||||
|  | ||||
| // 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 | ||||
|  | ||||
|   // Use the provided buffer for all hex operations | ||||
|  | ||||
|   // Small stack buffer for nonce seed bytes | ||||
|   uint8_t nonce_bytes[8];  // Max 8 bytes (2 x uint32_t for SHA256) | ||||
|  | ||||
|   // 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; | ||||
|   } | ||||
|   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); | ||||
|  | ||||
|   if (!this->writeall_(reinterpret_cast<uint8_t *>(buf), hex_size)) { | ||||
|     this->log_auth_warning_(LOG_STR("Writing nonce"), name); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   // Start challenge: password + nonce | ||||
|   hasher->init(); | ||||
|   hasher->add(password.c_str(), password.length()); | ||||
|   hasher->add(buf, hex_size); | ||||
|  | ||||
|   // 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); | ||||
|     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(); | ||||
|  | ||||
|   // 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; | ||||
|   } | ||||
|   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); | ||||
|  | ||||
|   if (!matches) { | ||||
|     this->log_auth_warning_(LOG_STR("Password mismatch"), name); | ||||
|   } | ||||
|  | ||||
|   return matches; | ||||
| } | ||||
| #endif  // USE_OTA_PASSWORD | ||||
|  | ||||
| }  // namespace esphome | ||||
| #endif | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/hash_base.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| @@ -30,6 +31,11 @@ class ESPHomeOTAComponent : public ota::OTAComponent { | ||||
|  protected: | ||||
|   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); | ||||
| #endif  // USE_OTA_PASSWORD | ||||
|   bool readall_(uint8_t *buf, size_t len); | ||||
|   bool writeall_(const uint8_t *buf, size_t len); | ||||
|   void log_socket_error_(const LogString *msg); | ||||
|   | ||||
| @@ -14,6 +14,7 @@ namespace ota { | ||||
| enum OTAResponseTypes { | ||||
|   OTA_RESPONSE_OK = 0x00, | ||||
|   OTA_RESPONSE_REQUEST_AUTH = 0x01, | ||||
|   OTA_RESPONSE_REQUEST_SHA256_AUTH = 0x02, | ||||
|  | ||||
|   OTA_RESPONSE_HEADER_OK = 0x40, | ||||
|   OTA_RESPONSE_AUTH_OK = 0x41, | ||||
|   | ||||
| @@ -123,7 +123,9 @@ | ||||
| #define USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
| #define USE_ONLINE_IMAGE_JPEG_SUPPORT | ||||
| #define USE_OTA | ||||
| #define USE_OTA_MD5 | ||||
| #define USE_OTA_PASSWORD | ||||
| #define USE_OTA_SHA256 | ||||
| #define USE_OTA_STATE_CALLBACK | ||||
| #define USE_OTA_VERSION 2 | ||||
| #define USE_TIME_TIMEZONE | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from collections.abc import Callable | ||||
| import gzip | ||||
| import hashlib | ||||
| import io | ||||
| @@ -9,12 +10,14 @@ import random | ||||
| import socket | ||||
| import sys | ||||
| import time | ||||
| from typing import Any | ||||
|  | ||||
| from esphome.core import EsphomeError | ||||
| from esphome.helpers import resolve_ip_address | ||||
|  | ||||
| RESPONSE_OK = 0x00 | ||||
| RESPONSE_REQUEST_AUTH = 0x01 | ||||
| RESPONSE_REQUEST_SHA256_AUTH = 0x02 | ||||
|  | ||||
| RESPONSE_HEADER_OK = 0x40 | ||||
| RESPONSE_AUTH_OK = 0x41 | ||||
| @@ -45,6 +48,7 @@ OTA_VERSION_2_0 = 2 | ||||
| MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] | ||||
|  | ||||
| FEATURE_SUPPORTS_COMPRESSION = 0x01 | ||||
| FEATURE_SUPPORTS_SHA256_AUTH = 0x02 | ||||
|  | ||||
|  | ||||
| UPLOAD_BLOCK_SIZE = 8192 | ||||
| @@ -52,6 +56,12 @@ UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # Authentication method lookup table: response -> (hash_func, nonce_size, name) | ||||
| _AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = { | ||||
|     RESPONSE_REQUEST_SHA256_AUTH: (hashlib.sha256, 64, "SHA256"), | ||||
|     RESPONSE_REQUEST_AUTH: (hashlib.md5, 32, "MD5"), | ||||
| } | ||||
|  | ||||
|  | ||||
| class ProgressBar: | ||||
|     def __init__(self): | ||||
| @@ -81,18 +91,43 @@ class OTAError(EsphomeError): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def recv_decode(sock, amount, decode=True): | ||||
| def recv_decode( | ||||
|     sock: socket.socket, amount: int, decode: bool = True | ||||
| ) -> bytes | list[int]: | ||||
|     """Receive data from socket and optionally decode to list of integers. | ||||
|  | ||||
|     :param sock: Socket to receive data from. | ||||
|     :param amount: Number of bytes to receive. | ||||
|     :param decode: If True, convert bytes to list of integers, otherwise return raw bytes. | ||||
|     :return: List of integers if decode=True, otherwise raw bytes. | ||||
|     """ | ||||
|     data = sock.recv(amount) | ||||
|     if not decode: | ||||
|         return data | ||||
|     return list(data) | ||||
|  | ||||
|  | ||||
| def receive_exactly(sock, amount, msg, expect, decode=True): | ||||
|     data = [] if decode else b"" | ||||
| def receive_exactly( | ||||
|     sock: socket.socket, | ||||
|     amount: int, | ||||
|     msg: str, | ||||
|     expect: int | list[int] | None, | ||||
|     decode: bool = True, | ||||
| ) -> list[int] | bytes: | ||||
|     """Receive exactly the specified amount of data from socket with error checking. | ||||
|  | ||||
|     :param sock: Socket to receive data from. | ||||
|     :param amount: Exact number of bytes to receive. | ||||
|     :param msg: Description of what is being received for error messages. | ||||
|     :param expect: Expected response code(s) for validation, None to skip validation. | ||||
|     :param decode: If True, return list of integers, otherwise return raw bytes. | ||||
|     :return: List of integers if decode=True, otherwise raw bytes. | ||||
|     :raises OTAError: If receiving fails or response doesn't match expected. | ||||
|     """ | ||||
|     data: list[int] | bytes = [] if decode else b"" | ||||
|  | ||||
|     try: | ||||
|         data += recv_decode(sock, 1, decode=decode) | ||||
|         data += recv_decode(sock, 1, decode=decode)  # type: ignore[operator] | ||||
|     except OSError as err: | ||||
|         raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err | ||||
|  | ||||
| @@ -104,13 +139,19 @@ def receive_exactly(sock, amount, msg, expect, decode=True): | ||||
|  | ||||
|     while len(data) < amount: | ||||
|         try: | ||||
|             data += recv_decode(sock, amount - len(data), decode=decode) | ||||
|             data += recv_decode(sock, amount - len(data), decode=decode)  # type: ignore[operator] | ||||
|         except OSError as err: | ||||
|             raise OTAError(f"Error receiving {msg}: {err}") from err | ||||
|     return data | ||||
|  | ||||
|  | ||||
| def check_error(data, expect): | ||||
| def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None: | ||||
|     """Check response data for error codes and validate against expected response. | ||||
|  | ||||
|     :param data: Response data from device (first byte is the response code). | ||||
|     :param expect: Expected response code(s), None to skip validation. | ||||
|     :raises OTAError: If an error code is detected or response doesn't match expected. | ||||
|     """ | ||||
|     if not expect: | ||||
|         return | ||||
|     dat = data[0] | ||||
| @@ -125,7 +166,7 @@ def check_error(data, expect): | ||||
|         raise OTAError("Error: Authentication invalid. Is the password correct?") | ||||
|     if dat == RESPONSE_ERROR_WRITING_FLASH: | ||||
|         raise OTAError( | ||||
|             "Error: Wring OTA data to flash memory failed. See USB logs for more " | ||||
|             "Error: Writing OTA data to flash memory failed. See USB logs for more " | ||||
|             "information." | ||||
|         ) | ||||
|     if dat == RESPONSE_ERROR_UPDATE_END: | ||||
| @@ -177,7 +218,16 @@ def check_error(data, expect): | ||||
|         raise OTAError(f"Unexpected response from ESP: 0x{data[0]:02X}") | ||||
|  | ||||
|  | ||||
| def send_check(sock, data, msg): | ||||
| def send_check( | ||||
|     sock: socket.socket, data: list[int] | tuple[int, ...] | int | str | bytes, msg: str | ||||
| ) -> None: | ||||
|     """Send data to socket with error handling. | ||||
|  | ||||
|     :param sock: Socket to send data to. | ||||
|     :param data: Data to send (can be list/tuple of ints, single int, string, or bytes). | ||||
|     :param msg: Description of what is being sent for error messages. | ||||
|     :raises OTAError: If sending fails. | ||||
|     """ | ||||
|     try: | ||||
|         if isinstance(data, (list, tuple)): | ||||
|             data = bytes(data) | ||||
| @@ -210,10 +260,14 @@ def perform_ota( | ||||
|             f"Device uses unsupported OTA version {version}, this ESPHome supports {supported_versions}" | ||||
|         ) | ||||
|  | ||||
|     # Features | ||||
|     send_check(sock, FEATURE_SUPPORTS_COMPRESSION, "features") | ||||
|     # Features - send both compression and SHA256 auth support | ||||
|     features_to_send = FEATURE_SUPPORTS_COMPRESSION | FEATURE_SUPPORTS_SHA256_AUTH | ||||
|     send_check(sock, features_to_send, "features") | ||||
|     features = receive_exactly( | ||||
|         sock, 1, "features", [RESPONSE_HEADER_OK, RESPONSE_SUPPORTS_COMPRESSION] | ||||
|         sock, | ||||
|         1, | ||||
|         "features", | ||||
|         None,  # Accept any response | ||||
|     )[0] | ||||
|  | ||||
|     if features == RESPONSE_SUPPORTS_COMPRESSION: | ||||
| @@ -222,31 +276,52 @@ def perform_ota( | ||||
|     else: | ||||
|         upload_contents = file_contents | ||||
|  | ||||
|     (auth,) = receive_exactly( | ||||
|         sock, 1, "auth", [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK] | ||||
|     ) | ||||
|     if auth == RESPONSE_REQUEST_AUTH: | ||||
|     def perform_auth( | ||||
|         sock: socket.socket, | ||||
|         password: str, | ||||
|         hash_func: Callable[..., Any], | ||||
|         nonce_size: int, | ||||
|         hash_name: str, | ||||
|     ) -> None: | ||||
|         """Perform challenge-response authentication using specified hash algorithm.""" | ||||
|         if not password: | ||||
|             raise OTAError("ESP requests password, but no password given!") | ||||
|         nonce = receive_exactly( | ||||
|             sock, 32, "authentication nonce", [], decode=False | ||||
|         ).decode() | ||||
|         _LOGGER.debug("Auth: Nonce is %s", nonce) | ||||
|         cnonce = hashlib.md5(str(random.random()).encode()).hexdigest() | ||||
|         _LOGGER.debug("Auth: CNonce is %s", cnonce) | ||||
|  | ||||
|         nonce_bytes = receive_exactly( | ||||
|             sock, nonce_size, f"{hash_name} authentication nonce", [], decode=False | ||||
|         ) | ||||
|         assert isinstance(nonce_bytes, bytes) | ||||
|         nonce = nonce_bytes.decode() | ||||
|         _LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce) | ||||
|  | ||||
|         # Generate cnonce | ||||
|         cnonce = hash_func(str(random.random()).encode()).hexdigest() | ||||
|         _LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce) | ||||
|  | ||||
|         send_check(sock, cnonce, "auth cnonce") | ||||
|  | ||||
|         result_md5 = hashlib.md5() | ||||
|         result_md5.update(password.encode("utf-8")) | ||||
|         result_md5.update(nonce.encode()) | ||||
|         result_md5.update(cnonce.encode()) | ||||
|         result = result_md5.hexdigest() | ||||
|         _LOGGER.debug("Auth: Result is %s", result) | ||||
|         # Calculate challenge response | ||||
|         hasher = hash_func() | ||||
|         hasher.update(password.encode("utf-8")) | ||||
|         hasher.update(nonce.encode()) | ||||
|         hasher.update(cnonce.encode()) | ||||
|         result = hasher.hexdigest() | ||||
|         _LOGGER.debug("Auth: %s Result is %s", hash_name, result) | ||||
|  | ||||
|         send_check(sock, result, "auth result") | ||||
|         receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK) | ||||
|  | ||||
|     (auth,) = receive_exactly( | ||||
|         sock, | ||||
|         1, | ||||
|         "auth", | ||||
|         [RESPONSE_REQUEST_AUTH, RESPONSE_REQUEST_SHA256_AUTH, RESPONSE_AUTH_OK], | ||||
|     ) | ||||
|  | ||||
|     if auth != RESPONSE_AUTH_OK: | ||||
|         hash_func, nonce_size, hash_name = _AUTH_METHODS[auth] | ||||
|         perform_auth(sock, password, hash_func, nonce_size, hash_name) | ||||
|  | ||||
|     # Set higher timeout during upload | ||||
|     sock.settimeout(30.0) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user