mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
Guard the session start against a missing key, reword the warning, and let the wizard use the key for OTA
This commit is contained in:
@@ -132,10 +132,11 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
||||
elif CONF_PASSWORD in ota_conf and _api_static_key(api_conf) is not None:
|
||||
_LOGGER.warning(
|
||||
"'%s' %s wastes significant flash and RAM (about 3.5 KB and 60 "
|
||||
"bytes plus the password on the heap): the '%s' %s %s already "
|
||||
"authenticates and encrypts OTA uploads, the password only serves "
|
||||
"older clients that do not support encryption; remove '%s' and "
|
||||
"add '%s' under '%s' to require encryption",
|
||||
"bytes plus the password on the heap): the device already offers "
|
||||
"encryption with the '%s' %s %s, which authenticates any uploader "
|
||||
"that takes it, so the password only serves older clients that do "
|
||||
"not support encryption; remove '%s' and add '%s' under '%s' to "
|
||||
"require encryption",
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
CONF_API,
|
||||
|
||||
@@ -64,8 +64,11 @@ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
|
||||
*p++ = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
||||
*p++ = server_feature_flags;
|
||||
|
||||
int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY
|
||||
: this->noise_->handshake.init(this->noise_ctx_, prologue, sizeof(prologue));
|
||||
// Codegen always pairs USE_OTA_ENCRYPTION with a real key; never fall back
|
||||
// to the all-zeros provisioning key here
|
||||
int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY
|
||||
: !this->noise_ctx_.has_psk() ? NOISE_ERROR_PSK_REQUIRED
|
||||
: this->noise_->handshake.init(this->noise_ctx_, prologue, sizeof(prologue));
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Session init: %d", err);
|
||||
this->cleanup_connection_();
|
||||
@@ -142,7 +145,7 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() {
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
ESP_LOGV(TAG, "Bad handshake state");
|
||||
ESP_LOGW(TAG, "Bad handshake state");
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
@@ -151,7 +154,7 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() {
|
||||
}
|
||||
|
||||
/// Payload length from a frame header, or 0 (logged) when the indicator or
|
||||
/// the length is out of range.
|
||||
/// the length is out of range. Callers pass min_len >= 1 so 0 is never valid.
|
||||
size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) {
|
||||
const size_t payload_len = encode_uint16(header[1], header[2]);
|
||||
if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) {
|
||||
|
||||
+2
-2
@@ -537,8 +537,8 @@ def perform_ota(
|
||||
raise OTAError(
|
||||
"An OTA encryption key is configured but the device did not "
|
||||
"offer encryption; refusing to send the image in plaintext. "
|
||||
"The running firmware was built before OTA encryption "
|
||||
"(ESPHome 2026.9.0) or without an 'api: encryption: key'. "
|
||||
"The running firmware does not offer encryption (built before "
|
||||
"ESPHome 2026.9.0 or without an 'api: encryption: key'). "
|
||||
"If the config has an 'api: encryption: key', install once "
|
||||
"with the 'ota: encryption:' block removed (that firmware "
|
||||
"offers encryption), then restore the block and install "
|
||||
|
||||
+8
-4
@@ -148,11 +148,14 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str:
|
||||
if "api_encryption_key" in kwargs:
|
||||
config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n'
|
||||
|
||||
# Configure OTA
|
||||
# Configure OTA: the api key also secures OTA updates, a password is only
|
||||
# for uploaders that do not support encryption
|
||||
config += "\nota:\n"
|
||||
config += " - platform: esphome\n"
|
||||
if "ota_password" in kwargs:
|
||||
config += f' password: "{kwargs["ota_password"]}"'
|
||||
elif "api_encryption_key" in kwargs:
|
||||
config += " encryption:"
|
||||
|
||||
# Configuring wifi
|
||||
config += "\n\nwifi:\n"
|
||||
@@ -532,12 +535,13 @@ def wizard(path: Path) -> int:
|
||||
|
||||
safe_print()
|
||||
safe_print(
|
||||
f"Do you want to set a {color(AnsiFore.GREEN, 'password')} for OTA updates? "
|
||||
"This can be insecure if you do not trust the WiFi network."
|
||||
"The API encryption key also secures OTA updates. Do you want to set a "
|
||||
f"{color(AnsiFore.GREEN, 'password')} for OTA updates instead? Only "
|
||||
"older uploaders that do not support encryption need one."
|
||||
)
|
||||
safe_print()
|
||||
sleep(0.25)
|
||||
safe_print("Press ENTER for no password")
|
||||
safe_print("Press ENTER to use the encryption key")
|
||||
ota_password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): "))
|
||||
else:
|
||||
ssid, psk = "", ""
|
||||
|
||||
@@ -452,7 +452,7 @@ def test_encryption_offer_codegen(
|
||||
assert defines_present <= defines
|
||||
assert not (defines_absent & defines)
|
||||
encrypted = "USE_OTA_ENCRYPTION" in defines_present
|
||||
assert ("set_noise_psk(" in main_cpp) is encrypted
|
||||
assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is encrypted
|
||||
assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present)
|
||||
# The noise transport source compiles only when the define is set
|
||||
assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"])
|
||||
|
||||
@@ -101,6 +101,25 @@ def test_config_file_should_include_ota(default_config: dict[str, Any]):
|
||||
assert "ota:" in config
|
||||
|
||||
|
||||
def test_config_file_should_use_encryption_when_api_key_set(
|
||||
default_config: dict[str, Any],
|
||||
):
|
||||
"""
|
||||
With an API encryption key and no OTA password the OTA block reuses the key
|
||||
"""
|
||||
# Given
|
||||
default_config["api_encryption_key"] = (
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
)
|
||||
|
||||
# When
|
||||
config = wz.wizard_file(**default_config)
|
||||
|
||||
# Then
|
||||
assert "ota:\n - platform: esphome\n encryption:" in config
|
||||
assert "password" not in config.split("ota:")[1].split("wifi:")[0]
|
||||
|
||||
|
||||
def test_config_file_should_include_ota_when_password_set(
|
||||
default_config: dict[str, Any],
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user