Beispiel #1
0
    async def remove_pairing(self, pairingId):
        """
        Remove a pairing between the controller and the accessory. The pairing data is delete on both ends, on the
        accessory and the controller.

        Important: no automatic saving of the pairing data is performed. If you don't do this, the accessory seems still
            to be paired on the next start of the application.

        :param alias: the controller's alias for the accessory
        :param pairingId: the pairing id to be removed
        :raises AuthenticationError: if the controller isn't authenticated to the accessory.
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        :raises UnknownError: on unknown errors
        """
        await self._ensure_connected()

        request_tlv = [
            (TLV.kTLVType_State, TLV.M1),
            (TLV.kTLVType_Method, TLV.RemovePairing),
            (TLV.kTLVType_Identifier, pairingId.encode("utf-8")),
        ]

        data = dict(await self.connection.post_tlv("/pairings", request_tlv))

        if data.get(TLV.kTLVType_State, TLV.M2) != TLV.M2:
            raise InvalidError("Unexpected state after removing pairing request")

        if TLV.kTLVType_Error in data:
            if data[TLV.kTLVType_Error] == TLV.kTLVError_Authentication:
                raise AuthenticationError("Remove pairing failed: insufficient access")
            raise UnknownError("Remove pairing failed: unknown error")

        return True
Beispiel #2
0
    async def remove_pairing(self, pairingId):
        """
        Remove a pairing between the controller and the accessory. The pairing data is delete on both ends, on the
        accessory and the controller.

        Important: no automatic saving of the pairing data is performed. If you don't do this, the accessory seems still
            to be paired on the next start of the application.

        :param alias: the controller's alias for the accessory
        :param pairingId: the pairing id to be removed
        :raises AuthenticationError: if the controller isn't authenticated to the accessory.
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        :raises UnknownError: on unknown errors
        """
        await self._ensure_connected()

        request_tlv = [
            (TLV.kTLVType_State, TLV.M1),
            (TLV.kTLVType_Method, TLV.RemovePairing),
            (TLV.kTLVType_Identifier, pairingId.encode("utf-8")),
        ]

        data = await self.connection.post_tlv("/pairings", request_tlv)

        # act upon the response (the same is returned for IP and BLE accessories)
        # handle the result, spec says, if it has only one entry with state == M2 we unpaired, else its an error.
        logging.debug("response data: %s", data)

        if len(data) == 1 and data[0][0] == TLV.kTLVType_State and data[0][
                1] == TLV.M2:
            return True

        await self.connection.close()

        if (data[1][0] == TLV.kTLVType_Error
                and data[1][1] == TLV.kTLVError_Authentication):
            raise AuthenticationError(
                "Remove pairing failed: missing authentication")

        raise UnknownError("Remove pairing failed: unknown error")
Beispiel #3
0
async def test_discovery_invalid_config_entry(hass, controller):
    """There is already a config entry for the pairing id but it's invalid."""
    pairing = await controller.add_paired_device(Accessories(),
                                                 "00:00:00:00:00:00")

    MockConfigEntry(
        domain="homekit_controller",
        data={
            "AccessoryPairingID": "00:00:00:00:00:00"
        },
        unique_id="00:00:00:00:00:00",
    ).add_to_hass(hass)

    # We just added a mock config entry so it must be visible in hass
    assert len(hass.config_entries.async_entries()) == 1

    device = setup_mock_accessory(controller)
    discovery_info = get_device_discovery_info(device)

    # Device is discovered
    with patch.object(
            pairing,
            "list_accessories_and_characteristics",
            side_effect=AuthenticationError("Invalid pairing keys"),
    ):
        result = await hass.config_entries.flow.async_init(
            "homekit_controller",
            context={"source": config_entries.SOURCE_ZEROCONF},
            data=discovery_info,
        )

    # Discovery of a HKID that is in a pairable state but for which there is
    # already a config entry - in that case the stale config entry is
    # automatically removed.
    config_entry_count = len(hass.config_entries.async_entries())
    assert config_entry_count == 0

    # And new config flow should continue allowing user to set up a new pairing
    assert result["type"] == "form"
Beispiel #4
0
def error_handler(error: bytearray, stage: str):
    """
    Transform the various error messages defined in table 4-5 page 60 into exceptions

    :param error: the kind of error
    :param stage: the stage it appeared in
    :return: None
    """
    if error == TLV.kTLVError_Unavailable:
        raise UnavailableError(stage)
    elif error == TLV.kTLVError_Authentication:
        raise AuthenticationError(stage)
    elif error == TLV.kTLVError_Backoff:
        raise BackoffError(stage)
    elif error == TLV.kTLVError_MaxPeers:
        raise MaxPeersError(stage)
    elif error == TLV.kTLVError_MaxTries:
        raise MaxTriesError(stage)
    elif error == TLV.kTLVError_Busy:
        raise BusyError(stage)
    else:
        raise InvalidError(stage)
Beispiel #5
0
def perform_pair_setup_part2(
    pin: str, ios_pairing_id: str, salt: bytearray,
    server_public_key: bytearray
) -> Generator[Tuple[List[Tuple[int, bytearray]], List[int]], None, Dict[str,
                                                                         str]]:
    """
    Performs a pair setup operation as described in chapter 4.7 page 39 ff.

    :param pin: the setup code from the accessory
    :param ios_pairing_id: the id of the simulated ios device
    :return: a dict with the ios device's part of the pairing information
    :raises UnavailableError: if the device is already paired
    :raises MaxTriesError: if the device received more than 100 unsuccessful pairing attempts
    :raises BusyError: if a parallel pairing is ongoing
    :raises AuthenticationError: if the verification of the device's SRP proof fails
    :raises MaxPeersError: if the device cannot accept an additional pairing
    :raises IllegalData: if the verification of the accessory's data fails
    """

    srp_client = SrpClient("Pair-Setup", pin)
    srp_client.set_salt(salt)
    srp_client.set_server_public_key(server_public_key)
    client_pub_key = srp_client.get_public_key()
    client_proof = srp_client.get_proof()

    response_tlv = [
        (TLV.kTLVType_State, TLV.M3),
        (TLV.kTLVType_PublicKey, SrpClient.to_byte_array(client_pub_key)),
        (TLV.kTLVType_Proof, SrpClient.to_byte_array(client_proof)),
    ]

    step4_expectations = [
        TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_Proof
    ]
    response_tlv = yield (response_tlv, step4_expectations)

    #
    # Step #5 ios --> accessory (Exchange Request) (see page 43)
    #
    logging.debug("#5 ios -> accessory: send SRP exchange request")

    # M4 Verification (page 43)
    response_tlv = TLV.reorder(response_tlv, step4_expectations)
    assert (response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][1]
            == TLV.M4), "perform_pair_setup: State not M4"
    if response_tlv[1][0] == TLV.kTLVType_Error:
        error_handler(response_tlv[1][1], "step 5")

    assert response_tlv[1][
        0] == TLV.kTLVType_Proof, "perform_pair_setup: Not a proof"
    if not srp_client.verify_servers_proof(response_tlv[1][1]):
        raise AuthenticationError("Step #5: wrong proof!")

    # M5 Request generation (page 44)
    session_key = srp_client.get_session_key()

    ios_device_ltsk = ed25519.Ed25519PrivateKey.generate()
    ios_device_ltpk = ios_device_ltsk.public_key()
    ios_device_public_bytes = ios_device_ltpk.public_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PublicFormat.Raw)

    # reversed:
    #   Pair-Setup-Encrypt-Salt instead of Pair-Setup-Controller-Sign-Salt
    #   Pair-Setup-Encrypt-Info instead of Pair-Setup-Controller-Sign-Info
    ios_device_x = hkdf_derive(
        SrpClient.to_byte_array(session_key),
        "Pair-Setup-Controller-Sign-Salt",
        "Pair-Setup-Controller-Sign-Info",
    )

    session_key = hkdf_derive(
        SrpClient.to_byte_array(session_key),
        "Pair-Setup-Encrypt-Salt",
        "Pair-Setup-Encrypt-Info",
    )

    ios_device_pairing_id = ios_pairing_id.encode()
    ios_device_info = ios_device_x + ios_device_pairing_id + ios_device_public_bytes

    ios_device_signature = ios_device_ltsk.sign(ios_device_info)

    sub_tlv = [
        (TLV.kTLVType_Identifier, ios_device_pairing_id),
        (TLV.kTLVType_PublicKey, ios_device_public_bytes),
        (TLV.kTLVType_Signature, ios_device_signature),
    ]
    sub_tlv_b = TLV.encode_list(sub_tlv)

    # taking tge iOSDeviceX as key was reversed from
    # https://github.com/KhaosT/HAP-NodeJS/blob/2ea9d761d9bd7593dd1949fec621ab085af5e567/lib/HAPServer.js
    # function handlePairStepFive calling encryption.encryptAndSeal
    encrypted_data_with_auth_tag = chacha20_aead_encrypt(
        bytes(), session_key, "PS-Msg05".encode(), bytes([0, 0, 0, 0]),
        sub_tlv_b)

    response_tlv = [
        (TLV.kTLVType_State, TLV.M5),
        (TLV.kTLVType_EncryptedData, encrypted_data_with_auth_tag),
    ]

    step6_expectations = [
        TLV.kTLVType_State,
        TLV.kTLVType_Error,
        TLV.kTLVType_EncryptedData,
    ]
    response_tlv = yield (response_tlv, step6_expectations)

    #
    # Step #7 ios (Verification) (page 47)
    #
    response_tlv = TLV.reorder(response_tlv, step6_expectations)
    assert (response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][1]
            == TLV.M6), "perform_pair_setup: State not M6"
    if response_tlv[1][0] == TLV.kTLVType_Error:
        error_handler(response_tlv[1][1], "step 7")

    assert (response_tlv[1][0] == TLV.kTLVType_EncryptedData
            ), "perform_pair_setup: No encrypted data"
    decrypted_data = chacha20_aead_decrypt(
        bytes(),
        session_key,
        "PS-Msg06".encode(),
        bytes([0, 0, 0, 0]),
        response_tlv[1][1],
    )
    if decrypted_data is False:
        raise IllegalData("step 7")

    response_tlv = TLV.decode_bytearray(decrypted_data)
    response_tlv = TLV.reorder(
        response_tlv,
        [
            TLV.kTLVType_Identifier, TLV.kTLVType_PublicKey,
            TLV.kTLVType_Signature
        ],
    )

    assert (response_tlv[2][0] == TLV.kTLVType_Signature
            ), "perform_pair_setup: No signature"
    accessory_sig = response_tlv[2][1]

    assert (response_tlv[0][0] == TLV.kTLVType_Identifier
            ), "perform_pair_setup: No identifier"
    accessory_pairing_id = response_tlv[0][1]

    assert (response_tlv[1][0] == TLV.kTLVType_PublicKey
            ), "perform_pair_setup: No public key"
    accessory_ltpk = response_tlv[1][1]

    accessory_x = hkdf_derive(
        SrpClient.to_byte_array(srp_client.get_session_key()),
        "Pair-Setup-Accessory-Sign-Salt",
        "Pair-Setup-Accessory-Sign-Info",
    )

    accessory_info = accessory_x + accessory_pairing_id + accessory_ltpk

    e25519s = ed25519.Ed25519PublicKey.from_public_bytes(
        bytes(response_tlv[1][1]))
    try:
        e25519s.verify(bytes(accessory_sig), bytes(accessory_info))
    except cryptography_exceptions.InvalidSignature:
        raise InvalidSignatureError("step #7")

    ios_device_ltsk_private_bytes = ios_device_ltsk.private_bytes(
        encoding=serialization.Encoding.Raw,
        format=serialization.PrivateFormat.Raw,
        encryption_algorithm=serialization.NoEncryption(),
    )

    return {
        "AccessoryPairingID": response_tlv[0][1].decode(),
        "AccessoryLTPK": hexlify(response_tlv[1][1]).decode(),
        "iOSPairingId": ios_pairing_id,
        "iOSDeviceLTSK": ios_device_ltsk_private_bytes.hex(),
        "iOSDeviceLTPK": ios_device_public_bytes.hex(),
    }