From 1d5b2738bfd147fd7f5815980cb20cf3cceac1ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:37:19 +0000 Subject: [PATCH 1/3] Initial plan From f0b43f66495376885aad618201ae72230b10aaec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:41:21 +0000 Subject: [PATCH 2/3] Handle KeyImportError for invalid SSH private key files Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/cfdfc2b5-4db9-47e3-8950-405dcc5ee9c3 --- coordinator.py | 9 ++++++++- strings.json | 3 +++ tests/unit_tests/test_async_execute.py | 12 +++++++++++- translations/de.json | 3 +++ translations/en.json | 3 +++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/coordinator.py b/coordinator.py index 5992914..f734c1c 100644 --- a/coordinator.py +++ b/coordinator.py @@ -17,7 +17,7 @@ from aiofiles import open as aioopen from aiofiles.ospath import exists -from asyncssh import HostKeyNotVerifiable, PermissionDenied, connect, read_known_hosts +from asyncssh import HostKeyNotVerifiable, KeyImportError, PermissionDenied, connect, read_known_hosts from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_COMMAND, CONF_TIMEOUT from homeassistant.core import HomeAssistant @@ -95,6 +95,13 @@ async def async_execute(self, data: dict[str, Any]) -> dict[str, Any]: translation_domain=DOMAIN, translation_key="host_key_not_verifiable", ) from exc + except KeyImportError as exc: + _LOGGER.warning("Invalid key file for %s@%s: %s", username, host, exc) + raise ServiceValidationError( + "The key file is not a valid private key.", + translation_domain=DOMAIN, + translation_key="invalid_key_file", + ) from exc except PermissionDenied as exc: _LOGGER.warning("SSH login failed for %s@%s: %s", username, host, exc) raise ServiceValidationError( diff --git a/strings.json b/strings.json index b17ed4f..99519a8 100644 --- a/strings.json +++ b/strings.json @@ -59,6 +59,9 @@ "key_file_not_found": { "message": "Could not find key file." }, + "invalid_key_file": { + "message": "The key file is not a valid private key." + }, "known_hosts_with_check_disabled": { "message": "Known hosts provided while check known hosts is disabled." }, diff --git a/tests/unit_tests/test_async_execute.py b/tests/unit_tests/test_async_execute.py index b7993ad..c14567d 100644 --- a/tests/unit_tests/test_async_execute.py +++ b/tests/unit_tests/test_async_execute.py @@ -12,7 +12,7 @@ absolute_plugin_path = str(Path(__file__).parent.parent.parent.parent.absolute()) sys.path.insert(0, absolute_plugin_path) -from asyncssh import HostKeyNotVerifiable, PermissionDenied +from asyncssh import HostKeyNotVerifiable, KeyImportError, PermissionDenied from homeassistant.exceptions import ServiceValidationError @@ -102,6 +102,16 @@ async def test_host_key_not_verifiable(self): self.assertEqual(ctx.exception.translation_key, "host_key_not_verifiable") + async def test_invalid_key_file(self): + service_call = self._make_service_call(SERVICE_DATA_BASE) + + with patch("ssh_command.coordinator.connect", return_value=_MockConnectRaises(KeyImportError("Invalid private key"))): + with patch("ssh_command.coordinator.exists", return_value=True): + with self.assertRaises(ServiceValidationError) as ctx: + await self.handler(service_call) + + self.assertEqual(ctx.exception.translation_key, "invalid_key_file") + async def test_permission_denied(self): service_call = self._make_service_call(SERVICE_DATA_BASE) diff --git a/translations/de.json b/translations/de.json index 24b7b9b..3c4e4c3 100644 --- a/translations/de.json +++ b/translations/de.json @@ -55,6 +55,9 @@ "key_file_not_found": { "message": "Konnte Schlüsseldatei nicht finden." }, + "invalid_key_file": { + "message": "Die Schlüsseldatei ist kein gültiger privater Schlüssel." + }, "known_hosts_with_check_disabled": { "message": "Bekannte Hosts wurden angegeben, obwohl die Überprüfung deaktiviert ist." }, diff --git a/translations/en.json b/translations/en.json index e2a8919..844b406 100644 --- a/translations/en.json +++ b/translations/en.json @@ -55,6 +55,9 @@ "key_file_not_found": { "message": "Could not find key file." }, + "invalid_key_file": { + "message": "The key file is not a valid private key." + }, "known_hosts_with_check_disabled": { "message": "Known hosts provided while check known hosts is disabled." }, From 22baf848cf0ebbfa45f3c6eabb20211757a9dc11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:59:58 +0000 Subject: [PATCH 3/3] Fix test_invalid_key_file: remove stale exists patch Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> Agent-Logs-Url: https://github.com/gensyn/ssh_command/sessions/d0422de0-11ab-4939-9b1a-e62ce3171fef --- tests/unit_tests/test_async_execute.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/test_async_execute.py b/tests/unit_tests/test_async_execute.py index 6cd68c2..c268587 100644 --- a/tests/unit_tests/test_async_execute.py +++ b/tests/unit_tests/test_async_execute.py @@ -104,9 +104,8 @@ async def test_invalid_key_file(self): service_call = self._make_service_call(SERVICE_DATA_BASE) with patch("ssh_command.coordinator.connect", return_value=_MockConnectRaises(KeyImportError("Invalid private key"))): - with patch("ssh_command.coordinator.exists", return_value=True): - with self.assertRaises(ServiceValidationError) as ctx: - await self.handler(service_call) + with self.assertRaises(ServiceValidationError) as ctx: + await self.handler(service_call) self.assertEqual(ctx.exception.translation_key, "invalid_key_file")