class MrpPairingHandler(PairingHandler): """Base class for API used to pair with an Apple TV.""" def __init__(self, config, session_manager: ClientSessionManager, loop): """Initialize a new MrpPairingHandler.""" super().__init__(session_manager, config.get_service(Protocol.MRP)) self.connection = MrpConnection(config.address, self.service.port, loop) self.srp = SRPAuthHandler() self.protocol = MrpProtocol(self.connection, self.srp, self.service) self.pairing_procedure = MrpPairingProcedure(self.protocol, self.srp) self.pin_code = None self._has_paired = False async def close(self): """Call to free allocated resources after pairing.""" self.connection.close() await super().close() @property def has_paired(self): """If a successful pairing has been performed.""" return self._has_paired async def begin(self): """Start pairing process.""" return await error_handler(self.pairing_procedure.start_pairing, exceptions.PairingError) async def finish(self): """Stop pairing process.""" if not self.pin_code: raise exceptions.PairingError("no pin given") credentials = str(await error_handler( self.pairing_procedure.finish_pairing, exceptions.PairingError, self.pin_code, )) _LOGGER.debug("Verifying credentials %s", credentials) verifier = MrpPairingVerifier(self.protocol, self.srp, Credentials.parse(credentials)) await error_handler(verifier.verify_credentials, exceptions.PairingError) self.service.credentials = credentials self._has_paired = True @property def device_provides_pin(self): """Return True if remote device presents PIN code, else False.""" return True def pin(self, pin): """Pin code used for pairing.""" self.pin_code = str(pin).zfill(4) _LOGGER.debug("MRP PIN changed to %s", self.pin_code)
def __init__(self, config, session_manager: ClientSessionManager, loop): """Initialize a new MrpPairingHandler.""" super().__init__(session_manager, config.get_service(Protocol.MRP)) self.connection = MrpConnection(config.address, self.service.port, loop) self.srp = SRPAuthHandler() self.protocol = MrpProtocol(self.connection, self.srp, self.service) self.pairing_procedure = MrpPairingProcedure(self.protocol, self.srp) self.pin_code = None self._has_paired = False
async def start(self, address, port, credentials): """Start the proxy instance.""" self.connection = MrpConnection(address, port, self.loop) protocol = MrpProtocol( self.loop, self.connection, SRPAuthHandler(), MrpService(None, port, credentials=credentials)) await protocol.start(skip_initial_messages=True) self.connection.listener = self self._process_buffer()
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)
def __init__( self, loop: asyncio.AbstractEventLoop, session_manager: ClientSessionManager, config: conf.AppleTV, airplay: Stream, ) -> None: """Initialize a new Apple TV.""" super().__init__() self._session_manager = session_manager self._config = config self._mrp_service = config.get_service(Protocol.MRP) assert self._mrp_service is not None self._connection = MrpConnection(config.address, self._mrp_service.port, loop, atv=self) self._srp = SRPAuthHandler() self._protocol = MrpProtocol(self._connection, self._srp, self._mrp_service) self._psm = PlayerStateManager(self._protocol, loop) self._mrp_remote = MrpRemoteControl(loop, self._psm, self._protocol) self._mrp_metadata = MrpMetadata(self._protocol, self._psm, config.identifier) self._mrp_power = MrpPower(loop, self._protocol, self._mrp_remote) self._mrp_push_updater = MrpPushUpdater(loop, self._mrp_metadata, self._psm) self._mrp_features = MrpFeatures(self._config, self._psm) self._airplay = airplay
async def mrp_protocol(event_loop, mrp_atv): port = mrp_atv.get_port(Protocol.MRP) service = MrpService("mrp_id", port) connection = MrpConnection("127.0.0.1", port, event_loop) protocol = MrpProtocol(connection, SRPAuthHandler(), service) yield protocol protocol.stop()
class MrpPairingHandler(PairingHandler): """Base class for API used to pair with an Apple TV.""" def __init__(self, config, session, loop): """Initialize a new MrpPairingHandler.""" super().__init__(session, config.get_service(Protocol.MRP)) self.connection = MrpConnection(config.address, self.service.port, loop) self.srp = SRPAuthHandler() self.protocol = MrpProtocol(loop, self.connection, self.srp, self.service) self.pairing_procedure = MrpPairingProcedure(self.protocol, self.srp) self.pin_code = None async def close(self): """Call to free allocated resources after pairing.""" self.connection.close() await super().close() @property def has_paired(self): """If a successful pairing has been performed.""" return self.service.credentials is not None def begin(self): """Start pairing process.""" return error_handler(self.pairing_procedure.start_pairing, exceptions.PairingError) async def finish(self): """Stop pairing process.""" if not self.pin_code: raise exceptions.PairingError("no pin given") self.service.credentials = str(await error_handler( self.pairing_procedure.finish_pairing, exceptions.PairingError, self.pin_code, )) @property def device_provides_pin(self): """Return True if remote device presents PIN code, else False.""" return True def pin(self, pin): """Pin code used for pairing.""" self.pin_code = str(pin).zfill(4)
def setup( # pylint: disable=too-many-locals loop: asyncio.AbstractEventLoop, config: conf.AppleTV, interfaces: Dict[Any, Relayer], device_listener: StateProducer, session_manager: ClientSessionManager, ) -> Optional[ Tuple[Callable[[], Awaitable[None]], Callable[[], None], Set[FeatureName]] ]: """Set up a new MRP service.""" service = config.get_service(Protocol.MRP) assert service is not None connection = MrpConnection(config.address, service.port, loop, atv=device_listener) protocol = MrpProtocol(connection, SRPAuthHandler(), service) psm = PlayerStateManager(protocol) remote_control = MrpRemoteControl(loop, psm, protocol) metadata = MrpMetadata(protocol, psm, config.identifier) push_updater = MrpPushUpdater(loop, metadata, psm) power = MrpPower(loop, protocol, remote_control) interfaces[RemoteControl].register(remote_control, Protocol.MRP) interfaces[Metadata].register(metadata, Protocol.MRP) interfaces[Power].register(power, Protocol.MRP) interfaces[PushUpdater].register(push_updater, Protocol.MRP) interfaces[Features].register(MrpFeatures(config, psm), Protocol.MRP) # Forward power events to the facade instance power.listener = interfaces[Power] async def _connect() -> None: await protocol.start() def _close() -> None: push_updater.stop() protocol.stop() # Features managed by this protocol features = set( [ FeatureName.Artwork, FeatureName.VolumeDown, FeatureName.VolumeUp, FeatureName.App, ] ) features.update(_FEATURES_SUPPORTED) features.update(_FEATURE_COMMAND_MAP.keys()) features.update(_FIELD_FEATURES.keys()) return _connect, _close, features
def __init__(self, loop, session, details, airplay): """Initialize a new Apple TV.""" super().__init__() self._session = session self._mrp_service = details.usable_service() self._connection = MrpConnection(details.address, self._mrp_service.port, loop) self._srp = SRPAuthHandler() self._protocol = MrpProtocol(loop, self._connection, self._srp, self._mrp_service) self._mrp_remote = MrpRemoteControl(loop, self._protocol) self._mrp_metadata = MrpMetadata(self._protocol) self._mrp_push_updater = MrpPushUpdater(loop, self._mrp_metadata, self._protocol) self._mrp_pairing = MrpPairingHandler(self._protocol, self._srp, self._mrp_service) self._airplay = airplay
def __init__(self, loop, session, config, airplay): """Initialize a new Apple TV.""" super().__init__() self._session = session self._mrp_service = config.get_service(Protocol.MRP) self._connection = MrpConnection(config.address, self._mrp_service.port, loop, atv=self) self._srp = SRPAuthHandler() self._protocol = MrpProtocol(loop, self._connection, self._srp, self._mrp_service) self._psm = PlayerStateManager(self._protocol, loop) self._mrp_remote = MrpRemoteControl(loop, self._protocol) self._mrp_metadata = MrpMetadata(self._protocol, self._psm, config.identifier) self._mrp_push_updater = MrpPushUpdater(loop, self._mrp_metadata, self._psm) self._airplay = airplay
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")
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)