def _handle_response(self): # following the information from page 71 about HTTP Message splitting: # The blocks start with 2 byte little endian defining the length of the encrypted data (max 1024 bytes) # followed by 16 byte authTag blocks = [] tmp = bytearray() exp_len = 512 while True: data = self.sock.recv(exp_len) tmp += data length = int.from_bytes(tmp[0:2], 'little') if length + 18 > len(tmp): # if the the amount of data in tmp is not length + 2 bytes for length + 16 bytes for the tag, the block # is not complete yet continue tmp = tmp[2:] block = tmp[0:length] tmp = tmp[length:] tag = tmp[0:16] tmp = tmp[16:] blocks.append((length, block, tag)) # check how long next block will be if int.from_bytes(tmp[0:2], 'little') < 1024: exp_len = int.from_bytes(tmp[0:2], 'little') - len(tmp) + 18 if length < 1024: break # now decrypt the blocks and assemble the answer to our request result = bytearray() for b in blocks: tmp = chacha20_aead_decrypt( b[0].to_bytes(2, byteorder='little'), self.a2c_key, self.a2c_counter.to_bytes(8, byteorder='little'), bytes([0, 0, 0, 0]), b[1] + b[2]) if tmp is not False: result += tmp self.a2c_counter += 1 # # I expected a full http response but the first real homekit accessory (Koogeek-P1) just replies with body # in chunked mode... # if result.startswith(b'HTTP/1.1'): r = http.client.HTTPResponse(SecureHttp.Wrapper(result)) r.begin() return r else: data = SecureHttp._parse(result) return self.HTTPResponseWrapper(data)
def _read_response(self): # following the information from page 71 about HTTP Message splitting: # The blocks start with 2 byte little endian defining the length of the encrypted data (max 1024 bytes) # followed by 16 byte authTag blocks = [] tmp = bytearray() exp_len = 512 while True: data = self.sock.recv(exp_len) tmp += data length = int.from_bytes(tmp[0:2], 'little') if length + 18 > len(tmp): # if the the amount of data in tmp is not length + 2 bytes for length + 16 bytes for the tag, the block # is not complete yet continue tmp = tmp[2:] block = tmp[0:length] tmp = tmp[length:] tag = tmp[0:16] tmp = tmp[16:] blocks.append((length, block, tag)) # check how long next block will be if int.from_bytes(tmp[0:2], 'little') < 1024: exp_len = int.from_bytes(tmp[0:2], 'little') - len(tmp) + 18 if length < 1024: break # now decrypt the blocks and assemble the answer to our request result = bytearray() for b in blocks: tmp = chacha20_aead_decrypt( b[0].to_bytes(2, byteorder='little'), self.a2c_key, self.a2c_counter.to_bytes(8, byteorder='little'), bytes([0, 0, 0, 0]), b[1] + b[2]) if tmp is not False: result += tmp self.a2c_counter += 1 return result
def handle_one_request(self): """ This is used to determine wether the request is encrypted or not. This is done by looking at the first bytes of the request. To be valid unencrypted HTTP call, it must be one of the methods defined in RFC7231 Section 4 "Request Methods". :return: """ try: # make connection non blocking so the select can work self.connection.setblocking(0) ready = select.select([self.connection], [], [], 1) # no data was to be received, so we count up to track how many seconds in total this happened if not ready[0]: self.timeout_counter += 1 # if this is above our configured timeout the connection gets closed if self.timeout_counter >= self.timeout: self.close_connection = True return raw_peeked_data = self.rfile.peek(10) if len(raw_peeked_data) == 0: # since select says ready but no data is there, close the connection to prevent hidden busy waiting to # rise load self.close_connection = True return # data was received so reset the timeout handler self.timeout_counter = 0 # RFC7230 Section 3 tells us, that US-ASCII is fine peeked_data = raw_peeked_data[:10] peeked_data = peeked_data.decode(encoding='ASCII') # self.log_message('deciding over: >%s<', peeked_data) # If the request line starts with a known HTTP verb, then use handle_one_request from super class if ' ' in peeked_data: method = peeked_data.split(' ', 1)[0] if method in self.VALID_METHODS: self.server.sessions[self.session_id]['enrypted_connection'] = False BaseHTTPRequestHandler.handle_one_request(self) return except (socket.timeout, OSError) as e: # a read or a write timed out. Discard this connection self.log_error(' %r', e) self.close_connection = True return except UnicodeDecodeError as e: self.log_error('Unicode exception %s' % e) pass # the first 2 bytes are the length of the encrypted data to follow len_bytes = self.rfile.read(2) data_len = int.from_bytes(len_bytes, byteorder='little') # the authtag is not counted, so add its length data = self.rfile.read(data_len + 16) if HomeKitRequestHandler.DEBUG_CRYPT: self.log_message('data >%i< >%s<', len(data), binascii.hexlify(data)) # get the crypto key from the session c2a_key = self.server.sessions[self.session_id]['controller_to_accessory_key'] # verify & decrypt the read data cnt_bytes = self.server.sessions[self.session_id]['controller_to_accessory_count'].to_bytes(8, byteorder='little') decrypted = chacha20_aead_decrypt(len_bytes, c2a_key, cnt_bytes, bytes([0, 0, 0, 0]), data) if decrypted == False: self.log_error('Could not decrypt %s', binascii.hexlify(data)) # TODO: handle errors pass if HomeKitRequestHandler.DEBUG_CRYPT: self.log_message('crypted request >%s<', decrypted) self.server.sessions[self.session_id]['controller_to_accessory_count'] += 1 # replace the original rfile with a fake with the decrypted stuff old_rfile = self.rfile self.rfile = io.BytesIO(decrypted) # replace writefile to pass on encrypted data old_wfile = self.wfile self.wfile = io.BytesIO() # call known function self.server.sessions[self.session_id]['enrypted_connection'] = True BaseHTTPRequestHandler.handle_one_request(self) # read the plaintext and send it out encrypted self.wfile.seek(0) in_data = self.wfile.read(65537) if HomeKitRequestHandler.DEBUG_CRYPT: self.log_message('response >%s<', in_data) self.log_message('len(response) %s', len(in_data)) block_size = 1024 out_data = bytearray() while len(in_data) > 0: block = in_data[:block_size] if HomeKitRequestHandler.DEBUG_CRYPT: self.log_message('==> BLOCK: len %s', len(block)) in_data = in_data[block_size:] len_bytes = len(block).to_bytes(2, byteorder='little') a2c_key = self.server.sessions[self.session_id]['accessory_to_controller_key'] cnt_bytes = self.server.sessions[self.session_id]['accessory_to_controller_count'].to_bytes(8, byteorder='little') ciper_and_mac = chacha20_aead_encrypt(len_bytes, a2c_key, cnt_bytes, bytes([0, 0, 0, 0]), block) self.server.sessions[self.session_id]['accessory_to_controller_count'] += 1 out_data += len_bytes + ciper_and_mac[0] + ciper_and_mac[1] # change back to originals to handle multiple calls self.rfile = old_rfile self.wfile = old_wfile # send data to original requester self.wfile.write(out_data) self.wfile.flush()
def _post_pair_setup(self): d_req = TLV.decode_bytes(self.body) self.log_message('POST /pair-setup request body:\n%s', TLV.to_string(d_req)) d_res = {} if d_req[TLV.kTLVType_State] == TLV.M1: # step #2 Accessory -> iOS Device SRP Start Response self.log_message('Step #2 /pair-setup') # 1) Check if paired if self.server.data.is_paired: self.send_error_reply(TLV.M2, TLV.kTLVError_Unavailable) return # 2) Check if over 100 attempts if self.server.data.unsuccessful_tries > 100: self.log_error('to many failed attempts') self.send_error_reply(TLV.M2, TLV.kTLVError_MaxTries) return # 3) Check if already in pairing if False: self.send_error_reply(TLV.M2, TLV.kTLVError_Busy) return # 4) 5) 7) Create in SRP Session, set username and password server = SrpServer('Pair-Setup', self.server.data.setup_code) # 6) create salt salt = server.get_salt() # 8) show setup code to user sc = self.server.data.setup_code sc_str = 'Setup Code\n┌─' + '─' * len(sc) + '─┐\n│ ' + sc + ' │\n└─' + '─' * len(sc) + '─┘' self.log_message(sc_str) # 9) create public key public_key = server.get_public_key() # 10) create response tlv and send response d_res[TLV.kTLVType_State] = TLV.M2 d_res[TLV.kTLVType_PublicKey] = SrpServer.to_byte_array(public_key) d_res[TLV.kTLVType_Salt] = SrpServer.to_byte_array(salt) self._send_response_tlv(d_res) # store session self.server.sessions[self.session_id]['srp'] = server self.log_message('after step #2:\n%s', TLV.to_string(d_res)) return if d_req[TLV.kTLVType_State] == TLV.M3: # step #4 Accessory -> iOS Device SRP Verify Response self.log_message('Step #4 /pair-setup') # 1) use ios pub key to compute shared secret key ios_pub_key = bytes_to_mpz(d_req[TLV.kTLVType_PublicKey]) server = self.server.sessions[self.session_id]['srp'] server.set_client_public_key(ios_pub_key) hkdf_inst = hkdf.Hkdf('Pair-Setup-Encrypt-Salt'.encode(), SrpServer.to_byte_array(server.get_session_key()), hash=hashlib.sha512) session_key = hkdf_inst.expand('Pair-Setup-Encrypt-Info'.encode(), 32) self.server.sessions[self.session_id]['session_key'] = session_key # 2) verify ios proof ios_proof = bytes_to_mpz(d_req[TLV.kTLVType_Proof]) if not server.verify_clients_proof(ios_proof): d_res[TLV.kTLVType_State] = TLV.M4 d_res[TLV.kTLVType_Error] = TLV.kTLVError_Authentication self._send_response_tlv(d_res) print('error in step #4', d_res, self.server.sessions) return else: self.log_message('ios proof was verified') # 3) generate accessory proof accessory_proof = server.get_proof(ios_proof) # 4) create response tlv d_res[TLV.kTLVType_State] = TLV.M4 d_res[TLV.kTLVType_Proof] = SrpServer.to_byte_array(accessory_proof) # 5) send response tlv self._send_response_tlv(d_res) self.log_message('after step #4:\n%s', TLV.to_string(d_res)) return if d_req[TLV.kTLVType_State] == TLV.M5: # step #6 Accessory -> iOS Device Exchange Response self.log_message('Step #6 /pair-setup') # 1) Verify the iOS device's authTag # done by chacha20_aead_decrypt # 2) decrypt and test encrypted_data = d_req[TLV.kTLVType_EncryptedData] decrypted_data = chacha20_aead_decrypt(bytes(), self.server.sessions[self.session_id]['session_key'], 'PS-Msg05'.encode(), bytes([0, 0, 0, 0]), encrypted_data) if decrypted_data == False: d_res[TLV.kTLVType_State] = TLV.M6 d_res[TLV.kTLVType_Error] = TLV.kTLVError_Authentication self.send_error_reply(TLV.M6, TLV.kTLVError_Authentication) print('error in step #6', d_res, self.server.sessions) return d_req_2 = TLV.decode_bytearray(decrypted_data) # 3) Derive ios_device_x shared_secret = self.server.sessions[self.session_id]['srp'].get_session_key() hkdf_inst = hkdf.Hkdf('Pair-Setup-Controller-Sign-Salt'.encode(), SrpServer.to_byte_array(shared_secret), hash=hashlib.sha512) ios_device_x = hkdf_inst.expand('Pair-Setup-Controller-Sign-Info'.encode(), 32) # 4) construct ios_device_info ios_device_pairing_id = d_req_2[TLV.kTLVType_Identifier] ios_device_ltpk = d_req_2[TLV.kTLVType_PublicKey] ios_device_info = ios_device_x + ios_device_pairing_id + ios_device_ltpk # 5) verify signature ios_device_sig = d_req_2[TLV.kTLVType_Signature] verify_key = py25519.Key25519(pubkey=bytes(), verifyingkey=bytes(ios_device_ltpk)) if not verify_key.verify(bytes(ios_device_sig), bytes(ios_device_info)): self.send_error_reply(TLV.M6, TLV.kTLVError_Authentication) print('error in step #6', d_res, self.server.sessions) return # 6) save ios_device_pairing_id and ios_device_ltpk self.server.data.add_peer(ios_device_pairing_id, ios_device_ltpk) # Response Generation # 1) generate accessoryLTPK if not existing if self.server.data.accessory_ltsk is None or self.server.data.accessory_ltpk is None: accessory_ltsk = py25519.Key25519() accessory_ltpk = accessory_ltsk.verifyingkey self.server.data.set_accessory_keys(accessory_ltpk, accessory_ltsk.secretkey) else: accessory_ltsk = py25519.Key25519(self.server.data.accessory_ltsk) accessory_ltpk = accessory_ltsk.verifyingkey # 2) derive AccessoryX hkdf_inst = hkdf.Hkdf('Pair-Setup-Accessory-Sign-Salt'.encode(), SrpServer.to_byte_array(shared_secret), hash=hashlib.sha512) accessory_x = hkdf_inst.expand('Pair-Setup-Accessory-Sign-Info'.encode(), 32) # 3) accessory_info = accessory_x + self.server.data.accessory_pairing_id_bytes + accessory_ltpk # 4) generate signature accessory_signature = accessory_ltsk.sign(accessory_info) # 5) construct sub_tlv sub_tlv = { TLV.kTLVType_Identifier: self.server.data.accessory_pairing_id_bytes, TLV.kTLVType_PublicKey: accessory_ltpk, TLV.kTLVType_Signature: accessory_signature } sub_tlv_b = TLV.encode_dict(sub_tlv) # 6) encrypt sub_tlv encrypted_data_with_auth_tag = chacha20_aead_encrypt(bytes(), self.server.sessions[self.session_id]['session_key'], 'PS-Msg06'.encode(), bytes([0, 0, 0, 0]), sub_tlv_b) tmp = bytearray(encrypted_data_with_auth_tag[0]) tmp += encrypted_data_with_auth_tag[1] # 7) send response self.server.publish_device() d_res = dict() d_res[TLV.kTLVType_State] = TLV.M6 d_res[TLV.kTLVType_EncryptedData] = tmp self._send_response_tlv(d_res) self.log_message('after step #6:\n%s', TLV.to_string(d_res)) return self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED)
def _post_pair_verify(self): d_req = TLV.decode_bytes(self.body) d_res = {} if d_req[TLV.kTLVType_State] == TLV.M1: # step #2 Accessory -> iOS Device Verify Start Response if HomeKitRequestHandler.DEBUG_PAIR_VERIFY: self.log_message('Step #2 /pair-verify') # 1) generate new curve25519 key pair accessory_session_key = py25519.Key25519() accessory_spk = accessory_session_key.public_key().pubkey self.server.sessions[self.session_id]['accessory_pub_key'] = accessory_spk # 2) generate shared secret ios_device_curve25519_pub_key_bytes = d_req[TLV.kTLVType_PublicKey] self.server.sessions[self.session_id]['ios_device_pub_key'] = ios_device_curve25519_pub_key_bytes ios_device_curve25519_pub_key = py25519.Key25519(pubkey=bytes(ios_device_curve25519_pub_key_bytes), verifyingkey=bytes()) shared_secret = accessory_session_key.get_ecdh_key(ios_device_curve25519_pub_key) self.server.sessions[self.session_id]['shared_secret'] = shared_secret # 3) generate accessory info accessory_info = accessory_spk + self.server.data.accessory_pairing_id_bytes + \ ios_device_curve25519_pub_key_bytes # 4) sign accessory info for accessory signature accessory_ltsk = py25519.Key25519(secretkey=self.server.data.accessory_ltsk) accessory_signature = accessory_ltsk.sign(accessory_info) # 5) sub tlv sub_tlv = { TLV.kTLVType_Identifier: self.server.data.accessory_pairing_id_bytes, TLV.kTLVType_Signature: accessory_signature } sub_tlv_b = TLV.encode_dict(sub_tlv) # 6) derive session key hkdf_inst = hkdf.Hkdf('Pair-Verify-Encrypt-Salt'.encode(), shared_secret, hash=hashlib.sha512) session_key = hkdf_inst.expand('Pair-Verify-Encrypt-Info'.encode(), 32) self.server.sessions[self.session_id]['session_key'] = session_key # 7) encrypt sub tlv encrypted_data_with_auth_tag = chacha20_aead_encrypt(bytes(), session_key, 'PV-Msg02'.encode(), bytes([0, 0, 0, 0]), sub_tlv_b) tmp = bytearray(encrypted_data_with_auth_tag[0]) tmp += encrypted_data_with_auth_tag[1] # 8) construct result tlv d_res[TLV.kTLVType_State] = TLV.M2 d_res[TLV.kTLVType_PublicKey] = accessory_spk d_res[TLV.kTLVType_EncryptedData] = tmp self._send_response_tlv(d_res) if HomeKitRequestHandler.DEBUG_PAIR_VERIFY: self.log_message('after step #2\n%s', TLV.to_string(d_res)) return if d_req[TLV.kTLVType_State] == TLV.M3: # step #4 Accessory -> iOS Device Verify Finish Response if HomeKitRequestHandler.DEBUG_PAIR_VERIFY: self.log_message('Step #4 /pair-verify') session_key = self.server.sessions[self.session_id]['session_key'] # 1) verify ios' authtag # 2) decrypt encrypted = d_req[TLV.kTLVType_EncryptedData] decrypted = chacha20_aead_decrypt(bytes(), session_key, 'PV-Msg03'.encode(), bytes([0, 0, 0, 0]), encrypted) if decrypted == False: self.send_error_reply(TLV.M4, TLV.kTLVError_Authentication) print('error in step #4: authtag', d_res, self.server.sessions) return d1 = TLV.decode_bytes(decrypted) assert TLV.kTLVType_Identifier in d1 assert TLV.kTLVType_Signature in d1 # 3) get ios_device_ltpk ios_device_pairing_id = d1[TLV.kTLVType_Identifier] self.server.sessions[self.session_id]['ios_device_pairing_id'] = ios_device_pairing_id ios_device_ltpk_bytes = self.server.data.get_peer_key(ios_device_pairing_id) if ios_device_ltpk_bytes is None: self.send_error_reply(TLV.M4, TLV.kTLVError_Authentication) print('error in step #4: not paired', d_res, self.server.sessions) return ios_device_ltpk = py25519.Key25519(pubkey=bytes(), verifyingkey=ios_device_ltpk_bytes) # 4) verify ios_device_info ios_device_sig = d1[TLV.kTLVType_Signature] ios_device_curve25519_pub_key_bytes = self.server.sessions[self.session_id]['ios_device_pub_key'] accessory_spk = self.server.sessions[self.session_id]['accessory_pub_key'] ios_device_info = ios_device_curve25519_pub_key_bytes + ios_device_pairing_id + accessory_spk if not ios_device_ltpk.verify(bytes(ios_device_sig), bytes(ios_device_info)): self.send_error_reply(TLV.M4, TLV.kTLVError_Authentication) print('error in step #4: signature', d_res, self.server.sessions) return # shared_secret = self.server.sessions[self.session_id]['shared_secret'] hkdf_inst = hkdf.Hkdf('Control-Salt'.encode(), shared_secret, hash=hashlib.sha512) controller_to_accessory_key = hkdf_inst.expand('Control-Write-Encryption-Key'.encode(), 32) self.server.sessions[self.session_id]['controller_to_accessory_key'] = controller_to_accessory_key self.server.sessions[self.session_id]['controller_to_accessory_count'] = 0 hkdf_inst = hkdf.Hkdf('Control-Salt'.encode(), shared_secret, hash=hashlib.sha512) accessory_to_controller_key = hkdf_inst.expand('Control-Read-Encryption-Key'.encode(), 32) self.server.sessions[self.session_id]['accessory_to_controller_key'] = accessory_to_controller_key self.server.sessions[self.session_id]['accessory_to_controller_count'] = 0 d_res[TLV.kTLVType_State] = TLV.M4 self._send_response_tlv(d_res) if HomeKitRequestHandler.DEBUG_PAIR_VERIFY: self.log_message('after step #4\n%s', TLV.to_string(d_res)) return self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED)
def perform_pair_setup(connection, pin, ios_pairing_id): """ Performs a pair setup operation as described in chapter 4.7 page 39 ff. :param connection: the http connection to the target accessory :param pin: the setup code from the accessory :param ios_pairing_id: the id of the simulated ios device :return: a dict with the ios device's part of the pairing information """ headers = {'Content-Type': 'application/pairing+tlv8'} # # Step #1 ios --> accessory (send SRP start Request) (see page 39) # request_tlv = TLV.encode_list([(TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.PairSetup)]) connection.request('POST', '/pair-setup', request_tlv, headers) resp = connection.getresponse() response_tlv = TLV.decode_bytes(resp.read()) # # Step #3 ios --> accessory (send SRP verify request) (see page 41) # assert TLV.kTLVType_State in response_tlv, response_tlv assert response_tlv[TLV.kTLVType_State] == TLV.M2 if TLV.kTLVType_Error in response_tlv: error_handler(response_tlv[TLV.kTLVType_Error], "step 3") srp_client = SrpClient('Pair-Setup', pin) srp_client.set_salt(response_tlv[TLV.kTLVType_Salt]) srp_client.set_server_public_key(response_tlv[TLV.kTLVType_PublicKey]) client_pub_key = srp_client.get_public_key() client_proof = srp_client.get_proof() response_tlv = TLV.encode_list([ (TLV.kTLVType_State, TLV.M3), (TLV.kTLVType_PublicKey, SrpClient.to_byte_array(client_pub_key)), (TLV.kTLVType_Proof, SrpClient.to_byte_array(client_proof)), ]) connection.request('POST', '/pair-setup', response_tlv, headers) resp = connection.getresponse() response_tlv = TLV.decode_bytes(resp.read()) # # Step #5 ios --> accessory (Exchange Request) (see page 43) # # M4 Verification (page 43) assert TLV.kTLVType_State in response_tlv, response_tlv assert response_tlv[TLV.kTLVType_State] == TLV.M4 if TLV.kTLVType_Error in response_tlv: error_handler(response_tlv[TLV.kTLVType_Error], "step 5") assert TLV.kTLVType_Proof in response_tlv if not srp_client.verify_servers_proof(response_tlv[TLV.kTLVType_Proof]): print('Step #5: wrong proof!') # M5 Request generation (page 44) session_key = srp_client.get_session_key() ios_device_ltsk, ios_device_ltpk = ed25519.create_keypair() # reversed: # Pair-Setup-Encrypt-Salt instead of Pair-Setup-Controller-Sign-Salt # Pair-Setup-Encrypt-Info instead of Pair-Setup-Controller-Sign-Info hkdf_inst = hkdf.Hkdf('Pair-Setup-Controller-Sign-Salt'.encode(), SrpClient.to_byte_array(session_key), hash=hashlib.sha512) ios_device_x = hkdf_inst.expand('Pair-Setup-Controller-Sign-Info'.encode(), 32) hkdf_inst = hkdf.Hkdf('Pair-Setup-Encrypt-Salt'.encode(), SrpClient.to_byte_array(session_key), hash=hashlib.sha512) session_key = hkdf_inst.expand('Pair-Setup-Encrypt-Info'.encode(), 32) ios_device_pairing_id = ios_pairing_id.encode() ios_device_info = ios_device_x + ios_device_pairing_id + ios_device_ltpk.to_bytes( ) ios_device_signature = ios_device_ltsk.sign(ios_device_info) sub_tlv = [(TLV.kTLVType_Identifier, ios_device_pairing_id), (TLV.kTLVType_PublicKey, ios_device_ltpk.to_bytes()), (TLV.kTLVType_Signature, ios_device_signature)] sub_tlv_b = TLV.encode_list(sub_tlv) # taking tge iOSDeviceX as key was reversed from # https://github.com/KhaosT/HAP-NodeJS/blob/2ea9d761d9bd7593dd1949fec621ab085af5e567/lib/HAPServer.js # function handlePairStepFive calling encryption.encryptAndSeal encrypted_data_with_auth_tag = chacha20_aead_encrypt( bytes(), session_key, 'PS-Msg05'.encode(), bytes([0, 0, 0, 0]), sub_tlv_b) tmp = bytearray(encrypted_data_with_auth_tag[0]) tmp += encrypted_data_with_auth_tag[1] response_tlv = [(TLV.kTLVType_State, TLV.M5), (TLV.kTLVType_EncryptedData, tmp)] body = TLV.encode_list(response_tlv) connection.request('POST', '/pair-setup', body, headers) resp = connection.getresponse() response_tlv = TLV.decode_bytes(resp.read()) # # Step #7 ios (Verification) (page 47) # assert response_tlv[TLV.kTLVType_State] == TLV.M6 if TLV.kTLVType_Error in response_tlv: error_handler(response_tlv[TLV.kTLVType_Error], "step 7") assert TLV.kTLVType_EncryptedData in response_tlv decrypted_data = chacha20_aead_decrypt( bytes(), session_key, 'PS-Msg06'.encode(), bytes([0, 0, 0, 0]), response_tlv[TLV.kTLVType_EncryptedData]) if decrypted_data == False: raise homekit.exception.IllegalData("step 7") response_tlv = TLV.decode_bytearray(decrypted_data) assert TLV.kTLVType_Signature in response_tlv accessory_sig = response_tlv[TLV.kTLVType_Signature] assert TLV.kTLVType_Identifier in response_tlv accessory_pairing_id = response_tlv[TLV.kTLVType_Identifier] assert TLV.kTLVType_PublicKey in response_tlv accessory_ltpk = response_tlv[TLV.kTLVType_PublicKey] hkdf_inst = hkdf.Hkdf('Pair-Setup-Accessory-Sign-Salt'.encode(), SrpClient.to_byte_array( srp_client.get_session_key()), hash=hashlib.sha512) accessory_x = hkdf_inst.expand('Pair-Setup-Accessory-Sign-Info'.encode(), 32) accessory_info = accessory_x + accessory_pairing_id + accessory_ltpk e25519s = ed25519.VerifyingKey(bytes(response_tlv[TLV.kTLVType_PublicKey])) e25519s.verify(bytes(accessory_sig), bytes(accessory_info)) return { 'AccessoryPairingID': response_tlv[TLV.kTLVType_Identifier].decode(), 'AccessoryLTPK': hexlify(response_tlv[TLV.kTLVType_PublicKey]).decode(), 'iOSPairingId': ios_pairing_id, 'iOSDeviceLTSK': ios_device_ltsk.to_ascii(encoding='hex').decode(), 'iOSDeviceLTPK': hexlify(ios_device_ltpk.to_bytes()).decode() }
def get_session_keys(conn, pairing_data): """ Performs a pair verify operation as described in chapter 4.8 page 47 ff. :param conn: the http connection to the target accessory :param pairing_data: the paring data as returned by perform_pair_setup :return: tuple of the session keys (controller_to_accessory_key and accessory_to_controller_key) """ headers = {'Content-Type': 'application/pairing+tlv8'} # # Step #1 ios --> accessory (send verify start Request) (page 47) # ios_key = py25519.Key25519() request_tlv = TLV.encode_list([(TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_PublicKey, ios_key.pubkey)]) conn.request('POST', '/pair-verify', request_tlv, headers) resp = conn.getresponse() response_tlv = TLV.decode_bytes(resp.read()) # # Step #3 ios --> accessory (send SRP verify request) (page 49) # assert TLV.kTLVType_State in response_tlv, response_tlv assert response_tlv[TLV.kTLVType_State] == TLV.M2 assert TLV.kTLVType_PublicKey in response_tlv, response_tlv assert TLV.kTLVType_EncryptedData in response_tlv, response_tlv # 1) generate shared secret accessorys_session_pub_key_bytes = response_tlv[TLV.kTLVType_PublicKey] shared_secret = ios_key.get_ecdh_key( py25519.Key25519(pubkey=bytes(accessorys_session_pub_key_bytes), verifyingkey=bytes())) # 2) derive session key hkdf_inst = hkdf.Hkdf('Pair-Verify-Encrypt-Salt'.encode(), shared_secret, hash=hashlib.sha512) session_key = hkdf_inst.expand('Pair-Verify-Encrypt-Info'.encode(), 32) # 3) verify authtag on encrypted data and 4) decrypt encrypted = response_tlv[TLV.kTLVType_EncryptedData] decrypted = chacha20_aead_decrypt(bytes(), session_key, 'PV-Msg02'.encode(), bytes([0, 0, 0, 0]), encrypted) if decrypted == False: raise homekit.exception.InvalidAuth("step 3") d1 = TLV.decode_bytes(decrypted) assert TLV.kTLVType_Identifier in d1 assert TLV.kTLVType_Signature in d1 # 5) look up pairing by accessory name accessory_name = d1[TLV.kTLVType_Identifier].decode() if pairing_data['AccessoryPairingID'] != accessory_name: raise homekit.exception.IncorrectPairingID("step 3") accessory_ltpk = py25519.Key25519(pubkey=bytes(), verifyingkey=bytes.fromhex( pairing_data['AccessoryLTPK'])) # 6) verify accessory's signature accessory_sig = d1[TLV.kTLVType_Signature] accessory_session_pub_key_bytes = response_tlv[TLV.kTLVType_PublicKey] accessory_info = accessory_session_pub_key_bytes + accessory_name.encode( ) + ios_key.pubkey if not accessory_ltpk.verify(bytes(accessory_sig), bytes(accessory_info)): raise homekit.exception.InvalidSignature("step 3") # 7) create iOSDeviceInfo ios_device_info = ios_key.pubkey + pairing_data['iOSPairingId'].encode( ) + accessorys_session_pub_key_bytes # 8) sign iOSDeviceInfo with long term secret key ios_device_ltsk_h = pairing_data['iOSDeviceLTSK'] ios_device_ltsk = py25519.Key25519( secretkey=bytes.fromhex(ios_device_ltsk_h)) ios_device_signature = ios_device_ltsk.sign(ios_device_info) # 9) construct sub tlv sub_tlv = TLV.encode_list([(TLV.kTLVType_Identifier, pairing_data['iOSPairingId'].encode()), (TLV.kTLVType_Signature, ios_device_signature)]) # 10) encrypt and sign encrypted_data_with_auth_tag = chacha20_aead_encrypt( bytes(), session_key, 'PV-Msg03'.encode(), bytes([0, 0, 0, 0]), sub_tlv) tmp = bytearray(encrypted_data_with_auth_tag[0]) tmp += encrypted_data_with_auth_tag[1] # 11) create tlv request_tlv = TLV.encode_list([(TLV.kTLVType_State, TLV.M3), (TLV.kTLVType_EncryptedData, tmp)]) # 12) send to accessory conn.request('POST', '/pair-verify', request_tlv, headers) resp = conn.getresponse() response_tlv = TLV.decode_bytes(resp.read()) # # Post Step #4 verification (page 51) # if TLV.kTLVType_Error in response_tlv: error_handler(response_tlv[TLV.kTLVType_Error], "verification") assert TLV.kTLVType_State in response_tlv assert response_tlv[TLV.kTLVType_State] == TLV.M4 # calculate session keys hkdf_inst = hkdf.Hkdf('Control-Salt'.encode(), shared_secret, hash=hashlib.sha512) controller_to_accessory_key = hkdf_inst.expand( 'Control-Write-Encryption-Key'.encode(), 32) hkdf_inst = hkdf.Hkdf('Control-Salt'.encode(), shared_secret, hash=hashlib.sha512) accessory_to_controller_key = hkdf_inst.expand( 'Control-Read-Encryption-Key'.encode(), 32) return controller_to_accessory_key, accessory_to_controller_key