def _post_pairings(self): d_req = TLV.decode_bytes(self.body) self.log_message('POST /pairings request body:\n%s', TLV.to_string(d_req)) session = self.server.sessions[self.session_id] server_data = self.server.data d_res = {} if d_req[TLV.kTLVType_State] == TLV.M1 and d_req[TLV.kTLVType_Method] == TLV.AddPairing: self.log_message('Step #2 /pairings add pairing') # see page 51 self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED) return if d_req[TLV.kTLVType_State] == TLV.M1 and d_req[TLV.kTLVType_Method] == TLV.RemovePairing: # step #2 Accessory -> iOS Device remove pairing response self.log_message('Step #2 /pairings remove pairings') # 1) # 2) verify set admin bit ios_device_pairing_id = session['ios_device_pairing_id'] if not server_data.is_peer_admin(ios_device_pairing_id): self.send_error_reply(TLV.M2, TLV.kTLVError_Authentication) print('error in step #2: admin bit') return # 3) remove pairing and republish device server_data.remove_peer(d_req[TLV.kTLVType_Identifier]) self.server.publish_device() d_res[TLV.kTLVType_State] = TLV.M2 self._send_response_tlv(d_res) self.log_message('after step #2\n%s', TLV.to_string(d_res)) return if d_req[TLV.kTLVType_State] == TLV.M1 and d_req[TLV.kTLVType_Method] == TLV.ListPairings: # step #2 Accessory -> iOS Device list pairing response self.log_message('Step #2 /pairings list pairings') # 1) Validate against session # 2) verify set admin bit ios_device_pairing_id = session['ios_device_pairing_id'] if not server_data.is_peer_admin(ios_device_pairing_id): self.send_error_reply(TLV.M2, TLV.kTLVError_Authentication) print('error in step #2: admin bit') return # 3) construct response TLV tmp = [(TLV.kTLVType_State, TLV.M2)] for index, pairing_id in enumerate(server_data.peers): tmp.append((TLV.kTLVType_Identifier, pairing_id.encode())) tmp.append((TLV.kTLVType_PublicKey, server_data.get_peer_key(pairing_id.encode()))) user = TLV.kTLVType_Permission_RegularUser if server_data.is_peer_admin(pairing_id.encode()): user = TLV.kTLVType_Permission_AdminUser tmp.append((TLV.kTLVType_Permissions, user)) if index + 1 < len(server_data.peers): tmp.append((TLV.kTLVType_Separator, bytes(0))) result_bytes = TLV.encode_list(tmp) # 4) send response self.send_response(HttpStatusCodes.OK) # Send headers self.send_header('Content-Length', len(result_bytes)) self.send_header('Content-Type', 'application/pairing+tlv8') self.send_header('Connection', 'keep-alive') self.end_headers() self.wfile.write(result_bytes) return self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED)
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
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 _post_pairings(self): d_req = TLV.decode_bytes(self.body) self.log_message('POST /pairings request body:\n%s', TLV.to_string(d_req)) session = self.server.sessions[self.session_id] server_data = self.server.data d_res = {} if d_req[TLV.kTLVType_State] == TLV.M1 and d_req[ TLV.kTLVType_Method] == TLV.AddPairing: self.log_message('Step #2 /pairings add pairing') d_res[TLV.kTLVType_State] = TLV.M2 # see page 51 # 1) # 2) verify admin bit is set ios_device_pairing_id = session['ios_device_pairing_id'] if not server_data.is_peer_admin(ios_device_pairing_id): self.send_error_reply(TLV.M2, TLV.kTLVError_Authentication) self.log_error('error in step #2: admin bit') return additional_controller_pairing_identifier = d_req[ TLV.kTLVType_Identifier] additional_controller_LTPK = d_req[TLV.kTLVType_PublicKey] additional_controller_permissions = d_req[TLV.kTLVType_Permissions] is_admin = additional_controller_permissions == b'\x01' # 3) pairing exists? registered_controller_LTPK = server_data.get_peer_key( additional_controller_pairing_identifier) if registered_controller_LTPK is not None: self.log_message('controller was registered!') if registered_controller_LTPK != additional_controller_LTPK: self.log_message('with different key') # 3.a) d_res[TLV.kTLVType_Error] = TLV.kTLVError_Authentication self._send_response_tlv(d_res) else: self.log_message('with different permissions') # 3.b) update permission server_data.set_peer_permissions( additional_controller_pairing_identifier, is_admin) else: self.log_message('add pairing') # 4) no pairing exists # 4.a) no limit applied to number of pairings # 4.b) add pairing server_data.add_peer(additional_controller_pairing_identifier, additional_controller_LTPK, is_admin) self.log_message('after step #2\n%s', TLV.to_string(d_res)) return if d_req[TLV.kTLVType_State] == TLV.M1 and d_req[ TLV.kTLVType_Method] == TLV.RemovePairing: # step #2 Accessory -> iOS Device remove pairing response self.log_message('Step #2 /pairings remove pairings') # 1) # 2) verify set admin bit ios_device_pairing_id = session['ios_device_pairing_id'] if not server_data.is_peer_admin(ios_device_pairing_id): self.send_error_reply(TLV.M2, TLV.kTLVError_Authentication) print('error in step #2: admin bit') return # 3) remove pairing and republish device server_data.remove_peer(d_req[TLV.kTLVType_Identifier]) self.server.publish_device() d_res[TLV.kTLVType_State] = TLV.M2 self._send_response_tlv(d_res) self.log_message('after step #2\n%s', TLV.to_string(d_res)) return if d_req[TLV.kTLVType_State] == TLV.M1 and d_req[ TLV.kTLVType_Method] == TLV.ListPairings: # step #2 Accessory -> iOS Device list pairing response self.log_message('Step #2 /pairings list pairings') # 1) Validate against session # 2) verify set admin bit ios_device_pairing_id = session['ios_device_pairing_id'] if not server_data.is_peer_admin(ios_device_pairing_id): self.send_error_reply(TLV.M2, TLV.kTLVError_Authentication) print('error in step #2: admin bit') return # 3) construct response TLV tmp = [(TLV.kTLVType_State, TLV.M2)] for index, pairing_id in enumerate(server_data.peers): tmp.append((TLV.kTLVType_Identifier, pairing_id.encode())) tmp.append((TLV.kTLVType_PublicKey, server_data.get_peer_key(pairing_id.encode()))) user = TLV.kTLVType_Permission_RegularUser if server_data.is_peer_admin(pairing_id.encode()): user = TLV.kTLVType_Permission_AdminUser tmp.append((TLV.kTLVType_Permissions, user)) if index + 1 < len(server_data.peers): tmp.append((TLV.kTLVType_Separator, bytes(0))) result_bytes = TLV.encode_list(tmp) # 4) send response self.send_response(HttpStatusCodes.OK) # Send headers self.send_header('Content-Length', len(result_bytes)) self.send_header('Content-Type', 'application/pairing+tlv8') self.send_header('Connection', 'keep-alive') self.end_headers() self.wfile.write(result_bytes) return self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED)