mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Allow user to override insecure setup codes and pair with homekit_controller (#50986)
* Allow user to override invalid setup codes and pair with homekit_controller * adjust from manual testing * invalid -> insecure
This commit is contained in:
parent
87438dd401
commit
4b0b0f5db7
@ -37,7 +37,7 @@ PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$")
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DISALLOWED_CODES = {
|
INSECURE_CODES = {
|
||||||
"00000000",
|
"00000000",
|
||||||
"11111111",
|
"11111111",
|
||||||
"22222222",
|
"22222222",
|
||||||
@ -66,7 +66,7 @@ def find_existing_host(hass, serial):
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def ensure_pin_format(pin):
|
def ensure_pin_format(pin, allow_insecure_setup_codes=None):
|
||||||
"""
|
"""
|
||||||
Ensure a pin code is correctly formatted.
|
Ensure a pin code is correctly formatted.
|
||||||
|
|
||||||
@ -78,8 +78,8 @@ def ensure_pin_format(pin):
|
|||||||
if not match:
|
if not match:
|
||||||
raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
|
raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
|
||||||
pin_without_dashes = "".join(match.groups())
|
pin_without_dashes = "".join(match.groups())
|
||||||
if pin_without_dashes in DISALLOWED_CODES:
|
if not allow_insecure_setup_codes and pin_without_dashes in INSECURE_CODES:
|
||||||
raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}")
|
raise InsecureSetupCode(f"Invalid PIN code f{pin}")
|
||||||
return "-".join(match.groups())
|
return "-".join(match.groups())
|
||||||
|
|
||||||
|
|
||||||
@ -310,7 +310,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if pair_info and self.finish_pairing:
|
if pair_info and self.finish_pairing:
|
||||||
code = pair_info["pairing_code"]
|
code = pair_info["pairing_code"]
|
||||||
try:
|
try:
|
||||||
code = ensure_pin_format(code)
|
code = ensure_pin_format(
|
||||||
|
code,
|
||||||
|
allow_insecure_setup_codes=pair_info.get(
|
||||||
|
"allow_insecure_setup_codes"
|
||||||
|
),
|
||||||
|
)
|
||||||
pairing = await self.finish_pairing(code)
|
pairing = await self.finish_pairing(code)
|
||||||
return await self._entry_from_accessory(pairing)
|
return await self._entry_from_accessory(pairing)
|
||||||
except aiohomekit.exceptions.MalformedPinError:
|
except aiohomekit.exceptions.MalformedPinError:
|
||||||
@ -336,6 +341,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
except aiohomekit.AccessoryNotFoundError:
|
except aiohomekit.AccessoryNotFoundError:
|
||||||
# Can no longer find the device on the network
|
# Can no longer find the device on the network
|
||||||
return self.async_abort(reason="accessory_not_found_error")
|
return self.async_abort(reason="accessory_not_found_error")
|
||||||
|
except InsecureSetupCode:
|
||||||
|
errors["pairing_code"] = "insecure_setup_code"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Pairing attempt failed with an unhandled exception")
|
_LOGGER.exception("Pairing attempt failed with an unhandled exception")
|
||||||
self.finish_pairing = None
|
self.finish_pairing = None
|
||||||
@ -399,13 +406,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
placeholders = {"name": self.name}
|
placeholders = {"name": self.name}
|
||||||
self.context["title_placeholders"] = {"name": self.name}
|
self.context["title_placeholders"] = {"name": self.name}
|
||||||
|
|
||||||
|
schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)}
|
||||||
|
if errors and errors.get("pairing_code") == "insecure_setup_code":
|
||||||
|
schema[vol.Optional("allow_insecure_setup_codes")] = bool
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="pair",
|
step_id="pair",
|
||||||
errors=errors or {},
|
errors=errors or {},
|
||||||
description_placeholders=placeholders,
|
description_placeholders=placeholders,
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(schema),
|
||||||
{vol.Required("pairing_code"): vol.All(str, vol.Strip)}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _entry_from_accessory(self, pairing):
|
async def _entry_from_accessory(self, pairing):
|
||||||
@ -428,3 +437,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
name = get_accessory_name(bridge_info)
|
name = get_accessory_name(bridge_info)
|
||||||
|
|
||||||
return self.async_create_entry(title=name, data=pairing_data)
|
return self.async_create_entry(title=name, data=pairing_data)
|
||||||
|
|
||||||
|
|
||||||
|
class InsecureSetupCode(Exception):
|
||||||
|
"""An exception for insecure trivial setup codes."""
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
"title": "Pair with a device via HomeKit Accessory Protocol",
|
"title": "Pair with a device via HomeKit Accessory Protocol",
|
||||||
"description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
|
"description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
|
||||||
"data": {
|
"data": {
|
||||||
"pairing_code": "Pairing Code"
|
"pairing_code": "Pairing Code",
|
||||||
|
"allow_insecure_setup_codes": "Allow pairing with insecure setup codes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protocol_error": {
|
"protocol_error": {
|
||||||
@ -31,6 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"insecure_setup_code": "The requested setup code is insecure because of its trivial nature. This accessory fails to meet basic security requirements.",
|
||||||
"unable_to_pair": "Unable to pair, please try again.",
|
"unable_to_pair": "Unable to pair, please try again.",
|
||||||
"unknown_error": "Device reported an unknown error. Pairing failed.",
|
"unknown_error": "Device reported an unknown error. Pairing failed.",
|
||||||
"authentication_error": "Incorrect HomeKit code. Please check it and try again.",
|
"authentication_error": "Incorrect HomeKit code. Please check it and try again.",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"authentication_error": "Incorrect HomeKit code. Please check it and try again.",
|
"authentication_error": "Incorrect HomeKit code. Please check it and try again.",
|
||||||
|
"insecure_setup_code": "The requested setup code is insecure because of its trivial nature. This accessory fails to meet basic security requirements.",
|
||||||
"max_peers_error": "Device refused to add pairing as it has no free pairing storage.",
|
"max_peers_error": "Device refused to add pairing as it has no free pairing storage.",
|
||||||
"pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.",
|
"pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.",
|
||||||
"unable_to_pair": "Unable to pair, please try again.",
|
"unable_to_pair": "Unable to pair, please try again.",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"pair": {
|
"pair": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"allow_insecure_setup_codes": "Allow pairing with insecure setup codes.",
|
||||||
"pairing_code": "Pairing Code"
|
"pairing_code": "Pairing Code"
|
||||||
},
|
},
|
||||||
"description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
|
"description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
|
||||||
|
@ -42,6 +42,16 @@ PAIRING_FINISH_ABORT_ERRORS = [
|
|||||||
(aiohomekit.AccessoryNotFoundError, "accessory_not_found_error")
|
(aiohomekit.AccessoryNotFoundError, "accessory_not_found_error")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
INSECURE_PAIRING_CODES = [
|
||||||
|
"111-11-111",
|
||||||
|
"123-45-678",
|
||||||
|
"22222222",
|
||||||
|
"111-11-111 ",
|
||||||
|
" 111-11-111",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
INVALID_PAIRING_CODES = [
|
INVALID_PAIRING_CODES = [
|
||||||
"aaa-aa-aaa",
|
"aaa-aa-aaa",
|
||||||
"aaa-11-aaa",
|
"aaa-11-aaa",
|
||||||
@ -49,11 +59,8 @@ INVALID_PAIRING_CODES = [
|
|||||||
"aaa-aa-111",
|
"aaa-aa-111",
|
||||||
"1111-1-111",
|
"1111-1-111",
|
||||||
"a111-11-111",
|
"a111-11-111",
|
||||||
" 111-11-111",
|
|
||||||
"111-11-111 ",
|
|
||||||
"111-11-111a",
|
"111-11-111a",
|
||||||
"1111111",
|
"1111111",
|
||||||
"22222222",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -94,6 +101,15 @@ def test_invalid_pairing_codes(pairing_code):
|
|||||||
config_flow.ensure_pin_format(pairing_code)
|
config_flow.ensure_pin_format(pairing_code)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pairing_code", INSECURE_PAIRING_CODES)
|
||||||
|
def test_insecure_pairing_codes(pairing_code):
|
||||||
|
"""Test ensure_pin_format raises for an invalid setup code."""
|
||||||
|
with pytest.raises(config_flow.InsecureSetupCode):
|
||||||
|
config_flow.ensure_pin_format(pairing_code)
|
||||||
|
|
||||||
|
config_flow.ensure_pin_format(pairing_code, allow_insecure_setup_codes=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pairing_code", VALID_PAIRING_CODES)
|
@pytest.mark.parametrize("pairing_code", VALID_PAIRING_CODES)
|
||||||
def test_valid_pairing_codes(pairing_code):
|
def test_valid_pairing_codes(pairing_code):
|
||||||
"""Test ensure_pin_format corrects format for a valid pin in an alternative format."""
|
"""Test ensure_pin_format corrects format for a valid pin in an alternative format."""
|
||||||
@ -624,6 +640,49 @@ async def test_user_works(hass, controller):
|
|||||||
assert result["title"] == "Koogeek-LS1-20833F"
|
assert result["title"] == "Koogeek-LS1-20833F"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_pairing_with_insecure_setup_code(hass, controller):
|
||||||
|
"""Test user initiated disovers devices."""
|
||||||
|
device = setup_mock_accessory(controller)
|
||||||
|
device.pairing_code = "123-45-678"
|
||||||
|
|
||||||
|
# Device is discovered
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
"homekit_controller", context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert get_flow_context(hass, result) == {
|
||||||
|
"source": config_entries.SOURCE_USER,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={"device": "TestDevice"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "pair"
|
||||||
|
|
||||||
|
assert get_flow_context(hass, result) == {
|
||||||
|
"source": config_entries.SOURCE_USER,
|
||||||
|
"unique_id": "00:00:00:00:00:00",
|
||||||
|
"title_placeholders": {"name": "TestDevice"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={"pairing_code": "123-45-678"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "pair"
|
||||||
|
assert result["errors"] == {"pairing_code": "insecure_setup_code"}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={"pairing_code": "123-45-678", "allow_insecure_setup_codes": True},
|
||||||
|
)
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "Koogeek-LS1-20833F"
|
||||||
|
|
||||||
|
|
||||||
async def test_user_no_devices(hass, controller):
|
async def test_user_no_devices(hass, controller):
|
||||||
"""Test user initiated pairing where no devices discovered."""
|
"""Test user initiated pairing where no devices discovered."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user