Ejemplo n.º 1
0
    def verify2(self, atv_public_key, data):
        """
        Last device verification step.
        """
        # Generate a shared secret key
        public = curve25519.Public(atv_public_key)
        shared = self._verify_private.get_shared_key(
            public, hashfunc=lambda x: x)  # No additional hashing used

        # Derive new AES key and IV from shared key
        aes_key = hash_sha512('Pair-Verify-AES-Key', shared)[0:16]
        aes_iv = hash_sha512('Pair-Verify-AES-IV', shared)[0:16]

        # Sign public keys and encrypt with AES
        signer = SigningKey(self._auth_private)
        signed = signer.sign(self._verify_public.serialize() + atv_public_key)
        signature, _ = aes_encrypt(modes.CTR, aes_key, aes_iv, data, signed)

        # Signature is prepended with 0x00000000 (alignment?)
        return b'\x00\x00\x00\x00' + signature
Ejemplo n.º 2
0
class SRPAuthHandler:
    """Handle SRP crypto routines for auth and key derivation."""

    def __init__(self):
        """Initialize a new SRPAuthHandler."""
        self.pairing_id = str(uuid.uuid4()).encode()
        self._signing_key = None
        self._auth_private = None
        self._auth_public = None
        self._verify_private = None
        self._verify_public = None
        self._session = None
        self._shared = None
        self._session_key = None
        self._client_session_key = None  # TODO: can remove?

    def initialize(self):
        """Initialize operation by generating new keys."""
        self._signing_key = SigningKey(os.urandom(32))
        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=os.urandom(32))
        self._verify_public = self._verify_private.get_public()
        return self._auth_public, self._verify_public.serialize()

    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 Exception('incorrect device response')  # TODO: new exception

        info = session_pub_key + \
            bytes(identifier) + self._verify_public.serialize()
        ltpk = VerifyingKey(bytes(credentials.ltpk))
        ltpk.verify(bytes(signature), bytes(info))  # throws if no match

        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())

    def verify2(self):
        """Last verification step.

        The derived keys (output, input) are returned here.
        """
        output_key = hkdf_expand('MediaRemote-Salt',
                                 'MediaRemote-Write-Encryption-Key',
                                 self._shared)

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

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

    def step1(self, pin):
        """First pairing step."""
        context = SRPContext(
            'Pair-Setup', str(pin),
            prime=constants.PRIME_3072,
            generator=constants.PRIME_3072_GEN,
            hash_func=hashlib.sha512)
        self._session = SRPClientSession(
            context, binascii.hexlify(self._auth_private).decode())

    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._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)
        proof = binascii.unhexlify(self._session.key_proof)
        log_binary(_LOGGER, 'Client', Public=pub_key, Proof=proof)
        return pub_key, proof

    def step3(self):
        """Third pairing step."""
        ios_device_x = hkdf_expand(
            'Pair-Setup-Controller-Sign-Salt',
            'Pair-Setup-Controller-Sign-Info',
            binascii.unhexlify(self._client_session_key))

        self._session_key = hkdf_expand(
            'Pair-Setup-Encrypt-Salt',
            'Pair-Setup-Encrypt-Info',
            binascii.unhexlify(self._client_session_key))

        device_info = ios_device_x + self.pairing_id + self._auth_public
        device_signature = self._signing_key.sign(device_info)

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

        chacha = chacha20.Chacha20Cipher(self._session_key, self._session_key)
        encrypted_data = chacha.encrypt(tlv, nounce='PS-Msg05'.encode())
        log_binary(_LOGGER, 'Data', Encrypted=encrypted_data)
        return encrypted_data

    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 Exception('data decrypt failed')  # TODO: new exception
        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)
Ejemplo n.º 3
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)