"""Device pairing and derivation of encryption keys.""" import logging from typing import Dict, Optional, Tuple from pyatv import exceptions from pyatv.auth.hap_pairing import ( HapCredentials, PairSetupProcedure, PairVerifyProcedure, ) from pyatv.auth.hap_srp import SRPAuthHandler from pyatv.auth.hap_tlv8 import TlvValue, read_tlv, stringify, write_tlv from pyatv.protocols.companion.connection import FrameType from pyatv.support import log_binary _LOGGER = logging.getLogger(__name__) PAIRING_DATA_KEY = "_pd" 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 class CompanionPairSetupProcedure(PairSetupProcedure): """Perform pairing and return new credentials.""" def __init__(self, protocol, srp: SRPAuthHandler) -> None: """Initialize a new CompanionPairingProcedure.""" self.protocol = protocol self.srp = srp self._atv_salt = None self._atv_pub_key = None async def start_pairing(self) -> None: """Start pairing procedure.""" self.srp.initialize() await self.protocol.start() resp = await self.protocol.exchange_auth( FrameType.PS_Start, { PAIRING_DATA_KEY: write_tlv( {TlvValue.Method: b"\x00", TlvValue.SeqNo: b"\x01"} ), "_pwTy": 1, }, ) pairing_data = _get_pairing_data(resp) self._atv_salt = pairing_data[TlvValue.Salt] self._atv_pub_key = pairing_data[TlvValue.PublicKey] log_binary( _LOGGER, "Got pub key and salt", Salt=self._atv_salt, PubKey=self._atv_pub_key, ) async def finish_pairing( self, username: str, pin_code: int, display_name: Optional[str] ) -> HapCredentials: """Finish pairing process.""" self.srp.step1(pin_code) pub_key, proof = self.srp.step2(self._atv_pub_key, self._atv_salt) resp = await self.protocol.exchange_auth( FrameType.PS_Next, { PAIRING_DATA_KEY: write_tlv( { TlvValue.SeqNo: b"\x03", TlvValue.PublicKey: pub_key, TlvValue.Proof: proof, } ), "_pwTy": 1, }, ) pairing_data = _get_pairing_data(resp) atv_proof = pairing_data[TlvValue.Proof] log_binary(_LOGGER, "Device", Proof=atv_proof) encrypted_data = self.srp.step3(name=display_name) resp = await self.protocol.exchange_auth( FrameType.PS_Next, { PAIRING_DATA_KEY: write_tlv( { TlvValue.SeqNo: b"\x05", TlvValue.EncryptedData: encrypted_data, } ), "_pwTy": 1, }, ) pairing_data = _get_pairing_data(resp) encrypted_data = pairing_data[TlvValue.EncryptedData] return self.srp.step4(encrypted_data) class CompanionPairVerifyProcedure(PairVerifyProcedure): """Verify credentials and derive new encryption keys.""" def __init__( self, protocol, srp: SRPAuthHandler, credentials: HapCredentials ) -> None: """Initialize a new CompanionPairingVerifier.""" self.protocol = protocol self.srp = srp self.credentials = credentials async def verify_credentials(self) -> bool: """Verify credentials with device.""" _, public_key = self.srp.initialize() resp = await self.protocol.exchange_auth( FrameType.PV_Start, { PAIRING_DATA_KEY: write_tlv( {TlvValue.SeqNo: b"\x01", TlvValue.PublicKey: public_key} ), "_auTy": 4, }, ) pairing_data = _get_pairing_data(resp) server_pub_key = pairing_data[TlvValue.PublicKey] encrypted = pairing_data[TlvValue.EncryptedData] log_binary(_LOGGER, "Device", Public=self.credentials.ltpk, Encrypted=encrypted) encrypted_data = self.srp.verify1(self.credentials, server_pub_key, encrypted) await self.protocol.exchange_auth( FrameType.PV_Next, { PAIRING_DATA_KEY: write_tlv( {TlvValue.SeqNo: b"\x03", TlvValue.EncryptedData: encrypted_data} ), }, ) # TODO: check status code return True def encryption_keys( self, salt: str, output_info: str, input_info: str ) -> Tuple[bytes, bytes]: """Return derived encryption keys.""" return self.srp.verify2(salt, output_info, input_info)