Skip to content

Commit

Permalink
Create repairs issue if Thread network is insecure (home-assistant#88888
Browse files Browse the repository at this point in the history
)

* Bump python-otbr-api to 1.0.5

* Create repairs issue if Thread network is insecure

* Address review comments
  • Loading branch information
emontnemery authored and balloob committed Feb 28, 2023
1 parent 32b138b commit a8e1dc8
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 10 deletions.
65 changes: 62 additions & 3 deletions homeassistant/components/otbr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@

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

Expand All @@ -23,6 +26,18 @@
_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]]
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/otbr/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
6 changes: 6 additions & 0 deletions homeassistant/components/otbr/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
2 changes: 1 addition & 1 deletion homeassistant/components/thread/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."]
}
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions tests/components/otbr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,15 @@
"0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102"
"25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8"
)

DATASET_INSECURE_NW_KEY = bytes.fromhex(
"0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDD24657"
"0A336069051000112233445566778899AABBCCDDEEFF030E4F70656E54687265616444656D6F01"
"0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8"
)

DATASET_INSECURE_PASSPHRASE = bytes.fromhex(
"0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDD24657"
"0A336069051000112233445566778899AABBCCDDEEFA030E4F70656E54687265616444656D6F01"
"0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8"
)
6 changes: 5 additions & 1 deletion tests/components/otbr/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
45 changes: 43 additions & 2 deletions tests/components/otbr/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@
from homeassistant.components import otbr
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from . import BASE_URL, CONFIG_ENTRY_DATA, DATASET
from homeassistant.helpers import issue_registry as ir

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


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,
Expand All @@ -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(
Expand Down

0 comments on commit a8e1dc8

Please sign in to comment.