Beispiel #1
0
    async def send_and_receive(
        self,
        method: str,
        uri: str,
        protocol: str = "HTTP/1.1",
        user_agent: str = USER_AGENT,
        content_type: Optional[str] = None,
        headers: Optional[Mapping[str, object]] = None,
        body: Optional[Union[str, bytes]] = None,
        allow_error: bool = False,
    ) -> HttpResponse:
        """Send a HTTP message and return response."""
        output = _format_message(method, uri, protocol, user_agent,
                                 content_type, headers, body)

        _LOGGER.debug("Sending %s message: %s", protocol, output)
        if not self.transport:
            raise RuntimeError("not connected to remote")

        self.transport.write(self.send_processor(output))

        event = asyncio.Event()
        self._requests.appendleft(event)
        try:
            await asyncio.wait_for(event.wait(), timeout=4)
            response = cast(HttpResponse, self._responses.get())
        except asyncio.TimeoutError as ex:
            raise TimeoutError(
                f"no response to {method} {uri} ({protocol})") from ex
        finally:
            # If request failed and event is still in request queue, remove it
            if self._requests and self._requests[-1] == event:
                self._requests.pop()

        _LOGGER.debug("Got %s response: %s:", response.protocol, response)

        if response.code == 403:
            raise exceptions.AuthenticationError("not authenticated")

        # Password required
        if response.code == 401:
            if allow_error:
                return response
            raise exceptions.AuthenticationError("not authenticated")

        # Positive response
        if 200 <= response.code < 300 or allow_error:
            return response

        raise exceptions.HttpError(
            f"{protocol} method {method} failed with code "
            f"{response.code}: {response.message}",
            response.code,
        )
Beispiel #2
0
def _get_pairing_data(message: Dict[str, object]):
    pairing_data = message.get(PAIRING_DATA_KEY)
    if not pairing_data:
        raise exceptions.AuthenticationError("no pairing data in message")

    if not isinstance(pairing_data, bytes):
        raise exceptions.ProtocolError(
            f"Pairing data has unexpected type: {type(pairing_data)}")

    tlv = read_tlv(pairing_data)
    if TlvValue.Error in tlv:
        raise exceptions.AuthenticationError(stringify(tlv))

    return tlv
Beispiel #3
0
    async def _do(self, action, retry=True, is_login=False, is_daap=True):
        resp, status = await action()
        if is_daap:
            resp = parser.parse(resp, lookup_tag)

        self._log_response(action.log_text, resp, is_daap)
        if 200 <= status < 300:
            return resp

        # Seems to be the case?
        if status == 500:
            raise exceptions.NotSupportedError(
                "command not supported at this stage")

        if not is_login:
            # If a request fails, try to login again before retrying
            _LOGGER.info("implicitly logged out, logging in again")
            await self.login()

        # Retry once if we got a bad response, otherwise bail out
        if retry:
            return await self._do(action,
                                  False,
                                  is_login=is_login,
                                  is_daap=is_daap)

        raise exceptions.AuthenticationError("failed to login: " + str(status))
Beispiel #4
0
    def step4(self, encrypted_data):
        """Last pairing step."""
        chacha = chacha20.Chacha20Cipher(self._session_key, self._session_key)
        decrypted_tlv_bytes = chacha.decrypt(
            encrypted_data, nounce='PS-Msg06'.encode())

        if not decrypted_tlv_bytes:
            raise exceptions.AuthenticationError('data decrypt failed')

        decrypted_tlv = tlv8.read_tlv(decrypted_tlv_bytes)
        _LOGGER.debug('PS-Msg06: %s', decrypted_tlv)

        atv_identifier = decrypted_tlv[tlv8.TLV_IDENTIFIER]
        atv_signature = decrypted_tlv[tlv8.TLV_SIGNATURE]
        atv_pub_key = decrypted_tlv[tlv8.TLV_PUBLIC_KEY]
        log_binary(_LOGGER,
                   'Device',
                   Identifier=atv_identifier,
                   Signature=atv_signature,
                   Public=atv_pub_key)

        # TODO: verify signature here

        return Credentials(atv_pub_key, self._signing_key.to_seed(),
                           atv_identifier, self.pairing_id)
Beispiel #5
0
def _get_pairing_data(resp):
    pairing_message = CryptoPairingMessage.cryptoPairingMessage
    tlv = tlv8.read_tlv(resp.Extensions[pairing_message].pairingData)
    if tlv8.TLV_ERROR in tlv:
        error = int.from_bytes(tlv[tlv8.TLV_ERROR], byteorder="little")
        raise exceptions.AuthenticationError("got error: " + str(error))
    return tlv
Beispiel #6
0
    def step4(self, encrypted_data):
        """Last pairing step."""
        chacha = chacha20.Chacha20Cipher(self._session_key, self._session_key)
        decrypted_tlv_bytes = chacha.decrypt(encrypted_data, nounce="PS-Msg06".encode())

        if not decrypted_tlv_bytes:
            raise exceptions.AuthenticationError("data decrypt failed")

        decrypted_tlv = hap_tlv8.read_tlv(decrypted_tlv_bytes)
        _LOGGER.debug("PS-Msg06: %s", decrypted_tlv)

        atv_identifier = decrypted_tlv[TlvValue.Identifier]
        atv_signature = decrypted_tlv[TlvValue.Signature]
        atv_pub_key = decrypted_tlv[TlvValue.PublicKey]
        log_binary(
            _LOGGER,
            "Device",
            Identifier=atv_identifier,
            Signature=atv_signature,
            Public=atv_pub_key,
        )

        # TODO: verify signature here

        return HapCredentials(
            atv_pub_key, self._auth_private, atv_identifier, self.pairing_id
        )
Beispiel #7
0
    def verify1(self, credentials, session_pub_key, encrypted):
        """First verification step."""
        self._shared = self._verify_private.exchange(
            X25519PublicKey.from_public_bytes(session_pub_key)
        )

        session_key = hkdf_expand(
            "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", self._shared
        )

        chacha = chacha20.Chacha20Cipher(session_key, session_key)
        decrypted_tlv = hap_tlv8.read_tlv(
            chacha.decrypt(encrypted, nounce="PV-Msg02".encode())
        )

        identifier = decrypted_tlv[TlvValue.Identifier]
        signature = decrypted_tlv[TlvValue.Signature]

        if identifier != credentials.atv_id:
            raise exceptions.AuthenticationError("incorrect device response")

        info = session_pub_key + bytes(identifier) + self._public_bytes
        ltpk = Ed25519PublicKey.from_public_bytes(bytes(credentials.ltpk))

        try:
            ltpk.verify(bytes(signature), bytes(info))
        except InvalidSignature as ex:
            raise exceptions.AuthenticationError("signature error") from ex

        device_info = self._public_bytes + credentials.client_id + session_pub_key

        device_signature = Ed25519PrivateKey.from_private_bytes(credentials.ltsk).sign(
            device_info
        )

        tlv = hap_tlv8.write_tlv(
            {
                TlvValue.Identifier: credentials.client_id,
                TlvValue.Signature: device_signature,
            }
        )

        return chacha.encrypt(tlv, nounce="PV-Msg03".encode())
Beispiel #8
0
    async def _setup_encryption(self):
        if self.service.credentials:
            credentials = HapCredentials.parse(self.service.credentials)
            pair_verifier = CompanionPairingVerifier(self, self.srp,
                                                     credentials)

            try:
                await pair_verifier.verify_credentials()
                output_key, input_key = pair_verifier.encryption_keys()
                self.connection.enable_encryption(output_key, input_key)
            except Exception as ex:
                raise exceptions.AuthenticationError(str(ex)) from ex
Beispiel #9
0
    async def _setup_encryption(self):
        if self.service.credentials:
            credentials = parse_credentials(self.service.credentials)
            pair_verifier = CompanionPairVerifyProcedure(
                self, self.srp, credentials)

            try:
                await pair_verifier.verify_credentials()
                output_key, input_key = pair_verifier.encryption_keys(
                    SRP_SALT, SRP_OUTPUT_INFO, SRP_INPUT_INFO)
                self.connection.enable_encryption(output_key, input_key)
            except Exception as ex:
                raise exceptions.AuthenticationError(str(ex)) from ex
Beispiel #10
0
    def decrypt(self, data, nounce=None):
        """Decrypt data with counter or specified nounce."""
        if nounce is None:
            nounce = self._in_counter.to_bytes(length=8, byteorder='little')
            self._in_counter += 1

        decrypted = self._enc_in.open(
            b'\x00\x00\x00\x00' + nounce, data, bytes())

        if not decrypted:
            raise exceptions.AuthenticationError('data decrypt failed')

        return bytes(decrypted)
Beispiel #11
0
    def step2(self, atv_pub_key, atv_salt):
        """Second pairing step."""
        pk_str = binascii.hexlify(atv_pub_key).decode()
        salt = binascii.hexlify(atv_salt).decode()
        self._session.process(pk_str, salt)

        if not self._session.verify_proof(self._session.key_proof_hash):
            raise exceptions.AuthenticationError("proofs do not match")

        pub_key = binascii.unhexlify(self._session.public)
        proof = binascii.unhexlify(self._session.key_proof)
        log_binary(_LOGGER, "Client", Public=pub_key, Proof=proof)
        return pub_key, proof
Beispiel #12
0
    def decrypt(self, data, nounce=None):
        """Decrypt data with counter or specified nounce."""
        if nounce is None:
            nounce = self._in_counter.to_bytes(length=8, byteorder="little")
            self._in_counter += 1

        decrypted = self._enc_in.decrypt(b"\x00\x00\x00\x00" + nounce, data,
                                         None)

        if not decrypted:
            raise exceptions.AuthenticationError("data decrypt failed")

        return bytes(decrypted)
Beispiel #13
0
    def verify1(self, credentials, session_pub_key, encrypted):
        """First verification step."""
        # No additional hashing used
        self._shared = self._verify_private.get_shared_key(
            curve25519.Public(session_pub_key), hashfunc=lambda x: x)

        session_key = hkdf_expand('Pair-Verify-Encrypt-Salt',
                                  'Pair-Verify-Encrypt-Info',
                                  self._shared)

        chacha = chacha20.Chacha20Cipher(session_key, session_key)
        decrypted_tlv = tlv8.read_tlv(
            chacha.decrypt(encrypted, nounce='PV-Msg02'.encode()))

        identifier = decrypted_tlv[tlv8.TLV_IDENTIFIER]
        signature = decrypted_tlv[tlv8.TLV_SIGNATURE]

        if identifier != credentials.atv_id:
            raise exceptions.AuthenticationError('incorrect device response')

        info = session_pub_key + \
            bytes(identifier) + self._verify_public.serialize()
        ltpk = VerifyingKey(bytes(credentials.ltpk))

        try:
            ltpk.verify(bytes(signature), bytes(info))
        except (BadPrefixError, BadSignatureError) as ex:
            raise exceptions.AuthenticationError('signature error') from ex

        device_info = self._verify_public.serialize() + \
            credentials.client_id + session_pub_key

        device_signature = SigningKey(credentials.ltsk).sign(device_info)

        tlv = tlv8.write_tlv({tlv8.TLV_IDENTIFIER: credentials.client_id,
                              tlv8.TLV_SIGNATURE: device_signature})

        return chacha.encrypt(tlv, nounce='PV-Msg03'.encode())
Beispiel #14
0
    async def _enable_encryption(self):
        # Encryption can be enabled whenever credentials are available but only
        # after DEVICE_INFORMATION has been sent
        if self.service.credentials:
            # Verify credentials and generate keys
            credentials = HapCredentials.parse(self.service.credentials)
            pair_verifier = MrpPairingVerifier(self, self.srp, credentials)

            try:
                await pair_verifier.verify_credentials()
                output_key, input_key = pair_verifier.encryption_keys()
                self.connection.enable_encryption(output_key, input_key)
            except Exception as ex:
                raise exceptions.AuthenticationError(str(ex)) from ex
Beispiel #15
0
    def _do(self, action, url, retry=True, is_login=False, is_daap=True):
        resp, status = yield from action(url)
        self._log_response(str(action.__name__) + ': %s', resp, is_daap)
        if status >= 200 and status < 300:
            return resp

        # Retry once if we got a bad response, otherwise bail out
        if retry:
            return (yield from self._do(action,
                                        url,
                                        False,
                                        is_login=is_login,
                                        is_daap=is_daap))
        else:
            raise exceptions.AuthenticationError('failed to login: ' +
                                                 str(status))
Beispiel #16
0
    async def _enable_encryption(self) -> None:
        # Encryption can be enabled whenever credentials are available but only
        # after DEVICE_INFORMATION has been sent
        if self.service.credentials is None:
            return

        # Verify credentials and generate keys
        credentials = parse_credentials(self.service.credentials)
        pair_verifier = MrpPairVerifyProcedure(self, self.srp, credentials)

        try:
            await pair_verifier.verify_credentials()
            output_key, input_key = pair_verifier.encryption_keys(
                SRP_SALT, SRP_OUTPUT_INFO, SRP_INPUT_INFO)
            self.connection.enable_encryption(output_key, input_key)
        except Exception as ex:
            raise exceptions.AuthenticationError(str(ex)) from ex
Beispiel #17
0
    def step2(self, atv_pub_key, atv_salt):
        """Second pairing step."""
        pk_str = binascii.hexlify(atv_pub_key).decode()
        print("B is: ", pk_str)

        salt = binascii.hexlify(atv_salt).decode()
        print("Salt is: ", salt)

        self._client_session_key, _, _ = self._session.process(pk_str, salt)
        if not self._session.verify_proof(self._session.key_proof_hash):
            raise exceptions.AuthenticationError('proofs do not match (mitm?)')

        pub_key = binascii.unhexlify(self._session.public)
        print("A is: ", pub_key)

        proof = binascii.unhexlify(self._session.key_proof)
        print("Proof is: ", proof)

        log_binary(_LOGGER, 'Client', Public=pub_key, Proof=proof)
        return pub_key, proof
Beispiel #18
0
    async def _do(self, action, retry=True, is_login=False, is_daap=True):
        resp, status = await action()
        if is_daap:
            resp = parser.parse(resp, lookup_tag)

        self._log_response(str(action.__name__) + ': %s', resp, is_daap)
        if 200 <= status < 300:
            return resp

        if not is_login:
            # If a request fails, try to login again before retrying
            _LOGGER.info('implicitly logged out, logging in again')
            await self.login()

        # Retry once if we got a bad response, otherwise bail out
        if retry:
            return (await self._do(
                action, False, is_login=is_login, is_daap=is_daap))

        raise exceptions.AuthenticationError(
            'failed to login: ' + str(status))
Beispiel #19
0
    def _do(self, action, retry=True, is_login=False, is_daap=True):
        resp, status = yield from action()
        self._log_response(str(action.__name__) + ': %s', resp, is_daap)
        if status >= 200 and status < 300:
            return resp

        # When a 403 is received we are likely logged out, so a new
        # login must be performed to get a new session id
        if status == 403:
            _LOGGER.info('implicitly logged out, logging in again')
            yield from self.login()

        # Retry once if we got a bad response, otherwise bail out
        if retry:
            return (yield from self._do(action,
                                        False,
                                        is_login=is_login,
                                        is_daap=is_daap))
        else:
            raise exceptions.AuthenticationError('failed to login: ' +
                                                 str(status))
Beispiel #20
0
    async def play_url(self, url: str, position: float = 0) -> None:
        """Play media from an URL on the device."""
        body = {
            "Content-Location": url,
            "Start-Position": position,
            "X-Apple-Session-ID": str(uuid4()),
        }

        retry = 0
        while retry < PLAY_RETRIES:
            _LOGGER.debug("Starting to play %s", url)

            # pylint: disable=no-member
            resp = await self.http.post(
                "/play",
                headers=HEADERS,
                body=plistlib.dumps(body, fmt=plistlib.FMT_BINARY),
                allow_error=True,
            )

            # Sometimes AirPlay fails with "Internal Server Error", we
            # apply a "lets try again"-approach to that
            if resp.code == 500:
                retry += 1
                _LOGGER.debug("Failed to stream %s, retry %d of %d", url,
                              retry, PLAY_RETRIES)
                await asyncio.sleep(1.0)
                continue

            # TODO: Should be more fine-grained
            if 400 <= resp.code < 600:
                raise exceptions.AuthenticationError(
                    f"status code: {resp.code}")

            await self._wait_for_media_to_end()
            return

        raise exceptions.PlaybackError("Max retries exceeded")
Beispiel #21
0
    async def play_url(self, url, position=0):
        """Play media from an URL on the device."""
        body = {
            'Content-Location': url,
            'Start-Position': position,
            'X-Apple-Session-ID': str(uuid4()),
        }

        retry = 0
        while retry < PLAY_RETRIES:
            # pylint: disable=no-member
            _, status = await self.http.post_data('play',
                                                  headers=HEADERS,
                                                  data=plistlib.dumps(
                                                      body,
                                                      fmt=plistlib.FMT_BINARY),
                                                  timeout=TIMEOUT)

            # Sometimes AirPlay fails with "Internal Server Error", we
            # apply a "lets try again"-approach to that
            if status == 500:
                retry += 1
                _LOGGER.debug('Failed to stream %s, retry %d of %d', url,
                              retry, PLAY_RETRIES)
                await asyncio.sleep(1.0, loop=self.loop)
                continue

            # TODO: Should be more fine-grained
            if 400 <= status < 600:
                raise exceptions.AuthenticationError('Status code: ' +
                                                     str(status))

            await self._wait_for_media_to_end()
            return

        raise exceptions.PlaybackError('Max retries exceeded')
Beispiel #22
0
def _get_pairing_data(resp):
    tlv = read_tlv(resp.inner().pairingData)
    if TlvValue.Error in tlv:
        raise exceptions.AuthenticationError(stringify(tlv))
    return tlv