async def finish_pairing(self, pin): """Finish pairing process.""" self.srp.step1(pin) pub_key, proof = self.srp.step2(self._atv_pub_key, self._atv_salt) msg = messages.crypto_pairing({ tlv8.TLV_SEQ_NO: b'\x03', tlv8.TLV_PUBLIC_KEY: pub_key, tlv8.TLV_PROOF: proof }) resp = await self.protocol.send_and_receive(msg, generate_identifier=False) pairing_data = _get_pairing_data(resp) atv_proof = pairing_data[tlv8.TLV_PROOF] log_binary(_LOGGER, 'Device', Proof=atv_proof) encrypted_data = self.srp.step3() msg = messages.crypto_pairing({ tlv8.TLV_SEQ_NO: b'\x05', tlv8.TLV_ENCRYPTED_DATA: encrypted_data }) resp = await self.protocol.send_and_receive(msg, generate_identifier=False) pairing_data = _get_pairing_data(resp) encrypted_data = pairing_data[tlv8.TLV_ENCRYPTED_DATA] return self.srp.step4(encrypted_data)
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')
async def verify_credentials(self): """Verify credentials with device.""" _, public_key = self.srp.initialize() msg = messages.crypto_pairing({ tlv8.TLV_SEQ_NO: b'\x01', tlv8.TLV_PUBLIC_KEY: public_key }) resp = await self.protocol.send_and_receive(msg, generate_identifier=False) resp = _get_pairing_data(resp) session_pub_key = resp[tlv8.TLV_PUBLIC_KEY] encrypted = resp[tlv8.TLV_ENCRYPTED_DATA] log_binary(_LOGGER, 'Device', Public=self.credentials.ltpk, Encrypted=encrypted) encrypted_data = self.srp.verify1(self.credentials, session_pub_key, encrypted) msg = messages.crypto_pairing({ tlv8.TLV_SEQ_NO: b'\x03', tlv8.TLV_ENCRYPTED_DATA: encrypted_data }) resp = await self.protocol.send_and_receive(msg, generate_identifier=False) # TODO: check status code self._output_key, self._input_key = self.srp.verify2()
def send_raw(self, data): """Send message to device.""" log_binary(_LOGGER, '>> Send raw', Data=data) if self._chacha: data = self._chacha.encrypt(data) log_binary(_LOGGER, '>> Send raw', Encrypted=data) data = write_variant(len(data)) + data self._transport.write(data)
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 _handle_message(self, data): if self._chacha: data = self._chacha.decrypt(data) log_binary(_LOGGER, '<< Receive', Decrypted=data) parsed = protobuf.ProtocolMessage() parsed.ParseFromString(data) _LOGGER.debug('<< Receive: Protobuf=%s', parsed) if self.listener: self.listener.message_received(parsed)
def verify1(self): """First device verification step.""" self._check_initialized() self._verify_private = curve25519.Private(secret=self.seed) self._verify_public = self._verify_private.get_public() log_binary(_LOGGER, 'Verification keys', Private=self._verify_private.serialize(), Public=self._verify_public.serialize()) verify_public = self._verify_public.serialize() return b'\x01\x00\x00\x00' + verify_public + self._auth_public
def send(self, message): """Send message to device.""" serialized = message.SerializeToString() log_binary(_LOGGER, '>> Send', Data=serialized) if self._chacha: serialized = self._chacha.encrypt(serialized) log_binary(_LOGGER, '>> Send', Encrypted=serialized) data = write_variant(len(serialized)) + serialized self._transport.write(data) _LOGGER.debug('>> Send: Protobuf=%s', message)
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 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 _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 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 initialize(self, seed=None): """Initialize handler operation. This method will generate new encryption keys and must be called prior to doing authentication or verification. """ self.seed = seed or os.urandom(32) # Generate new seed if not provided signing_key = SigningKey(self.seed) verifying_key = signing_key.get_verifying_key() self._auth_private = signing_key.to_seed() self._auth_public = verifying_key.to_bytes() log_binary(_LOGGER, 'Authentication keys', Private=self._auth_private, Public=self._auth_public)
def step3(self): """Last authentication step.""" self._check_initialized() # TODO: verify: self.client_session_key same as self.session.key_b64()? session_key = binascii.unhexlify(self.client_session_key) aes_key = hash_sha512('Pair-Setup-AES-Key', session_key)[0:16] tmp = bytearray(hash_sha512('Pair-Setup-AES-IV', session_key)[0:16]) tmp[-1] = tmp[-1] + 1 # Last byte must be increased by 1 aes_iv = bytes(tmp) log_binary(_LOGGER, 'Pair-Setup-AES', Key=aes_key, IV=aes_iv) epk, tag = aes_encrypt(modes.GCM, aes_key, aes_iv, self._auth_public) log_binary(_LOGGER, 'Pair-Setup EPK+Tag', EPK=epk, Tag=tag) return epk, tag
def data_received(self, data): """Message was received from device.""" # A message might be split over several reads, so we store a buffer and # try to decode messages from that buffer self._buffer += data log_binary(_LOGGER, '<< Receive', Data=data) while self._buffer: # The variant tells us how much data must follow length, raw = read_variant(self._buffer) if len(raw) < length: _LOGGER.debug('Require %d bytes but only %d in buffer', length, len(raw)) break data = raw[:length] # Incoming message (might be encrypted) self._buffer = raw[length:] # Buffer, might contain more messages try: self._handle_message(data) except Exception: # pylint: disable=broad-except _LOGGER.error('Failed to handle message')
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)
def verify2(self, atv_public_key, data): """Last device verification step.""" self._check_initialized() log_binary(_LOGGER, 'Verify', PublicSecret=atv_public_key, Data=data) # 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 log_binary(_LOGGER, 'Shared secret', Secret=shared) # 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] log_binary(_LOGGER, 'Pair-Verify-AES', Key=aes_key, IV=aes_iv) # 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) log_binary(_LOGGER, 'Signature', Signature=signature) # Signature is prepended with 0x00000000 (alignment?) return b'\x00\x00\x00\x00' + signature
def test_log_single_arg_if_enabled(self): log.log_binary(self.mock_logger, 'abc', test=b'\x01\x02') self.assertEqual(self._debug_string(), 'abc (test=0102)')
def test_no_log_if_not_debug(self): self.mock_logger.isEnabledFor.return_value = False log.log_binary(self.mock_logger, 'test') self.mock_logger.isEnabledFor.assert_called_with(logging.DEBUG)
def test_log_no_args_if_enabled(self): log.log_binary(self.mock_logger, 'testing') self.assertEqual(self._debug_string(), 'testing ()')
def test_log_multiple_args_if_enabled(self): log.log_binary(self.mock_logger, 'k', test=b'\x01\x02', dummy=b'\xfe') self.assertEqual(self._debug_string(), 'k (dummy=fe, test=0102)')