def add_pairing(self, additional_controller_pairing_identifier, ios_device_ltpk, permissions): if not self.session: self.session = BleSession(self.pairing_data, self.adapter) if permissions == 'User': permissions = TlvTypes.Permission_RegularUser elif permissions == 'Admin': permissions = TlvTypes.Permission_AdminUser else: print('UNKNOWN') request_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.AddPairing), tlv8.Entry(TlvTypes.Identifier, additional_controller_pairing_identifier.encode()), tlv8.Entry(TlvTypes.PublicKey, bytes.fromhex(ios_device_ltpk)), tlv8.Entry(TlvTypes.Permissions, permissions) ]) request_tlv = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, request_tlv) ]) body = len(request_tlv).to_bytes(length=2, byteorder='little') + request_tlv cid = -1 for a in self.pairing_data['accessories']: for s in a['services']: for c in s['characteristics']: if CharacteristicsTypes.get_short_uuid(c['type'].upper()) == CharacteristicsTypes.PAIRING_PAIRINGS: cid = c['iid'] fc, _ = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) # TODO handle response properly print('unhandled response:', response)
def do_char_write(self, tid, value): """The value is actually a TLV with a command to perform""" request = { entry.type_id: entry.data for entry in tlv8.decode( value, { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Method: tlv8.DataType.INTEGER, TlvTypes.Identifier: tlv8.DataType.BYTES, }) } logging.debug('%s', request) assert request[TlvTypes.State] == States.M1 if request[TlvTypes.Method] == Methods.RemovePairing: ident = request[TlvTypes.Identifier].decode() self.service.device.peers.pop(ident, None) # If ident == this session then disconnect it # self.service.device.disconnect() response = bytearray([0x02, tid, 0x00]) inner = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M2), ]) outer = tlv8.encode( [tlv8.Entry(AdditionalParameterTypes.Value, inner)]) response.extend(len(outer).to_bytes(length=2, byteorder='little')) response.extend(outer) self.queue_read_response(self.encrypt_value(bytes(response)))
def _send_response_tlv(self, d_res, close=False, status=None): result_bytes = tlv8.encode(d_res) outer = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.Value, result_bytes), ]) self.value += b'\x00' + len(outer).to_bytes(length=2, byteorder='little') + outer
def write(request, expected): # TODO document me body = tlv8.encode(request) logger.debug('entering write function %s', tlv8.format_string(tlv8.decode(body))) request_tlv = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, body) ]) transaction_id = random.randrange(0, 255) # construct a hap characteristic write request following chapter 7.3.4.4 page 94 spec R2 data = bytearray([0x00, HapBleOpCodes.CHAR_WRITE, transaction_id]) data.extend(characteristic_id.to_bytes(length=2, byteorder='little')) data.extend(len(request_tlv).to_bytes(length=2, byteorder='little')) data.extend(request_tlv) logger.debug('sent %s', bytes(data).hex()) # write the request to the characteristic characteristic.write_value(value=data) # reading hap characteristic write response following chapter 7.3.4.5 page 95 spec R2 data = [] while len(data) == 0: time.sleep(1) logger.debug('reading characteristic') data = characteristic.read_value() resp_data = [b for b in data] expected_length = int.from_bytes(bytes(resp_data[3:5]), byteorder='little') logger.debug( 'control field: {c:x}, tid: {t:x}, status: {s:x}, length: {length}' .format(c=resp_data[0], t=resp_data[1], s=resp_data[2], length=expected_length)) while len(resp_data[3:]) < expected_length: time.sleep(1) logger.debug('reading characteristic') data = characteristic.read_value() resp_data.extend([b for b in data]) logger.debug('data %s of %s', len(resp_data[3:]), expected_length) logger.debug('received %s', bytes(resp_data).hex()) logger.debug('decode %s', bytes(resp_data[5:]).hex()) resp_tlv = tlv8.decode( bytes([int(a) for a in resp_data[5:]]), expected={AdditionalParameterTypes.Value: tlv8.DataType.BYTES}) result = tlv8.decode( resp_tlv.first_by_id(AdditionalParameterTypes.Value).data, expected) logger.debug('leaving write function %s', tlv8.format_string(result)) return result
def add_pairing(self, additional_controller_pairing_identifier, ios_device_ltpk, permissions): if not self.session: self.session = IpSession(self.pairing_data) if permissions == 'User': permissions = TlvTypes.Permission_RegularUser elif permissions == 'Admin': permissions = TlvTypes.Permission_AdminUser else: print('UNKNOWN') request_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.AddPairing), tlv8.Entry(TlvTypes.Identifier, additional_controller_pairing_identifier.encode()), tlv8.Entry(TlvTypes.PublicKey, bytes.fromhex(ios_device_ltpk)), tlv8.Entry(TlvTypes.Permissions, permissions) ]) response = self.session.sec_http.post('/pairings', request_tlv) data = response.read() data = tlv8.decode( data, { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.BYTES, }) # TODO handle the response properly self.session.close()
def test_encode_same_set_sep_type(self): data = [ tlv8.Entry(23, b'23', tlv8.DataType.BYTES), tlv8.Entry(23, '23', tlv8.DataType.STRING) ] result = tlv8.encode(data, 0) expected_data = data[0].encode() + b'\x00\x00' + data[1].encode() self.assertEqual(result, expected_data)
def test_encode_key(self): class TestKeys(enum.IntEnum): KEY_1 = 1 KEY_2 = 2 data = [tlv8.Entry(TestKeys.KEY_1, 'foo')] result = tlv8.encode(data) self.assertEqual(b'\x01\x03foo', result)
def test_encode_int_value(self): class TestValues(enum.IntEnum): VAL_1 = 1 VAL_2 = 2 data = [tlv8.Entry(1, TestValues.VAL_2)] result = tlv8.encode(data) self.assertEqual(b'\x01\x01\x02', result)
def test_encode_supported_rtp_configs(self): data = [tlv8.Entry(2, 0), tlv8.Entry(2, 1)] result = tlv8.encode(data, separator_type_id=0x00) expected_data = \ b'\x02\x01\x00' + \ b'\x00\x00' + \ b'\x02\x01\x01' self.assertEqual(result, expected_data)
def test_encode_different(self): data = [ tlv8.Entry(23, b'23', tlv8.DataType.BYTES), tlv8.Entry(22, '23', tlv8.DataType.STRING) ] result = tlv8.encode(data) expected_data = data[0].encode() + data[1].encode() self.assertEqual(result, expected_data)
def test_decode_misinterpretation(self): """This show how data may be misinterpreted by deep_decode""" data = tlv8.encode([tlv8.Entry(1, 16843330), tlv8.Entry(2, b'\x01')]) result = tlv8.deep_decode(data) expected_data = tlv8.EntryList([ tlv8.Entry(1, tlv8.EntryList([tlv8.Entry(66, b'\x01\x01')])), tlv8.Entry(2, b'\x01') ]) self.assertEqual(result, expected_data)
def list_pairings(self): if not self.session: self.session = BleSession(self.pairing_data, self.adapter) request_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.ListPairings) ]) request_tlv = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, request_tlv) ]) body = len(request_tlv).to_bytes(length=2, byteorder='little') + request_tlv cid = -1 for a in self.pairing_data['accessories']: for s in a['services']: for c in s['characteristics']: if CharacteristicsTypes.get_short_uuid(c['type'].upper( )) == CharacteristicsTypes.PAIRING_PAIRINGS: cid = c['iid'] fc, _ = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) response = tlv8.decode( response.first_by_id(AdditionalParameterTypes.Value).data) tmp = [] r = {} for d in response[1:]: if d.type_id == TlvTypes.Identifier: r = {} tmp.append(r) r['pairingId'] = d.data.decode() if d.type_id == TlvTypes.PublicKey: r['publicKey'] = d.data.hex() if d.type_id == TlvTypes.Permissions: controller_type = 'regular' if d.data == b'\x01': controller_type = 'admin' r['permissions'] = int.from_bytes(d.data, byteorder='little') r['controllerType'] = controller_type tmp.sort(key=lambda x: x['pairingId']) return tmp
def test_autodetection_of_types(self): data = [ tlv8.Entry(1, 3.141), tlv8.Entry(2, [tlv8.Entry(3, 'hello'), tlv8.Entry(4, 'world')]), tlv8.Entry(1, 2) ] result = tlv8.encode(data) expected_data = b'\x01\x04%\x06I@\x02\x0e\x03\x05hello\x04\x05world\x01\x01\x02' self.assertEqual(result, expected_data)
def identify_ble(accessory_mac, adapter='hci0'): """ This call can be used to trigger the identification of an accessory, that was not yet paired. A successful call should cause the accessory to perform some specific action by which it can be distinguished from others (blink a LED for example). It uses the /identify url as described on page 88 of the spec. :param accessory_mac: the accessory's mac address (e.g. retrieved via discover) :raises AccessoryNotFoundError: if the accessory could not be looked up via Bonjour :param adapter: the bluetooth adapter to be used (defaults to hci0) :raises AlreadyPairedError: if the accessory is already paired """ if not BLE_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('BLE') from .ble_impl.device import DeviceManager manager = DeviceManager(adapter) device = manager.make_device(accessory_mac) device.connect() disco_info = device.get_homekit_discovery_data() if disco_info.get('flags', 'unknown') == 'paired': raise AlreadyPairedError( 'identify of {mac_address} failed not allowed as device already paired' .format(mac_address=accessory_mac), ) identify, identify_iid = find_characteristic_by_uuid( device, ServicesTypes.ACCESSORY_INFORMATION_SERVICE, CharacteristicsTypes.IDENTIFY, ) if not identify: raise AccessoryNotFoundError( 'Device with address {mac_address} exists but did not find IDENTIFY characteristic' .format(mac_address=accessory_mac)) value = tlv8.encode( [tlv8.Entry(AdditionalParameterTypes.Value, b'\x01')]) body = len(value).to_bytes(length=2, byteorder='little') + value tid = random.randrange(0, 255) request = bytearray([0x00, HapBleOpCodes.CHAR_WRITE, tid]) request.extend(identify_iid.to_bytes(length=2, byteorder='little')) request.extend(body) identify.write_value(request) response = bytearray(identify.read_value()) if not response or not response[2] == 0x00: raise UnknownError('Unpaired identify failed') return True
def write_http(request, expected): body = tlv8.encode(request) logging.debug('write message: %s', tlv8.format_string(tlv8.deep_decode(body))) connection.putrequest('POST', '/pair-setup', skip_accept_encoding=True) connection.putheader('Content-Type', 'application/pairing+tlv8') connection.putheader('Content-Length', len(body)) connection.endheaders(body) resp = connection.getresponse() response_tlv = tlv8.decode(resp.read(), expected) logging.debug('response: %s', tlv8.format_string(response_tlv)) return response_tlv
def test_encode_3same(self): data = [ tlv8.Entry(23, b'23', tlv8.DataType.BYTES), tlv8.Entry(23, '23', tlv8.DataType.STRING), tlv8.Entry(23, '23', tlv8.DataType.STRING) ] result = tlv8.encode(data) expected_data = \ data[0].encode() + b'\xff\x00' + \ data[1].encode() + b'\xff\x00' + \ data[2].encode() self.assertEqual(result, expected_data)
def put_characteristics(self, characteristics, do_conversion=False): """ Update the values of writable characteristics. The characteristics have to be identified by accessory id (aid), instance id (iid). If do_conversion is False (the default), the value must be of proper format for the characteristic since no conversion is done. If do_conversion is True, the value is converted. :param characteristics: a list of 3-tupels of accessory id, instance id and the value :param do_conversion: select if conversion is done (False is default) :return: a dict from (aid, iid) onto {status, description} :raises FormatError: if the input value could not be converted to the target type and conversion was requested """ if not self.session: self.session = BleSession(self.pairing_data, self.adapter) results = {} for aid, cid, value in characteristics: # reply with an error if the characteristic does not exist if not self._find_characteristic_in_pairing_data(aid, cid): results[(aid, cid)] = { 'status': HapBleStatusCodes.INVALID_REQUEST, 'description': HapBleStatusCodes[HapBleStatusCodes.INVALID_REQUEST] } continue value = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.Value, self._convert_from_python(aid, cid, value)) ]) body = len(value).to_bytes(length=2, byteorder='little') + value try: fc, fc_info = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) logger.debug('response %s', tlv8.format_string(response)) # TODO does the response contain useful information here? except RequestRejected as e: results[(aid, cid)] = { 'status': e.status, 'description': e.message, } except Exception as e: self.session.close() self.session = None raise e return results
def buildTLV8(serverCert, verifyKey, rootCA): structure = [ tlv8.Entry(HAP_KEYSTORE_TYPE.HAP_KEYSTORE_TYPE_CONTAINER_ID.value, CONTAINER_VERSION), tlv8.Entry(HAP_KEYSTORE_TYPE.HAP_KEYSTORE_TYPE_ROOT_CA.value, readBinaryFile(rootCA)), tlv8.Entry( HAP_KEYSTORE_TYPE.HAP_KEYSTORE_TYPE_ROOT_CA_PUBLIC_KEY_SIGNATURE. value, readBinaryFile(verifyKey)), tlv8.Entry( HAP_KEYSTORE_TYPE.HAP_KEYSTORE_TYPE_DEVICE_WEBSERVER_CERT.value, readBinaryFile(serverCert)) #, #tlv8.Entry( HAP_KEYSTORE_TYPE.HAP_KEYSTORE_TYPE_DEVICE_PUBLIC_KEY.value, readBinaryFile("./certs/devices/esp32-cafeec/esp32-cafeec.publicKey.cer") ) ] bytes_data = tlv8.encode(structure) #print(bytes_data) return bytes_data, structure
def list_pairings(self): """ This method returns all pairings of a HomeKit accessory. This always includes the local controller and can only be done by an admin controller. The keys in the resulting dicts are: * pairingId: the pairing id of the controller * publicKey: the ED25519 long-term public key of the controller * permissions: bit value for the permissions * controllerType: either admin or regular :return: a list of dicts :raises: UnknownError: if it receives unexpected data :raises: UnpairedError: if the polled accessory is not paired """ if not self.session: self.session = IpSession(self.pairing_data) request_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.ListPairings) ]) try: response = self.session.sec_http.post('/pairings', request_tlv) data = response.read() except (AccessoryDisconnectedError, EncryptionError): self.session.close() self.session = None raise data = tlv8.decode( data, { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.INTEGER, TlvTypes.Identifier: tlv8.DataType.BYTES, TlvTypes.PublicKey: tlv8.DataType.BYTES, TlvTypes.Permissions: tlv8.DataType.BYTES }) error = data.first_by_id(TlvTypes.Error) if not (data.first_by_id(TlvTypes.State).data == States.M2): raise UnknownError('unexpected data received: ' + tlv8.format_string(data)) elif error and error.data == Errors.Authentication: raise UnpairedError('Must be paired') else: tmp = [] r = {} for d in data[1:]: if d.type_id == TlvTypes.Identifier: r = {} tmp.append(r) r['pairingId'] = d.data.decode() if d.type_id == TlvTypes.PublicKey: r['publicKey'] = d.data.hex() if d.type_id == TlvTypes.Permissions: controller_type = 'regular' if d.data == b'\x01': controller_type = 'admin' r['permissions'] = int.from_bytes(d.data, byteorder='little') r['controllerType'] = controller_type tmp.sort(key=lambda x: x['pairingId']) return tmp
def test_encode_64bit_int(self): result = tlv8.encode([tlv8.Entry(1, 4611686018427387904)]) self.assertEqual(b'\x01\x08\x00\x00\x00\x00\x00\x00\x00@', result)
def test_encode_32bit_int(self): result = tlv8.encode([tlv8.Entry(1, 1073741824)]) self.assertEqual(b'\x01\x04\x00\x00\x00@', result)
def test_encode_same_autodetect(self): structure = [tlv8.Entry(1, 23), tlv8.Entry(2, 2345)] result = tlv8.encode(structure) expected_data = b'\x01\x01\x17\x02\x02)\t' self.assertEqual(result, expected_data)
def get_session_keys(pairing_data): """ HomeKit Controller state machine to perform a pair verify operation as described in chapter 4.8 page 47 ff. :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) :raises InvalidAuthTagError: if the auth tag could not be verified, :raises IncorrectPairingIdError: if the accessory's LTPK could not be found :raises InvalidSignatureError: if the accessory's signature could not be verified :raises AuthenticationError: if the secured session could not be established """ # # Step #1 ios --> accessory (send verify start Request) (page 47) # ios_key = x25519.X25519PrivateKey.generate() ios_key_pub = ios_key.public_key().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw) request_tlv = [ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.PublicKey, ios_key_pub) ] step2_expectations = { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.PublicKey: tlv8.DataType.BYTES, TlvTypes.EncryptedData: tlv8.DataType.BYTES } response_tlv = yield (request_tlv, step2_expectations) # # Step #3 ios --> accessory (send SRP verify request) (page 49) # state = response_tlv.first_by_id(TlvTypes.State).data assert state == States.M2, 'get_session_keys: not M2' assert response_tlv.first_by_id( TlvTypes.PublicKey), 'get_session_keys: no public key' assert response_tlv.first_by_id( TlvTypes.EncryptedData), 'get_session_keys: no encrypted data' # 1) generate shared secret accessorys_session_pub_key_bytes = bytes( response_tlv.first_by_id(TlvTypes.PublicKey).data) accessorys_session_pub_key = x25519.X25519PublicKey.from_public_bytes( accessorys_session_pub_key_bytes) shared_secret = ios_key.exchange(accessorys_session_pub_key) # 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 auth tag on encrypted data and 4) decrypt encrypted = response_tlv.first_by_id(TlvTypes.EncryptedData).data decrypted = chacha20_aead_decrypt(bytes(), session_key, 'PV-Msg02'.encode(), bytes([0, 0, 0, 0]), encrypted) if type(decrypted) == bool and not decrypted: raise InvalidAuthTagError('step 3') d1 = tlv8.decode(decrypted) assert d1.first_by_id( TlvTypes.Identifier), 'get_session_keys: no identifier' assert d1.first_by_id(TlvTypes.Signature), 'get_session_keys: no signature' # 5) look up pairing by accessory name accessory_name = d1.first_by_id(TlvTypes.Identifier).data.decode() if pairing_data['AccessoryPairingID'] != accessory_name: raise IncorrectPairingIdError('step 3') accessory_ltpk = ed25519.VerifyingKey( bytes.fromhex(pairing_data['AccessoryLTPK'])) # 6) verify accessory's signature accessory_sig = d1.first_by_id(TlvTypes.Signature).data accessory_session_pub_key_bytes = response_tlv.first_by_id( TlvTypes.PublicKey).data accessory_info = accessory_session_pub_key_bytes + accessory_name.encode( ) + ios_key_pub try: accessory_ltpk.verify(bytes(accessory_sig), bytes(accessory_info)) except ed25519.BadSignatureError: raise InvalidSignatureError('step 3') # 7) create iOSDeviceInfo ios_device_info = ios_key_pub + 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_ltpk_h = pairing_data['iOSDeviceLTPK'] ios_device_ltsk = ed25519.SigningKey( bytes.fromhex(ios_device_ltsk_h) + bytes.fromhex(ios_device_ltpk_h)) ios_device_signature = ios_device_ltsk.sign(ios_device_info) # 9) construct sub tlv sub_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.Identifier, pairing_data['iOSPairingId'].encode()), tlv8.Entry(TlvTypes.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 = [ tlv8.Entry(TlvTypes.State, States.M3), tlv8.Entry(TlvTypes.EncryptedData, tmp) ] step3_expectations = { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.INTEGER } response_tlv = yield (request_tlv, step3_expectations) # # Post Step #4 verification (page 51) # state = response_tlv.first_by_id(TlvTypes.State).data assert state == States.M4, 'get_session_keys: not M4' error = response_tlv.first_by_id(TlvTypes.Error) if len(response_tlv) == 2 and error: error_handler(error.data, 'verification') # 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_part2(pin, ios_pairing_id, salt, server_public_key): """ Performs a pair setup operation as described in chapter 4.7 page 39 ff. :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 :raises UnavailableError: if the device is already paired :raises MaxTriesError: if the device received more than 100 unsuccessful pairing attempts :raises BusyError: if a parallel pairing is ongoing :raises AuthenticationError: if the verification of the device's SRP proof fails :raises MaxPeersError: if the device cannot accept an additional pairing :raises IllegalData: if the verification of the accessory's data fails """ srp_client = SrpClient('Pair-Setup', pin) srp_client.set_salt(salt) srp_client.set_server_public_key(server_public_key) client_pub_key = srp_client.get_public_key() client_proof = srp_client.get_proof() response_tlv = [ tlv8.Entry(TlvTypes.State, States.M3), tlv8.Entry(TlvTypes.PublicKey, SrpClient.to_byte_array(client_pub_key)), tlv8.Entry(TlvTypes.Proof, SrpClient.to_byte_array(client_proof)), ] step4_expectations = { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.INTEGER, TlvTypes.Proof: tlv8.DataType.BYTES } response_tlv = yield (response_tlv, step4_expectations) # # Step #5 ios --> accessory (Exchange Request) (see page 43) # logging.debug('#5 ios -> accessory: send SRP exchange request') # M4 Verification (page 43) assert response_tlv.first_by_id( TlvTypes.State) and response_tlv.first_by_id( TlvTypes.State ).data == States.M4, 'perform_pair_setup: State not M4' error = response_tlv.first_by_id(TlvTypes.Error) if error: error_handler(error.data, 'step 5') assert response_tlv.first_by_id( TlvTypes.Proof), 'perform_pair_setup: Not a proof' if not srp_client.verify_servers_proof( response_tlv.first_by_id(TlvTypes.Proof).data): raise AuthenticationError('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 = [ tlv8.Entry(TlvTypes.Identifier, ios_device_pairing_id), tlv8.Entry(TlvTypes.PublicKey, ios_device_ltpk.to_bytes()), tlv8.Entry(TlvTypes.Signature, ios_device_signature) ] sub_tlv_b = tlv8.encode(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 = [ tlv8.Entry(TlvTypes.State, States.M5), tlv8.Entry(TlvTypes.EncryptedData, tmp) ] step6_expectations = { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.INTEGER, TlvTypes.EncryptedData: tlv8.DataType.BYTES } response_tlv = yield (response_tlv, step6_expectations) # # Step #7 ios (Verification) (page 47) # assert response_tlv.first_by_id( TlvTypes.State) and response_tlv.first_by_id( TlvTypes.State ).data == States.M6, 'perform_pair_setup: State not M6' error = response_tlv.first_by_id(TlvTypes.Error) if error: error_handler(error.data, 'step 7') assert response_tlv.first_by_id( TlvTypes.EncryptedData), 'perform_pair_setup: No encrypted data' decrypted_data = chacha20_aead_decrypt( bytes(), session_key, 'PS-Msg06'.encode(), bytes([0, 0, 0, 0]), response_tlv.first_by_id(TlvTypes.EncryptedData).data) if decrypted_data is False: raise homekit.exception.IllegalData('step 7') response_tlv = tlv8.decode(decrypted_data) assert response_tlv.first_by_id( TlvTypes.Identifier), 'perform_pair_setup: No identifier' accessory_pairing_id = response_tlv.first_by_id(TlvTypes.Identifier).data assert response_tlv.first_by_id( TlvTypes.PublicKey), 'perform_pair_setup: No public key' accessory_ltpk = response_tlv.first_by_id(TlvTypes.PublicKey).data assert response_tlv.first_by_id( TlvTypes.Signature), 'perform_pair_setup: No signature' accessory_sig = response_tlv.first_by_id(TlvTypes.Signature).data 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(accessory_ltpk)) try: e25519s.verify(bytes(accessory_sig), bytes(accessory_info)) except AssertionError: raise InvalidSignatureError('step #7') return { 'AccessoryPairingID': accessory_pairing_id.decode(), 'AccessoryLTPK': hexlify(accessory_ltpk).decode(), 'iOSPairingId': ios_pairing_id, 'iOSDeviceLTSK': ios_device_ltsk.to_ascii(encoding='hex').decode()[:64], 'iOSDeviceLTPK': hexlify(ios_device_ltpk.to_bytes()).decode() }
def test_encode_8bit_signed_int(self): result = tlv8.encode([tlv8.Entry(1, -64, tlv8.DataType.INTEGER)]) self.assertEqual(b'\x01\x01\xc0', result)
def test_encode_8bit_unsigned_int(self): result = tlv8.encode( [tlv8.Entry(1, 64, tlv8.DataType.UNSIGNED_INTEGER)]) self.assertEqual(b'\x01\x01@', result)
def test_encode_bytearray_autodetect(self): result = tlv8.encode([tlv8.Entry(1, bytearray(b'\x01'))]) self.assertEqual(b'\x01\x01\x01', result)
def test_encode_16bit_int(self): result = tlv8.encode([tlv8.Entry(1, 16384)]) self.assertEqual(b'\x01\x02\x00@', result)
def process_value(self, value): assert value[0] == 0 opcode = value[1] tid = value[2] payload = value[7:] if opcode == HapBleOpCodes.CHAR_WRITE: new_value = { entry.type_id: entry.data for entry in tlv8.decode(payload) } self.do_char_write(tid, new_value[1]) elif opcode == HapBleOpCodes.CHAR_READ: value = self.char.get_value_for_ble() value = tlv8.encode( [tlv8.Entry(AdditionalParameterTypes.Value, value)]) response = bytearray([0x02, tid, 0x00]) tlv = len(value).to_bytes(2, byteorder='little') + value response.extend(tlv) self.queue_read_response(self.encrypt_value(bytes(response))) elif opcode == HapBleOpCodes.CHAR_SIG_READ: response = bytearray([0x02, tid, 0x00]) service_type = list(uuid.UUID(self.service.service.type).bytes) service_type.reverse() service_type = bytes(bytearray(service_type)) char_type = list(uuid.UUID(self.char.type).bytes) char_type.reverse() char_type = bytes(bytearray(char_type)) fmt = BleCharacteristicFormats.get_reverse( self.char.format, b'\x00').to_bytes(length=1, byteorder='little') unit = b'\x00\x00' gatt_fmt = fmt + unit data = [ tlv8.Entry( AdditionalParameterTypes. HAPCharacteristicPropertiesDescriptor, b'\x00'), tlv8.Entry( AdditionalParameterTypes.GATTPresentationFormatDescriptor, gatt_fmt), tlv8.Entry(AdditionalParameterTypes.CharacteristicType, char_type), tlv8.Entry( AdditionalParameterTypes.ServiceInstanceId, self.service.service.iid.to_bytes(length=8, byteorder='little')), tlv8.Entry(AdditionalParameterTypes.ServiceType, service_type), ] tlv = tlv8.encode(data) response.extend(len(tlv).to_bytes(2, byteorder='little') + tlv) self.queue_read_response(self.encrypt_value(bytes(response))) else: raise RuntimeError('Fake does not implement opcode %s' % opcode)
def test_encode_bytearray(self): result = tlv8.encode( [tlv8.Entry(1, bytearray(b'\x01'), tlv8.DataType.BYTES)]) self.assertEqual(b'\x01\x01\x01', result)