KNX ConfigFlow: Validate contents of knxkeys file (#84411)

pull/84607/head^2
Matthias Alphart 2022-12-27 21:00:19 +01:00 committed by GitHub
parent c99025be26
commit a752232de8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 44 deletions

View File

@ -440,18 +440,37 @@ class KNXCommonFlow(ABC, FlowHandler):
) -> FlowResult:
"""Configure secure knxkeys used to authenticate."""
errors = {}
description_placeholders = {}
if user_input is not None:
connection_type = self.new_entry_data[CONF_KNX_CONNECTION_TYPE]
storage_key = CONST_KNX_STORAGE_KEY + user_input[CONF_KNX_KNXKEY_FILENAME]
try:
await load_keyring(
keyring = await load_keyring(
path=self.hass.config.path(STORAGE_DIR, storage_key),
password=user_input[CONF_KNX_KNXKEY_PASSWORD],
)
except FileNotFoundError:
errors[CONF_KNX_KNXKEY_FILENAME] = "file_not_found"
errors[CONF_KNX_KNXKEY_FILENAME] = "keyfile_not_found"
except InvalidSecureConfiguration:
errors[CONF_KNX_KNXKEY_PASSWORD] = "invalid_signature"
errors[CONF_KNX_KNXKEY_PASSWORD] = "keyfile_invalid_signature"
else:
if (
connection_type == CONF_KNX_TUNNELING_TCP_SECURE
and self._selected_tunnel is not None
):
tunnel_endpoints = []
if host_ia := self._selected_tunnel.individual_address:
tunnel_endpoints = keyring.get_tunnel_interfaces_by_host(
host=host_ia
)
if not tunnel_endpoints:
errors["base"] = "keyfile_no_tunnel_for_host"
description_placeholders = {CONF_HOST: str(host_ia)}
if connection_type == CONF_KNX_ROUTING_SECURE:
if not (keyring.backbone is not None and keyring.backbone.key):
errors["base"] = "keyfile_no_backbone_key"
if not errors:
self.new_entry_data |= KNXConfigEntryData(
@ -463,10 +482,7 @@ class KNXCommonFlow(ABC, FlowHandler):
user_id=None,
user_password=None,
)
if (
self.new_entry_data[CONF_KNX_CONNECTION_TYPE]
== CONF_KNX_ROUTING_SECURE
):
if connection_type == CONF_KNX_ROUTING_SECURE:
title = (
"Secure Routing as"
f" {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}"
@ -488,7 +504,10 @@ class KNXCommonFlow(ABC, FlowHandler):
}
return self.async_show_form(
step_id="secure_knxkeys", data_schema=vol.Schema(fields), errors=errors
step_id="secure_knxkeys",
data_schema=vol.Schema(fields),
errors=errors,
description_placeholders=description_placeholders,
)
async def async_step_routing(self, user_input: dict | None = None) -> FlowResult:

View File

@ -2,18 +2,21 @@
"config": {
"step": {
"connection_type": {
"title": "KNX connection",
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"data": {
"connection_type": "KNX Connection Type"
}
},
"tunnel": {
"title": "Tunnel",
"description": "Please select a gateway from the list.",
"data": {
"gateway": "KNX Tunnel Connection"
}
},
"manual_tunnel": {
"title": "Tunnel settings",
"description": "Please enter the connection information of your tunneling device.",
"data": {
"tunneling_type": "KNX Tunneling Type",
@ -30,6 +33,7 @@
}
},
"secure_key_source": {
"title": "KNX IP-Secure",
"description": "Select how you want to configure KNX/IP Secure.",
"menu_options": {
"secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys",
@ -38,6 +42,7 @@
}
},
"secure_knxkeys": {
"title": "Keyfile",
"description": "Please enter the information for your `.knxkeys` file.",
"data": {
"knxkeys_filename": "The filename of your `.knxkeys` file (including extension)",
@ -49,6 +54,7 @@
}
},
"secure_tunnel_manual": {
"title": "Secure tunnelling",
"description": "Please enter your IP secure information.",
"data": {
"user_id": "User ID",
@ -62,6 +68,7 @@
}
},
"secure_routing_manual": {
"title": "Secure routing",
"description": "Please enter your IP secure information.",
"data": {
"backbone_key": "Backbone key",
@ -73,6 +80,7 @@
}
},
"routing": {
"title": "Routing",
"description": "Please configure the routing options.",
"data": {
"individual_address": "Individual address",
@ -96,8 +104,10 @@
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
"invalid_ip_address": "Invalid IPv4 address.",
"invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
"file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
"keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.",
"keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.",
"keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
"no_router_discovered": "No KNXnet/IP router was discovered on the network.",
"no_tunnel_discovered": "Could not find a KNX tunneling server on your network.",
"unsupported_tunnel_type": "Selected tunnelling type not supported by gateway."
@ -106,12 +116,14 @@
"options": {
"step": {
"options_init": {
"title": "KNX Settings",
"menu_options": {
"connection_type": "Configure KNX interface",
"communication_settings": "Communication settings"
}
},
"communication_settings": {
"title": "Communication settings",
"data": {
"state_updater": "State updater",
"rate_limit": "Rate limit"
@ -122,18 +134,21 @@
}
},
"connection_type": {
"title": "[%key:component::knx::config::step::connection_type::title%]",
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"data": {
"connection_type": "KNX Connection Type"
}
},
"tunnel": {
"title": "[%key:component::knx::config::step::tunnel::title%]",
"description": "[%key:component::knx::config::step::tunnel::description%]",
"data": {
"gateway": "[%key:component::knx::config::step::tunnel::data::gateway%]"
}
},
"manual_tunnel": {
"title": "[%key:component::knx::config::step::manual_tunnel::title%]",
"description": "[%key:component::knx::config::step::manual_tunnel::description%]",
"data": {
"tunneling_type": "[%key:component::knx::config::step::manual_tunnel::data::tunneling_type%]",
@ -150,6 +165,7 @@
}
},
"secure_key_source": {
"title": "[%key:component::knx::config::step::secure_key_source::title%]",
"description": "[%key:component::knx::config::step::secure_key_source::description%]",
"menu_options": {
"secure_knxkeys": "[%key:component::knx::config::step::secure_key_source::menu_options::secure_knxkeys%]",
@ -158,6 +174,7 @@
}
},
"secure_knxkeys": {
"title": "[%key:component::knx::config::step::secure_knxkeys::title%]",
"description": "[%key:component::knx::config::step::secure_knxkeys::description%]",
"data": {
"knxkeys_filename": "[%key:component::knx::config::step::secure_knxkeys::data::knxkeys_filename%]",
@ -169,6 +186,7 @@
}
},
"secure_tunnel_manual": {
"title": "[%key:component::knx::config::step::secure_tunnel_manual::title%]",
"description": "[%key:component::knx::config::step::secure_tunnel_manual::description%]",
"data": {
"user_id": "[%key:component::knx::config::step::secure_tunnel_manual::data::user_id%]",
@ -182,6 +200,7 @@
}
},
"secure_routing_manual": {
"title": "[%key:component::knx::config::step::secure_routing_manual::title%]",
"description": "[%key:component::knx::config::step::secure_routing_manual::description%]",
"data": {
"backbone_key": "[%key:component::knx::config::step::secure_routing_manual::data::backbone_key%]",
@ -193,6 +212,7 @@
}
},
"routing": {
"title": "[%key:component::knx::config::step::routing::title%]",
"description": "[%key:component::knx::config::step::routing::description%]",
"data": {
"individual_address": "[%key:component::knx::config::step::routing::data::individual_address%]",
@ -212,8 +232,10 @@
"invalid_backbone_key": "[%key:component::knx::config::error::invalid_backbone_key%]",
"invalid_individual_address": "[%key:component::knx::config::error::invalid_individual_address%]",
"invalid_ip_address": "[%key:component::knx::config::error::invalid_ip_address%]",
"invalid_signature": "[%key:component::knx::config::error::invalid_signature%]",
"file_not_found": "[%key:component::knx::config::error::file_not_found%]",
"keyfile_no_backbone_key": "[%key:component::knx::config::error::keyfile_no_backbone_key%]",
"keyfile_invalid_signature": "[%key:component::knx::config::error::keyfile_invalid_signature%]",
"keyfile_no_tunnel_for_host": "[%key:component::knx::config::error::keyfile_no_tunnel_for_host%]",
"keyfile_not_found": "[%key:component::knx::config::error::keyfile_not_found%]",
"no_router_discovered": "[%key:component::knx::config::error::no_router_discovered%]",
"no_tunnel_discovered": "[%key:component::knx::config::error::no_tunnel_discovered%]",
"unsupported_tunnel_type": "[%key:component::knx::config::error::unsupported_tunnel_type%]"

View File

@ -6,11 +6,13 @@
},
"error": {
"cannot_connect": "Failed to connect",
"file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
"invalid_ip_address": "Invalid IPv4 address.",
"invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
"keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.",
"keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.",
"keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
"no_router_discovered": "No KNXnet/IP router was discovered on the network.",
"no_tunnel_discovered": "Could not find a KNX tunneling server on your network.",
"unsupported_tunnel_type": "Selected tunnelling type not supported by gateway."
@ -20,7 +22,8 @@
"data": {
"connection_type": "KNX Connection Type"
},
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing."
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"title": "KNX connection"
},
"manual_tunnel": {
"data": {
@ -36,7 +39,8 @@
"port": "Port of the KNX/IP tunneling device.",
"route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections."
},
"description": "Please enter the connection information of your tunneling device."
"description": "Please enter the connection information of your tunneling device.",
"title": "Tunnel settings"
},
"routing": {
"data": {
@ -50,7 +54,8 @@
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
"local_ip": "Leave blank to use auto-discovery."
},
"description": "Please configure the routing options."
"description": "Please configure the routing options.",
"title": "Routing"
},
"secure_key_source": {
"description": "Select how you want to configure KNX/IP Secure.",
@ -58,7 +63,8 @@
"secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys",
"secure_routing_manual": "Configure IP secure backbone key manually",
"secure_tunnel_manual": "Configure IP secure credentials manually"
}
},
"title": "KNX IP-Secure"
},
"secure_knxkeys": {
"data": {
@ -69,7 +75,8 @@
"knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`",
"knxkeys_password": "This was set when exporting the file from ETS."
},
"description": "Please enter the information for your `.knxkeys` file."
"description": "Please enter the information for your `.knxkeys` file.",
"title": "Keyfile"
},
"secure_routing_manual": {
"data": {
@ -80,7 +87,8 @@
"backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'",
"sync_latency_tolerance": "Default is 1000."
},
"description": "Please enter your IP secure information."
"description": "Please enter your IP secure information.",
"title": "Secure routing"
},
"secure_tunnel_manual": {
"data": {
@ -93,24 +101,28 @@
"user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.",
"user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS."
},
"description": "Please enter your IP secure information."
"description": "Please enter your IP secure information.",
"title": "Secure tunnelling"
},
"tunnel": {
"data": {
"gateway": "KNX Tunnel Connection"
},
"description": "Please select a gateway from the list."
"description": "Please select a gateway from the list.",
"title": "Tunnel"
}
}
},
"options": {
"error": {
"cannot_connect": "Failed to connect",
"file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
"invalid_ip_address": "Invalid IPv4 address.",
"invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
"keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.",
"keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.",
"keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
"no_router_discovered": "No KNXnet/IP router was discovered on the network.",
"no_tunnel_discovered": "Could not find a KNX tunneling server on your network.",
"unsupported_tunnel_type": "Selected tunnelling type not supported by gateway."
@ -124,13 +136,15 @@
"data_description": {
"rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40",
"state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options."
}
},
"title": "Communication settings"
},
"connection_type": {
"data": {
"connection_type": "KNX Connection Type"
},
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing."
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"title": "KNX connection"
},
"manual_tunnel": {
"data": {
@ -146,13 +160,15 @@
"port": "Port of the KNX/IP tunneling device.",
"route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections."
},
"description": "Please enter the connection information of your tunneling device."
"description": "Please enter the connection information of your tunneling device.",
"title": "Tunnel settings"
},
"options_init": {
"menu_options": {
"communication_settings": "Communication settings",
"connection_type": "Configure KNX interface"
}
},
"title": "KNX Settings"
},
"routing": {
"data": {
@ -166,7 +182,8 @@
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
"local_ip": "Leave blank to use auto-discovery."
},
"description": "Please configure the routing options."
"description": "Please configure the routing options.",
"title": "Routing"
},
"secure_key_source": {
"description": "Select how you want to configure KNX/IP Secure.",
@ -174,7 +191,8 @@
"secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys",
"secure_routing_manual": "Configure IP secure backbone key manually",
"secure_tunnel_manual": "Configure IP secure credentials manually"
}
},
"title": "KNX IP-Secure"
},
"secure_knxkeys": {
"data": {
@ -185,7 +203,8 @@
"knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`",
"knxkeys_password": "This was set when exporting the file from ETS."
},
"description": "Please enter the information for your `.knxkeys` file."
"description": "Please enter the information for your `.knxkeys` file.",
"title": "Keyfile"
},
"secure_routing_manual": {
"data": {
@ -196,7 +215,8 @@
"backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'",
"sync_latency_tolerance": "Default is 1000."
},
"description": "Please enter your IP secure information."
"description": "Please enter your IP secure information.",
"title": "Secure routing"
},
"secure_tunnel_manual": {
"data": {
@ -209,13 +229,15 @@
"user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.",
"user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS."
},
"description": "Please enter your IP secure information."
"description": "Please enter your IP secure information.",
"title": "Secure tunnelling"
},
"tunnel": {
"data": {
"gateway": "KNX Tunnel Connection"
},
"description": "Please select a gateway from the list."
"description": "Please select a gateway from the list.",
"title": "Tunnel"
}
}
}

View File

@ -1,10 +1,11 @@
"""Test the KNX config flow."""
from unittest.mock import patch
from unittest.mock import Mock, patch
import pytest
from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.io.gateway_scanner import GatewayDescriptor
from xknx.telegram import IndividualAddress
from homeassistant import config_entries
from homeassistant.components.knx.config_flow import (
@ -44,6 +45,8 @@ from homeassistant.data_entry_flow import FlowResult, FlowResultType
from tests.common import MockConfigEntry
GATEWAY_INDIVIDUAL_ADDRESS = IndividualAddress("1.0.0")
@pytest.fixture(name="knx_setup")
def fixture_knx_setup():
@ -63,6 +66,7 @@ def _gateway_descriptor(
"""Get mock gw descriptor."""
descriptor = GatewayDescriptor(
name="Test",
individual_address=GATEWAY_INDIVIDUAL_ADDRESS,
ip_addr=ip,
port=port,
local_interface="eth0",
@ -352,9 +356,25 @@ async def test_routing_secure_keyfile(
assert result4["step_id"] == "secure_knxkeys"
assert not result4["errors"]
# test file without backbone key
with patch(
"homeassistant.components.knx.config_flow.load_keyring", return_value=True
):
"homeassistant.components.knx.config_flow.load_keyring"
) as mock_load_keyring:
mock_keyring = Mock()
mock_keyring.backbone.key = None
mock_load_keyring.return_value = mock_keyring
secure_knxkeys = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
},
)
assert secure_knxkeys["type"] == FlowResultType.FORM
assert secure_knxkeys["errors"] == {"base": "keyfile_no_backbone_key"}
# test valid file
with patch("homeassistant.components.knx.config_flow.load_keyring"):
routing_secure_knxkeys = await hass.config_entries.flow.async_configure(
result4["flow_id"],
{
@ -976,8 +996,11 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup):
assert not result["errors"]
with patch(
"homeassistant.components.knx.config_flow.load_keyring", return_value=True
):
"homeassistant.components.knx.config_flow.load_keyring"
) as mock_load_keyring:
mock_keyring = Mock()
mock_keyring.get_tunnel_interfaces_by_host.return_value = ["stub"]
mock_load_keyring.return_value = mock_keyring
secure_knxkeys = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@ -1031,7 +1054,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant):
)
assert secure_knxkeys["type"] == FlowResultType.FORM
assert secure_knxkeys["errors"]
assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found"
assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "keyfile_not_found"
async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant):
@ -1059,7 +1082,39 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant):
)
assert secure_knxkeys["type"] == FlowResultType.FORM
assert secure_knxkeys["errors"]
assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature"
assert (
secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD]
== "keyfile_invalid_signature"
)
async def test_configure_secure_knxkeys_no_tunnel_for_host(hass: HomeAssistant):
"""Test configure secure knxkeys but file was not found."""
menu_step = await _get_menu_step(hass)
result = await hass.config_entries.flow.async_configure(
menu_step["flow_id"],
{"next_step_id": "secure_knxkeys"},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "secure_knxkeys"
assert not result["errors"]
with patch(
"homeassistant.components.knx.config_flow.load_keyring"
) as mock_load_keyring:
mock_keyring = Mock()
mock_keyring.get_tunnel_interfaces_by_host.return_value = []
mock_load_keyring.return_value = mock_keyring
secure_knxkeys = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
},
)
assert secure_knxkeys["type"] == FlowResultType.FORM
assert secure_knxkeys["errors"] == {"base": "keyfile_no_tunnel_for_host"}
async def test_options_flow_connection_type(
@ -1185,9 +1240,7 @@ async def test_options_flow_secure_manual_to_keyfile(
assert result4["step_id"] == "secure_knxkeys"
assert not result4["errors"]
with patch(
"homeassistant.components.knx.config_flow.load_keyring", return_value=True
):
with patch("homeassistant.components.knx.config_flow.load_keyring"):
secure_knxkeys = await hass.config_entries.options.async_configure(
result4["flow_id"],
{