def handle_pair_setup(self, request: http.HttpRequest): """Handle incoming /pair-setup request.""" if request.headers.get("X-Apple-HKP") != "4": return http.HttpResponse( "RTSP", "1.0", 501, "Not implemented", {"CSeq": request.headers["CSeq"]}, b"", ) body = ( request.body if isinstance(request.body, bytes) else request.body.encode("utf-8") ) pairing_data = read_tlv(body) _LOGGER.debug("Transient pair-setup message received: %s", pairing_data) seqno = int.from_bytes(pairing_data[TlvValue.SeqNo], byteorder="little") tlv = getattr(self, f"_m{seqno}_setup".format(seqno))(pairing_data) return http.HttpResponse( "RTSP", "1.0", 200, "OK", {"CSeq": request.headers["CSeq"]}, tlv, )
async def verify_credentials(self) -> bool: """Verify if device is allowed to use AirPlau.""" self.srp.initialize() await self.http.post("/pair-pin-start", headers=_AIRPLAY_HEADERS) data = { hap_tlv8.TlvValue.Method: b"\x00", hap_tlv8.TlvValue.SeqNo: b"\x01" } resp = await self.http.post("/pair-setup", body=hap_tlv8.write_tlv(data), headers=_AIRPLAY_HEADERS) pairing_data = hap_tlv8.read_tlv(resp.body) atv_salt = pairing_data[hap_tlv8.TlvValue.Salt] atv_pub_key = pairing_data[hap_tlv8.TlvValue.PublicKey] self.srp.step1(_TRANSIENT_PIN) pub_key, proof = self.srp.step2(atv_pub_key, atv_salt) data = { hap_tlv8.TlvValue.SeqNo: b"\x03", hap_tlv8.TlvValue.PublicKey: pub_key, hap_tlv8.TlvValue.Proof: proof, } await self.http.post("/pair-setup", body=hap_tlv8.write_tlv(data), headers=_AIRPLAY_HEADERS) return True
async def finish_pairing(self, username: str, pin_code: int) -> bool: """Finish authentication process. A username (generated by new_credentials) and the PIN code shown on screen must be provided. """ # Step 1 self.srp.step1(pin_code) pub_key, proof = self.srp.step2(self._atv_pub_key, self._atv_salt) data = { hap_tlv8.TlvValue.SeqNo: b"\x03", hap_tlv8.TlvValue.PublicKey: pub_key, hap_tlv8.TlvValue.Proof: proof, } await self.http.post("/pair-setup", body=hap_tlv8.write_tlv(data), headers=_AIRPLAY_HEADERS) data = { hap_tlv8.TlvValue.SeqNo: b"\x05", hap_tlv8.TlvValue.EncryptedData: self.srp.step3(), } resp = await self.http.post("/pair-setup", body=hap_tlv8.write_tlv(data), headers=_AIRPLAY_HEADERS) pairing_data = hap_tlv8.read_tlv(resp.body) encrypted_data = pairing_data[hap_tlv8.TlvValue.EncryptedData] return self.srp.step4(encrypted_data)
async def verify_credentials(self) -> bool: """Verify if device is allowed to use AirPlau.""" _, public_key = self.srp.initialize() resp = await self._send({ hap_tlv8.TlvValue.SeqNo: b"\x01", hap_tlv8.TlvValue.PublicKey: public_key, }) pairing_data = hap_tlv8.read_tlv(resp.body) session_pub_key = pairing_data[hap_tlv8.TlvValue.PublicKey] encrypted = pairing_data[hap_tlv8.TlvValue.EncryptedData] log_binary(_LOGGER, "Device", Public=self.credentials.ltpk, Encrypted=encrypted) encrypted_data = self.srp.verify1(self.credentials, session_pub_key, encrypted) await self._send({ hap_tlv8.TlvValue.SeqNo: b"\x03", hap_tlv8.TlvValue.EncryptedData: encrypted_data, }) # TODO: check status code return True
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 = 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)
def handle_auth_frame(self, frame_type, data): """Handle incoming auth message.""" _LOGGER.debug("Received auth frame: type=%s, data=%s", frame_type, data) pairing_data = read_tlv(data["_pd"]) seqno = int.from_bytes(pairing_data[TlvValue.SeqNo], byteorder="little") suffix = ("verify" if frame_type in [FrameType.PV_Start, FrameType.PV_Next] else "setup") getattr(self, f"_m{seqno}_{suffix}")(pairing_data)
def _m5_setup(self, pairing_data): 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), ) chacha = chacha20.Chacha20Cipher(session_key, session_key) decrypted_tlv_bytes = chacha.decrypt( pairing_data[TlvValue.EncryptedData], nounce="PS-Msg05".encode()) _LOGGER.debug("MSG5 EncryptedData=%s", read_tlv(decrypted_tlv_bytes)) other = { "altIRK": b"-\x54\xe0\x7a\x88*en\x11\xab\x82v-'%\xc5", "accountID": "DC6A7CB6-CA1A-4BF4-880D-A61B717814DB", "model": "AppleTV6,2", "wifiMAC": b"@\xff\xa1\x8f\xa1\xb9", "name": "Living Room", "mac": b"@\xc4\xff\x8f\xb1\x99", } device_info = acc_device_x + self.unique_id + self.keys.auth_pub signature = self.keys.sign.sign(device_info) tlv = { TlvValue.Identifier: self.unique_id, TlvValue.PublicKey: self.keys.auth_pub, TlvValue.Signature: signature, 17: opack.pack(other), } tlv = write_tlv(tlv) chacha = chacha20.Chacha20Cipher(session_key, session_key) encrypted = chacha.encrypt(tlv, nounce="PS-Msg06".encode()) tlv = write_tlv({ TlvValue.SeqNo: b"\x06", TlvValue.EncryptedData: encrypted }) self.send_to_client(FrameType.PS_Next, {"_pd": tlv}) self.has_paired()
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
def handle_crypto_pairing(self, message, inner): """Handle incoming crypto pairing message.""" _LOGGER.debug("Received crypto pairing message") pairing_data = read_tlv(inner.pairingData) seqno = pairing_data[TlvValue.SeqNo][0] # Work-around for now to support "tries" to auth before pairing if seqno == 1: if TlvValue.PublicKey in pairing_data: self.has_paired = True elif TlvValue.Method in pairing_data: self.has_paired = False suffix = "verify" if self.has_paired else "setup" getattr(self, f"_m{seqno}_{suffix}")(pairing_data)
async def start_pairing(self) -> None: """Start the authentication process. This method will show the expected PIN on screen. """ self.srp.initialize() await self.http.post("/pair-pin-start", headers=_AIRPLAY_HEADERS) data = { hap_tlv8.TlvValue.Method: b"\x00", hap_tlv8.TlvValue.SeqNo: b"\x01" } resp = await self.http.post("/pair-setup", body=hap_tlv8.write_tlv(data), headers=_AIRPLAY_HEADERS) pairing_data = hap_tlv8.read_tlv(resp.body) self._atv_salt = pairing_data[hap_tlv8.TlvValue.Salt] self._atv_pub_key = pairing_data[hap_tlv8.TlvValue.PublicKey]
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 = 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 = write_tlv({ TlvValue.Identifier: credentials.client_id, TlvValue.Signature: device_signature, }) return chacha.encrypt(tlv, nounce="PV-Msg03".encode())
def test_read_key_larger_than_255_bytes(): assert read_tlv(LARGE_KEY_OUT) == LARGE_KEY_IN
def test_read_two_keys(): assert read_tlv(DOUBLE_KEY_OUT) == DOUBLE_KEY_IN
def test_read_single_key(): assert read_tlv(SINGLE_KEY_OUT) == SINGLE_KEY_IN
def _get_pairing_data(resp): tlv = read_tlv(resp.inner().pairingData) if TlvValue.Error in tlv: raise exceptions.AuthenticationError(stringify(tlv)) return tlv