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
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")
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"
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)
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(), }