Create repairs issue if Thread network is insecure (#88888)
* Bump python-otbr-api to 1.0.5 * Create repairs issue if Thread network is insecure * Address review commentspull/88979/head
parent
32b138b6c6
commit
a8e1dc8962
|
@ -9,11 +9,14 @@ from typing import Any, Concatenate, ParamSpec, TypeVar
|
|||
|
||||
import aiohttp
|
||||
import python_otbr_api
|
||||
from python_otbr_api import tlv_parser
|
||||
from python_otbr_api.pskc import compute_pskc
|
||||
|
||||
from homeassistant.components.thread import async_add_dataset
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
@ -23,6 +26,18 @@ from .const import DOMAIN
|
|||
_R = TypeVar("_R")
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
INSECURE_NETWORK_KEYS = (
|
||||
# Thread web UI default
|
||||
bytes.fromhex("00112233445566778899AABBCCDDEEFF"),
|
||||
)
|
||||
|
||||
INSECURE_PASSPHRASES = (
|
||||
# Thread web UI default
|
||||
"j01Nme",
|
||||
# Thread documentation default
|
||||
"J01NME",
|
||||
)
|
||||
|
||||
|
||||
def _handle_otbr_error(
|
||||
func: Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]]
|
||||
|
@ -70,21 +85,65 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _warn_on_default_network_settings(
|
||||
hass: HomeAssistant, entry: ConfigEntry, dataset_tlvs: bytes
|
||||
) -> None:
|
||||
"""Warn user if insecure default network settings are used."""
|
||||
dataset = tlv_parser.parse_tlv(dataset_tlvs.hex())
|
||||
insecure = False
|
||||
|
||||
if (
|
||||
network_key := dataset.get(tlv_parser.MeshcopTLVType.NETWORKKEY)
|
||||
) is not None and bytes.fromhex(network_key) in INSECURE_NETWORK_KEYS:
|
||||
insecure = True
|
||||
if (
|
||||
not insecure
|
||||
and tlv_parser.MeshcopTLVType.EXTPANID in dataset
|
||||
and tlv_parser.MeshcopTLVType.NETWORKNAME in dataset
|
||||
and tlv_parser.MeshcopTLVType.PSKC in dataset
|
||||
):
|
||||
ext_pan_id = dataset[tlv_parser.MeshcopTLVType.EXTPANID]
|
||||
network_name = dataset[tlv_parser.MeshcopTLVType.NETWORKNAME]
|
||||
pskc = bytes.fromhex(dataset[tlv_parser.MeshcopTLVType.PSKC])
|
||||
for passphrase in INSECURE_PASSPHRASES:
|
||||
if pskc == compute_pskc(ext_pan_id, network_name, passphrase):
|
||||
insecure = True
|
||||
break
|
||||
|
||||
if insecure:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"insecure_thread_network_{entry.entry_id}",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="insecure_thread_network",
|
||||
)
|
||||
else:
|
||||
ir.async_delete_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"insecure_thread_network_{entry.entry_id}",
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up an Open Thread Border Router config entry."""
|
||||
api = python_otbr_api.OTBR(entry.data["url"], async_get_clientsession(hass), 10)
|
||||
|
||||
otbrdata = OTBRData(entry.data["url"], api)
|
||||
try:
|
||||
dataset = await otbrdata.get_active_dataset_tlvs()
|
||||
dataset_tlvs = await otbrdata.get_active_dataset_tlvs()
|
||||
except (
|
||||
HomeAssistantError,
|
||||
aiohttp.ClientError,
|
||||
asyncio.TimeoutError,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady("Unable to connect") from err
|
||||
if dataset:
|
||||
await async_add_dataset(hass, entry.title, dataset.hex())
|
||||
if dataset_tlvs:
|
||||
_warn_on_default_network_settings(hass, entry, dataset_tlvs)
|
||||
await async_add_dataset(hass, entry.title, dataset_tlvs.hex())
|
||||
|
||||
hass.data[DOMAIN] = otbrdata
|
||||
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==1.0.4"]
|
||||
"requirements": ["python-otbr-api==1.0.5"]
|
||||
}
|
||||
|
|
|
@ -14,5 +14,11 @@
|
|||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"insecure_thread_network": {
|
||||
"title": "Insecure Thread network settings detected",
|
||||
"description": "Your Thread network is using a default network key or pass phrase.\n\nThis is a security risk, please create a new Thread network."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==1.0.4", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==1.0.5", "pyroute2==0.7.5"],
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
|
|
@ -2097,7 +2097,7 @@ python-nest==4.2.0
|
|||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==1.0.4
|
||||
python-otbr-api==1.0.5
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api==1.1.0
|
||||
|
|
|
@ -1490,7 +1490,7 @@ python-nest==4.2.0
|
|||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==1.0.4
|
||||
python-otbr-api==1.0.5
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api==1.1.0
|
||||
|
|
|
@ -6,3 +6,15 @@ DATASET = bytes.fromhex(
|
|||
"0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102"
|
||||
"25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8"
|
||||
)
|
||||
|
||||
DATASET_INSECURE_NW_KEY = bytes.fromhex(
|
||||
"0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDD24657"
|
||||
"0A336069051000112233445566778899AABBCCDDEEFF030E4F70656E54687265616444656D6F01"
|
||||
"0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8"
|
||||
)
|
||||
|
||||
DATASET_INSECURE_PASSPHRASE = bytes.fromhex(
|
||||
"0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDD24657"
|
||||
"0A336069051000112233445566778899AABBCCDDEEFA030E4F70656E54687265616444656D6F01"
|
||||
"0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8"
|
||||
)
|
||||
|
|
|
@ -20,7 +20,11 @@ async def otbr_config_entry_fixture(hass):
|
|||
title="Open Thread Border Router",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET):
|
||||
with patch(
|
||||
"python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET
|
||||
), patch(
|
||||
"homeassistant.components.otbr.compute_pskc"
|
||||
): # Patch to speed up tests
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
|
||||
|
|
|
@ -10,8 +10,15 @@ import python_otbr_api
|
|||
from homeassistant.components import otbr
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from . import BASE_URL, CONFIG_ENTRY_DATA, DATASET
|
||||
from . import (
|
||||
BASE_URL,
|
||||
CONFIG_ENTRY_DATA,
|
||||
DATASET,
|
||||
DATASET_INSECURE_NW_KEY,
|
||||
DATASET_INSECURE_PASSPHRASE,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
@ -19,6 +26,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker
|
|||
|
||||
async def test_import_dataset(hass: HomeAssistant) -> None:
|
||||
"""Test the active dataset is imported at setup."""
|
||||
issue_registry = ir.async_get(hass)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
|
@ -35,6 +43,39 @@ async def test_import_dataset(hass: HomeAssistant) -> None:
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
mock_add.assert_called_once_with(config_entry.title, DATASET.hex())
|
||||
assert not issue_registry.async_get_issue(
|
||||
domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dataset", [DATASET_INSECURE_NW_KEY, DATASET_INSECURE_PASSPHRASE]
|
||||
)
|
||||
async def test_import_insecure_dataset(hass: HomeAssistant, dataset: bytes) -> None:
|
||||
"""Test the active dataset is imported at setup.
|
||||
|
||||
This imports a dataset with insecure settings.
|
||||
"""
|
||||
issue_registry = ir.async_get(hass)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
domain=otbr.DOMAIN,
|
||||
options={},
|
||||
title="My OTBR",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset
|
||||
), patch(
|
||||
"homeassistant.components.thread.dataset_store.DatasetStore.async_add"
|
||||
) as mock_add:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
mock_add.assert_called_once_with(config_entry.title, dataset.hex())
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
Loading…
Reference in New Issue