Add support for KNX IP-Secure routing (#82765)

* always use instance variable for new entry data

- change `self._tunneling_config` to non-optional `self.new_entry_data`
- always use self.new_entry_data in `finish_flow()`

* support secure routing

* amend current tests

* use sync latency tolerance

* test secure routing config flow

* diagnostics redact backbone_key

* test xknx library setup

* check length of backbone_key

* better readable key validation
This commit is contained in:
Matthias Alphart 2022-11-27 23:33:12 +01:00 committed by GitHub
parent d6e287f47a
commit 4517af509c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 422 additions and 88 deletions

View file

@ -26,6 +26,9 @@ from homeassistant.components.knx.const import (
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTE_BACK,
CONF_KNX_ROUTING,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_ID,
CONF_KNX_SECURE_USER_PASSWORD,
@ -197,6 +200,162 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1
async def test_routing_secure_manual_setup(hass: HomeAssistant) -> None:
"""Test routing secure setup with manual key config."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.FORM
assert result2["step_id"] == "routing"
assert result2["errors"] == {"base": "no_router_discovered"}
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
CONF_KNX_MCAST_PORT: 3671,
CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123",
CONF_KNX_ROUTING_SECURE: True,
},
)
assert result3["type"] == FlowResultType.MENU
assert result3["step_id"] == "secure_key_source"
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{"next_step_id": "secure_routing_manual"},
)
assert result4["type"] == FlowResultType.FORM
assert result4["step_id"] == "secure_routing_manual"
assert not result4["errors"]
result_invalid_key1 = await hass.config_entries.flow.async_configure(
result4["flow_id"],
{
CONF_KNX_ROUTING_BACKBONE_KEY: "xxaacc44bbaacc44bbaacc44bbaaccyy", # invalid hex string
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000,
},
)
assert result_invalid_key1["type"] == FlowResultType.FORM
assert result_invalid_key1["step_id"] == "secure_routing_manual"
assert result_invalid_key1["errors"] == {"backbone_key": "invalid_backbone_key"}
result_invalid_key2 = await hass.config_entries.flow.async_configure(
result4["flow_id"],
{
CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44", # invalid length
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000,
},
)
assert result_invalid_key2["type"] == FlowResultType.FORM
assert result_invalid_key2["step_id"] == "secure_routing_manual"
assert result_invalid_key2["errors"] == {"backbone_key": "invalid_backbone_key"}
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
secure_routing_manual = await hass.config_entries.flow.async_configure(
result_invalid_key2["flow_id"],
{
CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44",
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000,
},
)
await hass.async_block_till_done()
assert secure_routing_manual["type"] == FlowResultType.CREATE_ENTRY
assert secure_routing_manual["title"] == "Secure Routing as 0.0.123"
assert secure_routing_manual["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44",
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000,
CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_routing_secure_keyfile(hass: HomeAssistant) -> None:
"""Test routing secure setup with keyfile."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.FORM
assert result2["step_id"] == "routing"
assert result2["errors"] == {"base": "no_router_discovered"}
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
CONF_KNX_MCAST_PORT: 3671,
CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123",
CONF_KNX_ROUTING_SECURE: True,
},
)
assert result3["type"] == FlowResultType.MENU
assert result3["step_id"] == "secure_key_source"
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{"next_step_id": "secure_knxkeys"},
)
assert result4["type"] == FlowResultType.FORM
assert result4["step_id"] == "secure_knxkeys"
assert not result4["errors"]
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.knx.config_flow.load_keyring", return_value=True
):
routing_secure_knxkeys = await hass.config_entries.flow.async_configure(
result4["flow_id"],
{
CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
},
)
await hass.async_block_till_done()
assert routing_secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY
assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123"
assert routing_secure_knxkeys["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING_SECURE,
CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_ROUTING_BACKBONE_KEY: None,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None,
CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123",
}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
"user_input,config_entry_data",
[
@ -506,7 +665,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N
async def _get_menu_step(hass: HomeAssistant) -> FlowResult:
"""Return flow in secure_tunnellinn menu step."""
"""Return flow in secure_tunnelling menu step."""
gateway = _gateway_descriptor(
"192.168.0.1",
3675,
@ -538,7 +697,7 @@ async def _get_menu_step(hass: HomeAssistant) -> FlowResult:
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.MENU
assert result3["step_id"] == "secure_tunneling"
assert result3["step_id"] == "secure_key_source"
return result3
@ -588,7 +747,7 @@ async def test_get_secure_menu_step_manual_tunnelling(
)
await hass.async_block_till_done()
assert result3["type"] == FlowResultType.MENU
assert result3["step_id"] == "secure_tunneling"
assert result3["step_id"] == "secure_key_source"
async def test_configure_secure_tunnel_manual(hass: HomeAssistant):
@ -665,6 +824,8 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant):
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE,
CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_ROUTING_BACKBONE_KEY: None,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None,
CONF_HOST: "192.168.0.1",
CONF_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240",

View file

@ -14,6 +14,7 @@ from homeassistant.components.knx.const import (
CONF_KNX_MCAST_GRP,
CONF_KNX_MCAST_PORT,
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
@ -107,6 +108,7 @@ async def test_diagnostic_redact(
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_SECURE_USER_PASSWORD: "user_password",
CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_authentication",
CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44",
},
)
knx: KNXTestKit = KNXTestKit(hass, mock_config_entry)
@ -128,6 +130,7 @@ async def test_diagnostic_redact(
"knxkeys_password": "**REDACTED**",
"user_password": "**REDACTED**",
"device_authentication": "**REDACTED**",
"backbone_key": "**REDACTED**",
},
"configuration_error": None,
"configuration_yaml": None,

View file

@ -23,6 +23,9 @@ from homeassistant.components.knx.const import (
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTE_BACK,
CONF_KNX_ROUTING,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_ID,
CONF_KNX_SECURE_USER_PASSWORD,
@ -167,6 +170,31 @@ from tests.common import MockConfigEntry
threaded=True,
),
),
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING_SECURE,
CONF_KNX_LOCAL_IP: "192.168.1.1",
CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT,
CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER,
CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT,
CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA,
CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44",
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000,
},
ConnectionConfig(
connection_type=ConnectionType.ROUTING_SECURE,
individual_address=DEFAULT_ROUTING_IA,
multicast_group=DEFAULT_MCAST_GRP,
multicast_port=DEFAULT_MCAST_PORT,
secure_config=SecureConfig(
backbone_key="bbaacc44bbaacc44bbaacc44bbaacc44",
latency_ms=2000,
),
local_ip="192.168.1.1",
threaded=True,
),
),
],
)
async def test_init_connection_handling(