Beispiel #1
0
class MrpAppleTVProxy(MrpServerAuth, asyncio.Protocol):
    """Implementation of a fake MRP Apple TV."""

    def __init__(self, loop):
        """Initialize a new instance of ProxyMrpAppleTV."""
        super().__init__(self, DEVICE_NAME)
        self.loop = loop
        self.buffer = b""
        self.transport = None
        self.chacha = None
        self.connection = None

    async def start(self, address, port, credentials):
        """Start the proxy instance."""
        self.connection = MrpConnection(address, port, self.loop)
        protocol = MrpProtocol(
            self.connection,
            SRPAuthHandler(),
            MrpService(None, port, credentials=credentials),
        )
        await protocol.start(skip_initial_messages=True)
        self.connection.listener = self
        self._process_buffer()

    def connection_made(self, transport):
        """Client did connect to proxy."""
        self.transport = transport

    def enable_encryption(self, input_key, output_key):
        """Enable encryption with specified keys."""
        self.chacha = chacha20.Chacha20Cipher(input_key, output_key)

    def send(self, message):
        """Send protobuf message to client."""
        data = message.SerializeToString()
        _LOGGER.info("<<(DECRYPTED): %s", message)
        if self.chacha:
            data = self.chacha.encrypt(data)
            log_binary(_LOGGER, "<<(ENCRYPTED)", Message=message)

        length = variant.write_variant(len(data))
        self.transport.write(length + data)

    def send_raw(self, raw):
        """Send raw data to client."""
        parsed = protobuf.ProtocolMessage()
        parsed.ParseFromString(raw)

        log_binary(_LOGGER, "ATV->APP", Raw=raw)
        _LOGGER.info("ATV->APP Parsed: %s", parsed)
        if self.chacha:
            raw = self.chacha.encrypt(raw)
            log_binary(_LOGGER, "ATV->APP", Encrypted=raw)

        length = variant.write_variant(len(raw))
        try:
            self.transport.write(length + raw)
        except Exception:  # pylint: disable=broad-except
            _LOGGER.exception("Failed to send to app")

    def message_received(self, _, raw):
        """Message received from ATV."""
        self.send_raw(raw)

    def data_received(self, data):
        """Message received from iOS app/client."""
        self.buffer += data
        if self.connection.connected:
            self._process_buffer()

    def _process_buffer(self):
        while self.buffer:
            length, raw = variant.read_variant(self.buffer)
            if len(raw) < length:
                break

            data = raw[:length]
            self.buffer = raw[length:]
            if self.chacha:
                log_binary(_LOGGER, "ENC Phone->ATV", Encrypted=data)
                data = self.chacha.decrypt(data)

            message = protobuf.ProtocolMessage()
            message.ParseFromString(data)
            _LOGGER.info("(DEC Phone->ATV): %s", message)

            try:
                if message.type == protobuf.DEVICE_INFO_MESSAGE:
                    self.handle_device_info(message, message.inner())
                elif message.type == protobuf.CRYPTO_PAIRING_MESSAGE:
                    self.handle_crypto_pairing(message, message.inner())
                else:
                    self.connection.send_raw(data)
            except Exception:  # pylint: disable=broad-except
                _LOGGER.exception("Error while dispatching message")
Beispiel #2
0
class ProxyMrpAppleTV(asyncio.Protocol):  # pylint: disable=too-many-instance-attributes  # noqa
    """Implementation of a fake MRP Apple TV."""
    def __init__(self, loop, credentials, atv_device_id):
        """Initialize a new instance of ProxyMrpAppleTV."""
        self.loop = loop
        self.credentials = credentials
        self.atv_device_id = atv_device_id
        self.server = None
        self.buffer = b''
        self.has_paired = False
        self.transport = None
        self.chacha = None
        self.connection = None
        self.input_key = None
        self.output_key = None
        self.mapping = {
            protobuf.DEVICE_INFO_MESSAGE: self.handle_device_info,
            protobuf.CRYPTO_PAIRING_MESSAGE: self.handle_crypto_pairing,
        }

        self._shared = None
        self._session_key = None
        self._signing_key = SigningKey(32 * b'\x01')
        self._auth_private = self._signing_key.to_seed()
        self._auth_public = self._signing_key.get_verifying_key().to_bytes()
        self._verify_private = curve25519.Private(secret=32 * b'\x01')
        self._verify_public = self._verify_private.get_public()

        self.context = SRPContext('Pair-Setup',
                                  str(1111),
                                  prime=constants.PRIME_3072,
                                  generator=constants.PRIME_3072_GEN,
                                  hash_func=hashlib.sha512,
                                  bits_salt=128)
        self.username, self.verifier, self.salt = \
            self.context.get_user_data_triplet()

        context_server = SRPContext('Pair-Setup',
                                    prime=constants.PRIME_3072,
                                    generator=constants.PRIME_3072_GEN,
                                    hash_func=hashlib.sha512,
                                    bits_salt=128)

        self._session = SRPServerSession(
            context_server, self.verifier,
            binascii.hexlify(self._auth_private).decode())

    def start(self, address, port):
        """Start the proxy instance."""
        # Establish connection to ATV
        self.connection = MrpConnection(address, port, self.loop)
        protocol = MrpProtocol(
            self.loop, self.connection, SRPAuthHandler(),
            MrpService(None, port, credentials=self.credentials))
        self.loop.run_until_complete(
            protocol.start(skip_initial_messages=True))
        self.connection.listener = self

        # Setup server used to publish a fake MRP server
        coro = self.loop.create_server(lambda: self, '0.0.0.0')
        self.server = self.loop.run_until_complete(coro)
        _LOGGER.info('Started MRP server at port %d', self.port)

    @property
    def port(self):
        """Port used by MRP proxy server."""
        return self.server.sockets[0].getsockname()[1]

    def connection_made(self, transport):
        """Client did connect to proxy."""
        self.transport = transport

    def _send(self, message):
        data = message.SerializeToString()
        _LOGGER.info('<<(DECRYPTED): %s', message)
        if self.chacha:
            data = self.chacha.encrypt(data)
            log_binary(_LOGGER, '<<(ENCRYPTED)', Message=message)

        length = variant.write_variant(len(data))
        self.transport.write(length + data)

    def _send_raw(self, raw):
        parsed = protobuf.ProtocolMessage()
        parsed.ParseFromString(raw)

        log_binary(_LOGGER, 'ATV->APP', Raw=raw)
        _LOGGER.info('ATV->APP Parsed: %s', parsed)
        if self.chacha:
            raw = self.chacha.encrypt(raw)
            log_binary(_LOGGER, 'ATV->APP', Encrypted=raw)

        length = variant.write_variant(len(raw))
        try:
            self.transport.write(length + raw)
        except Exception:  # pylint: disable=broad-except
            _LOGGER.exception('Failed to send to app')

    def message_received(self, _, raw):
        """Message received from ATV."""
        self._send_raw(raw)

    def data_received(self, data):
        """Message received from iOS app/client."""
        self.buffer += data

        while self.buffer:
            length, raw = variant.read_variant(self.buffer)
            if len(raw) < length:
                break

            data = raw[:length]
            self.buffer = raw[length:]
            if self.chacha:
                log_binary(_LOGGER, 'ENC Phone->ATV', Encrypted=data)
                data = self.chacha.decrypt(data)

            parsed = protobuf.ProtocolMessage()
            parsed.ParseFromString(data)
            _LOGGER.info('(DEC Phone->ATV): %s', parsed)

            try:

                def unhandled_message(_, raw):
                    self.connection.send_raw(raw)

                self.mapping.get(parsed.type, unhandled_message)(parsed, data)
            except Exception:  # pylint: disable=broad-except
                _LOGGER.exception('Error while dispatching message')

    def handle_device_info(self, message, _):
        """Handle received device information message."""
        _LOGGER.debug('Received device info message')

        # TODO: Consolidate this better with message.device_information(...)
        resp = messages.create(protobuf.DEVICE_INFO_MESSAGE)
        resp.identifier = message.identifier
        resp.inner().uniqueIdentifier = self.atv_device_id.decode()
        resp.inner().name = 'ATVProxy'
        resp.inner().systemBuildVersion = '15K600'
        resp.inner().applicationBundleIdentifier = 'com.apple.mediaremoted'
        resp.inner().protocolVersion = 1
        resp.inner().lastSupportedMessageType = 58
        resp.inner().supportsSystemPairing = True
        resp.inner().allowsPairing = True
        resp.inner().systemMediaApplication = "com.apple.TVMusic"
        resp.inner().supportsACL = True
        resp.inner().supportsSharedQueue = True
        resp.inner().supportsExtendedMotion = True
        resp.inner().sharedQueueVersion = 2
        self._send(resp)

    def handle_crypto_pairing(self, message, _):
        """Handle incoming crypto pairing message."""
        _LOGGER.debug('Received crypto pairing message')
        pairing_data = tlv8.read_tlv(message.inner().pairingData)
        seqno = pairing_data[tlv8.TLV_SEQ_NO][0]
        getattr(self, "_seqno_" + str(seqno))(pairing_data)

    def _seqno_1(self, pairing_data):
        if self.has_paired:
            server_pub_key = self._verify_public.serialize()
            client_pub_key = pairing_data[tlv8.TLV_PUBLIC_KEY]

            self._shared = self._verify_private.get_shared_key(
                curve25519.Public(client_pub_key), hashfunc=lambda x: x)

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

            info = server_pub_key + self.atv_device_id + client_pub_key
            signature = SigningKey(self._signing_key.to_seed()).sign(info)

            tlv = tlv8.write_tlv({
                tlv8.TLV_IDENTIFIER: self.atv_device_id,
                tlv8.TLV_SIGNATURE: signature
            })

            chacha = chacha20.Chacha20Cipher(session_key, session_key)
            encrypted = chacha.encrypt(tlv, nounce='PV-Msg02'.encode())

            msg = messages.crypto_pairing({
                tlv8.TLV_SEQ_NO: b'\x02',
                tlv8.TLV_PUBLIC_KEY: server_pub_key,
                tlv8.TLV_ENCRYPTED_DATA: encrypted
            })

            self.output_key = hkdf_expand('MediaRemote-Salt',
                                          'MediaRemote-Write-Encryption-Key',
                                          self._shared)

            self.input_key = hkdf_expand('MediaRemote-Salt',
                                         'MediaRemote-Read-Encryption-Key',
                                         self._shared)

            log_binary(_LOGGER,
                       'Keys',
                       Output=self.output_key,
                       Input=self.input_key)

        else:
            msg = messages.crypto_pairing({
                tlv8.TLV_SALT:
                binascii.unhexlify(self.salt),
                tlv8.TLV_PUBLIC_KEY:
                binascii.unhexlify(self._session.public),
                tlv8.TLV_SEQ_NO:
                b'\x02'
            })

        self._send(msg)

    def _seqno_3(self, pairing_data):
        if self.has_paired:
            self._send(messages.crypto_pairing({tlv8.TLV_SEQ_NO: b'\x04'}))
            self.chacha = chacha20.Chacha20Cipher(self.input_key,
                                                  self.output_key)
        else:
            pubkey = binascii.hexlify(
                pairing_data[tlv8.TLV_PUBLIC_KEY]).decode()
            self._session.process(pubkey, self.salt)

            proof = binascii.unhexlify(self._session.key_proof_hash)
            assert self._session.verify_proof(
                binascii.hexlify(pairing_data[tlv8.TLV_PROOF]))

            msg = messages.crypto_pairing({
                tlv8.TLV_PROOF: proof,
                tlv8.TLV_SEQ_NO: b'\x04'
            })
            self._send(msg)

    def _seqno_5(self, _):
        self._session_key = hkdf_expand('Pair-Setup-Encrypt-Salt',
                                        'Pair-Setup-Encrypt-Info',
                                        binascii.unhexlify(self._session.key))

        acc_device_x = hkdf_expand('Pair-Setup-Accessory-Sign-Salt',
                                   'Pair-Setup-Accessory-Sign-Info',
                                   binascii.unhexlify(self._session.key))

        device_info = acc_device_x + self.atv_device_id + self._auth_public
        signature = self._signing_key.sign(device_info)

        tlv = tlv8.write_tlv({
            tlv8.TLV_IDENTIFIER: self.atv_device_id,
            tlv8.TLV_PUBLIC_KEY: self._auth_public,
            tlv8.TLV_SIGNATURE: signature
        })

        chacha = chacha20.Chacha20Cipher(self._session_key, self._session_key)
        encrypted = chacha.encrypt(tlv, nounce='PS-Msg06'.encode())

        msg = messages.crypto_pairing({
            tlv8.TLV_SEQ_NO: b'\x06',
            tlv8.TLV_ENCRYPTED_DATA: encrypted,
        })
        self.has_paired = True

        self._send(msg)