diff --git a/esphome/ota_key_edit.py b/esphome/ota_key_edit.py index 9bf4404958..d866e24bc9 100644 --- a/esphome/ota_key_edit.py +++ b/esphome/ota_key_edit.py @@ -41,14 +41,22 @@ class KeyEdit(LineEdit): def _key_blocks(raw: ConfigType, key: str) -> list[ConfigType]: """The api and esphome ota ``encryption:`` mappings whose key is ``key``.""" blocks = [raw.get(CONF_API) or {}, *esphome_ota_items(raw)] - # A bare ota block gets the api key at validation; it has no key line - return [ - block[CONF_ENCRYPTION] - for block in blocks - if (node := static_encryption_key(block)) is not None - and str(node) == key - and source_of(block[CONF_ENCRYPTION], CONF_KEY) is not None - ] + found = [] + for block in blocks: + if (node := static_encryption_key(block)) is None or str(node) != key: + continue + encryption = block[CONF_ENCRYPTION] + if source_of(encryption, CONF_KEY) is not None: + found.append(encryption) + elif hasattr(next(k for k in encryption if k == CONF_KEY), "esp_range"): + # Read from a file but not on a line of its own, as with a merge + # key; only a key validation added, which has no range at all, + # is passed over (a bare ota block takes the api key) + raise EsphomeError( + f"The '{CONF_KEY}' of an '{CONF_ENCRYPTION}:' block is not on a " + "line of its own; edit the key by hand" + ) + return found _INDENT_RE = re.compile(r"\s*") @@ -165,8 +173,8 @@ def with_sharers(edits: list[KeyEdit]) -> list[str]: f"{path} includes {match['target']} through a substitution" ) for edit in edits: - if edit.insert_after: - continue + if edit.insert_after and edit.secret: + continue # a new secrets line has no users yet ref = ( _secret_use_re(edit.secret) if edit.secret else _file_use_re(edit.path.name) ) @@ -223,6 +231,7 @@ def _secret_insert(secrets_path: Path, after: str, name: str, value: str) -> Key m.string, f'{_indent(m.string)}{name}: "{value}"', insert_after=True, + secret=name, ) diff --git a/tests/unit_tests/test_ota_key_edit.py b/tests/unit_tests/test_ota_key_edit.py index 88610d0fb6..6ad4d1ce00 100644 --- a/tests/unit_tests/test_ota_key_edit.py +++ b/tests/unit_tests/test_ota_key_edit.py @@ -1074,10 +1074,27 @@ def test_a_key_merged_from_an_anchor_is_refused(tmp_path: Path) -> None: f'named: &named\n key: "{OLD_KEY}"\n\napi:\n encryption:\n <<: *named\n\n' "ota:\n - platform: esphome\n encryption:\n", ) - with pytest.raises(EsphomeError, match="was not found on a line of the yaml"): + with pytest.raises(EsphomeError, match="not on a line of its own"): locate_key_edits(OLD_KEY, NEW_KEY) +def test_old_key_inserted_into_a_shared_include_names_the_sharers( + tmp_path: Path, +) -> None: + """The ota block lives in an include another device loads too, so the + inserted old_key reaches that device at its next install.""" + (tmp_path / "ota.yaml").write_bytes( + f'platform: esphome\nencryption:\n key: "{OLD_KEY}"\n'.encode() + ) + (tmp_path / "other.yaml").write_bytes(b"ota:\n - !include ota.yaml\n") + _setup(tmp_path, "esphome:\n name: test\n\nota:\n - !include ota.yaml\n") + edits = _edits() + assert [(e.insert_after, [p.name for p in e.shared_with]) for e in edits] == [ + (False, ["other.yaml"]), + (True, ["other.yaml"]), + ] + + def test_key_overridden_on_top_of_a_package_block(tmp_path: Path) -> None: """The device yaml's line wins the merge and is the one rewritten.""" from esphome.config_helpers import merge_config