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 = TLV.encode_list([(1, 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 test_reorder_3(self): val = [ [TLV.kTLVType_State, TLV.M3], [TLV.kTLVType_Salt, (16 * 'a').encode()], [TLV.kTLVType_PublicKey, (384 * 'b').encode()], ] tmp = TLV.reorder( val, [TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_Salt]) self.assertEqual(tmp[0][0], TLV.kTLVType_State) self.assertEqual(tmp[0][1], TLV.M3) self.assertEqual(tmp[1][0], TLV.kTLVType_Salt) self.assertEqual(tmp[1][1], (16 * 'a').encode())
def check_convert_value(val, target_type): """ Checks if the given value is of the given type or is convertible into the type. If the value is not convertible, a HomeKitTypeException is thrown. :param val: the original value :param target_type: the target type of the conversion :return: the converted value :raises FormatError: if the input value could not be converted to the target type """ if target_type == CharacteristicFormats.bool: try: val = strtobool(str(val)) except ValueError: raise FormatError('"{v}" is no valid "{t}"!'.format(v=val, t=target_type)) if target_type in [CharacteristicFormats.uint64, CharacteristicFormats.uint32, CharacteristicFormats.uint16, CharacteristicFormats.uint8, CharacteristicFormats.int]: try: val = int(val) except ValueError: raise FormatError('"{v}" is no valid "{t}"!'.format(v=val, t=target_type)) if target_type == CharacteristicFormats.float: try: val = float(val) except ValueError: raise FormatError('"{v}" is no valid "{t}"!'.format(v=val, t=target_type)) if target_type == CharacteristicFormats.data: try: base64.decodebytes(val.encode()) except binascii.Error: raise FormatError('"{v}" is no valid "{t}"!'.format(v=val, t=target_type)) if target_type == CharacteristicFormats.tlv8: try: tmp_bytes = base64.decodebytes(val.encode()) TLV.decode_bytes(tmp_bytes) except (binascii.Error, TlvParseException): raise FormatError('"{v}" is no valid "{t}"!'.format(v=val, t=target_type)) return val
def list_pairings(self): if not self.session: self.session = BleSession(self.pairing_data, self.adapter) request_tlv = TLV.encode_list([(TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.ListPairings) ]) request_tlv = TLV.encode_list([(TLV.kTLVHAPParamParamReturnResponse, bytearray(b'\x01')), (TLV.kTLVHAPParamValue, 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 = TLV.decode_bytes(response[1]) tmp = [] r = {} for d in response[1:]: if d[0] == TLV.kTLVType_Identifier: r = {} tmp.append(r) r['pairingId'] = d[1].decode() if d[0] == TLV.kTLVType_PublicKey: r['publicKey'] = d[1].hex() if d[0] == TLV.kTLVType_Permissions: controller_type = 'regular' if d[1] == b'\x01': controller_type = 'admin' r['permissions'] = int.from_bytes(d[1], byteorder='little') r['controllerType'] = controller_type return tmp
def test_to_string_for_list(self): example = [ ( 1, 'hello', ), ] res = TLV.to_string(example) self.assertEqual(res, '[\n 1: (5 bytes) hello\n]\n') example = [ ( 1, 'hello', ), ( 2, 'world', ), ] res = TLV.to_string(example) self.assertEqual(res, '[\n 1: (5 bytes) hello\n 2: (5 bytes) world\n]\n')
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 = TLV.kTLVType_Permission_RegularUser elif permissions == 'Admin': permissions = TLV.kTLVType_Permission_AdminUser else: print('UNKNOWN') request_tlv = TLV.encode_list([ (TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.AddPairing), (TLV.kTLVType_Identifier, additional_controller_pairing_identifier.encode()), (TLV.kTLVType_PublicKey, bytes.fromhex(ios_device_ltpk)), (TLV.kTLVType_Permissions, permissions) ]) request_tlv = TLV.encode_list([(TLV.kTLVHAPParamParamReturnResponse, bytearray(b'\x01')), (TLV.kTLVHAPParamValue, 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 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 = TLV.kTLVType_Permission_RegularUser elif permissions == 'Admin': permissions = TLV.kTLVType_Permission_AdminUser else: print('UNKNOWN') request_tlv = TLV.encode_list([ (TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.AddPairing), (TLV.kTLVType_Identifier, additional_controller_pairing_identifier.encode()), (TLV.kTLVType_PublicKey, bytes.fromhex(ios_device_ltpk)), (TLV.kTLVType_Permissions, permissions) ]) response = self.session.sec_http.post('/pairings', request_tlv) data = response.read() data = TLV.decode_bytes(data) # TODO handle the response properly self.session.close()
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 = TLV.encode_list([ (1, 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', 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 remove_pairing(self, alias): """ Remove a pairing between the controller and the accessory. The pairing data is delete on both ends, on the accessory and the controller. Important: no automatic saving of the pairing data is performed. If you don't do this, the accessory seems still to be paired on the next start of the application. :param alias: the controller's alias for the accessory :raises AuthenticationError: if the controller isn't authenticated to the accessory. :raises AccessoryNotFoundError: if the device can not be found via zeroconf :raises UnknownError: on unknown errors """ # package visibility like in java would be nice here pairing_data = self.pairings[alias]._get_pairing_data() request_tlv = TLV.encode_list([ (TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.RemovePairing), (TLV.kTLVType_Identifier, pairing_data['iOSPairingId'].encode()) ]).decode( ) # decode is required because post needs a string representation session = Session(pairing_data) response = session.post('/pairings', request_tlv) session.close() data = response.read() data = TLV.decode_bytes(data) # handle the result, spec says, if it has only one entry with state == M2 we unpaired, else its an error. if len(data) == 1 and data[0][0] == TLV.kTLVType_State and data[0][ 1] == TLV.M2: del self.pairings[alias] else: if data[TLV.kTLVType_Error] == TLV.kTLVError_Authentication: raise AuthenticationError( 'Remove pairing failed: missing authentication') else: raise UnknownError('Remove pairing failed: unknown error')
def perform_pair_setup_part1(): """ Performs a pair setup operation as described in chapter 4.7 page 39 ff. :return: a tuple of salt and server's public key :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 """ # # Step #1 ios --> accessory (send SRP start Request) (see page 39) # logging.debug('#1 ios -> accessory: send SRP start request') request_tlv = [(TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.PairSetup)] step2_expectations = [ TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_PublicKey, TLV.kTLVType_Salt ] response_tlv = yield (request_tlv, step2_expectations) # # Step #3 ios --> accessory (send SRP verify request) (see page 41) # logging.debug('#3 ios -> accessory: send SRP verify request') response_tlv = TLV.reorder(response_tlv, step2_expectations) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M2, 'perform_pair_setup: State not M2' # the errors here can be: # * kTLVError_Unavailable: Device is paired # * kTLVError_MaxTries: More than 100 unsuccessful attempts # * kTLVError_Busy: There is already a pairing going on if response_tlv[1][0] == TLV.kTLVType_Error: error_handler(response_tlv[1][1], 'step 3') assert response_tlv[1][ 0] == TLV.kTLVType_PublicKey, 'perform_pair_setup: Not a public key' assert response_tlv[2][ 0] == TLV.kTLVType_Salt, 'perform_pair_setup: Not a salt' return response_tlv[2][1], response_tlv[1][1]
def write(request, expected): # TODO document me body = TLV.encode_list(request) logger.debug('entering write function %s', TLV.to_string(TLV.decode_bytes(body))) request_tlv = TLV.encode_list([(TLV.kTLVHAPParamParamReturnResponse, bytearray(b'\x01')), (TLV.kTLVHAPParamValue, body)]) transaction_id = random.randrange(0, 255) 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()) characteristic.write_value(value=data) 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 = TLV.decode_bytes(bytes([int(a) for a in resp_data[5:]]), expected=[TLV.kTLVHAPParamValue]) result = TLV.decode_bytes(resp_tlv[0][1], expected) logger.debug('leaving write function %s', TLV.to_string(result)) return result
def test_to_string_for_list_bytearray(self): example = [[1, bytearray([0x42, 0x23])]] res = TLV.to_string(example) self.assertEqual(res, '[\n 1: (2 bytes) 0x4223\n]\n')
def test_to_string_for_dict_bytearray(self): example = {1: bytearray([0x42, 0x23])} res = TLV.to_string(example) self.assertEqual(res, '{\n 1: (2 bytes) 0x4223\n}\n')
def remove_pairing(self, alias, pairingId=None): """ Remove a pairing between the controller and the accessory. The pairing data is delete on both ends, on the accessory and the controller. Important: no automatic saving of the pairing data is performed. If you don't do this, the accessory seems still to be paired on the next start of the application. :param alias: the controller's alias for the accessory :param pairingId: the pairing id to be removed :raises AuthenticationError: if the controller isn't authenticated to the accessory. :raises AccessoryNotFoundError: if the device can not be found via zeroconf :raises UnknownError: on unknown errors """ # package visibility like in java would be nice here pairing_data = self.pairings[alias]._get_pairing_data() connection_type = pairing_data['Connection'] if not pairingId: pairingIdToDelete = pairing_data['iOSPairingId'] else: pairingIdToDelete = pairingId # Prepare the common (for IP and BLE) request data request_tlv = TLV.encode_list([ (TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.RemovePairing), (TLV.kTLVType_Identifier, pairingIdToDelete.encode()) ]) if connection_type == 'IP': if not IP_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('IP') session = IpSession(pairing_data) # decode is required because post needs a string representation response = session.post('/pairings', request_tlv) session.close() data = response.read() data = TLV.decode_bytes(data) elif connection_type == 'BLE': if not BLE_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('BLE') inner = TLV.encode_list([(TLV.kTLVHAPParamParamReturnResponse, bytearray(b'\x01')), (TLV.kTLVHAPParamValue, request_tlv)]) body = len(inner).to_bytes(length=2, byteorder='little') + inner from .ble_impl.device import DeviceManager manager = DeviceManager(self.ble_adapter) device = manager.make_device(pairing_data['AccessoryMAC']) device.connect() logging.debug('resolved %d services', len(device.services)) pair_remove_char, pair_remove_char_id = find_characteristic_by_uuid( device, ServicesTypes.PAIRING_SERVICE, CharacteristicsTypes.PAIRING_PAIRINGS) logging.debug('setup char: %s %s', pair_remove_char, pair_remove_char.service.device) session = BleSession(pairing_data, self.ble_adapter) response = session.request(pair_remove_char, pair_remove_char_id, HapBleOpCodes.CHAR_WRITE, body) data = TLV.decode_bytes(response[1]) else: raise Exception('not implemented (neither IP nor BLE)') # act upon the response (the same is returned for IP and BLE accessories) # handle the result, spec says, if it has only one entry with state == M2 we unpaired, else its an error. logging.debug('response data: %s', data) if len(data) == 1 and data[0][0] == TLV.kTLVType_State and data[0][ 1] == TLV.M2: if not pairingId: del self.pairings[alias] else: if data[1][0] == TLV.kTLVType_Error and data[1][ 1] == TLV.kTLVError_Authentication: raise AuthenticationError( 'Remove pairing failed: missing authentication') else: raise UnknownError('Remove pairing failed: unknown error')
def get_session_keys(conn, pairing_data): """ HomeKit Controller side call to perform a pair verify operation as described in chapter 4.8 page 47 ff. :param conn: the http_impl 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) :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 """ 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) # response_tlv = TLV.reorder(response_tlv, [ TLV.kTLVType_State, TLV.kTLVType_PublicKey, TLV.kTLVType_EncryptedData ]) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M2, 'get_session_keys: not M2' assert response_tlv[1][ 0] == TLV.kTLVType_PublicKey, 'get_session_keys: no public key' assert response_tlv[2][ 0] == TLV.kTLVType_EncryptedData, 'get_session_keys: no encrypted data' # 1) generate shared secret accessorys_session_pub_key_bytes = response_tlv[1][1] 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 auth tag on encrypted data and 4) decrypt encrypted = response_tlv[2][1] 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 = TLV.decode_bytes(decrypted) d1 = TLV.reorder(d1, [TLV.kTLVType_Identifier, TLV.kTLVType_Signature]) assert d1[0][ 0] == TLV.kTLVType_Identifier, 'get_session_keys: no identifier' assert d1[1][0] == TLV.kTLVType_Signature, 'get_session_keys: no signature' # 5) look up pairing by accessory name accessory_name = d1[0][1].decode() if pairing_data['AccessoryPairingID'] != accessory_name: raise IncorrectPairingIdError('step 3') accessory_ltpk = py25519.Key25519(pubkey=bytes(), verifyingkey=bytes.fromhex( pairing_data['AccessoryLTPK'])) # 6) verify accessory's signature accessory_sig = d1[1][1] accessory_session_pub_key_bytes = response_tlv[1][1] 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 InvalidSignatureError('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) # response_tlv = TLV.reorder(response_tlv, [TLV.kTLVType_State, TLV.kTLVType_Error]) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M4, 'get_session_keys: not M4' if len(response_tlv) == 2 and response_tlv[1][0] == TLV.kTLVType_Error: error_handler(response_tlv[1][1], '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 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 = [(TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_PublicKey, ios_key_pub)] step2_expectations = [ TLV.kTLVType_State, TLV.kTLVType_PublicKey, TLV.kTLVType_EncryptedData ] response_tlv = yield (request_tlv, step2_expectations) # # Step #3 ios --> accessory (send SRP verify request) (page 49) # response_tlv = TLV.reorder(response_tlv, step2_expectations) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M2, 'get_session_keys: not M2' assert response_tlv[1][ 0] == TLV.kTLVType_PublicKey, 'get_session_keys: no public key' assert response_tlv[2][ 0] == TLV.kTLVType_EncryptedData, 'get_session_keys: no encrypted data' # 1) generate shared secret accessorys_session_pub_key_bytes = bytes(response_tlv[1][1]) 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[2][1] 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 = TLV.decode_bytes(decrypted) d1 = TLV.reorder(d1, [TLV.kTLVType_Identifier, TLV.kTLVType_Signature]) assert d1[0][ 0] == TLV.kTLVType_Identifier, 'get_session_keys: no identifier' assert d1[1][0] == TLV.kTLVType_Signature, 'get_session_keys: no signature' # 5) look up pairing by accessory name accessory_name = d1[0][1].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[1][1] accessory_session_pub_key_bytes = response_tlv[1][1] 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 = 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.kTLVType_State, TLV.M3), (TLV.kTLVType_EncryptedData, tmp)] step3_expectations = [TLV.kTLVType_State, TLV.kTLVType_Error] response_tlv = yield (request_tlv, step3_expectations) # # Post Step #4 verification (page 51) # response_tlv = TLV.reorder(response_tlv, step3_expectations) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M4, 'get_session_keys: not M4' if len(response_tlv) == 2 and response_tlv[1][0] == TLV.kTLVType_Error: error_handler(response_tlv[1][1], '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, write_fun, 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 :param write_fun: a function that takes a bytes representation of a TLV, the expected keys as list and returns decoded TLV as list :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 = 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)), ]) step4_expectations = [ TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_Proof ] response_tlv = write_fun(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) response_tlv = TLV.reorder(response_tlv, step4_expectations) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][1] == TLV.M4, \ 'perform_pair_setup: State not M4' if response_tlv[1][0] == TLV.kTLVType_Error: error_handler(response_tlv[1][1], 'step 5') assert response_tlv[1][ 0] == TLV.kTLVType_Proof, 'perform_pair_setup: Not a proof' if not srp_client.verify_servers_proof(response_tlv[1][1]): 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 = [(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) step6_expectations = [ TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_EncryptedData ] response_tlv = write_fun(body, step6_expectations) # # Step #7 ios (Verification) (page 47) # response_tlv = TLV.reorder(response_tlv, step6_expectations) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M6, 'perform_pair_setup: State not M6' if response_tlv[1][0] == TLV.kTLVType_Error: error_handler(response_tlv[1][1], 'step 7') assert response_tlv[1][ 0] == TLV.kTLVType_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[1][1]) if decrypted_data is False: raise homekit.exception.IllegalData('step 7') response_tlv = TLV.decode_bytearray(decrypted_data) response_tlv = TLV.reorder(response_tlv, [ TLV.kTLVType_Identifier, TLV.kTLVType_PublicKey, TLV.kTLVType_Signature ]) assert response_tlv[2][ 0] == TLV.kTLVType_Signature, 'perform_pair_setup: No signature' accessory_sig = response_tlv[2][1] assert response_tlv[0][ 0] == TLV.kTLVType_Identifier, 'perform_pair_setup: No identifier' accessory_pairing_id = response_tlv[0][1] assert response_tlv[1][ 0] == TLV.kTLVType_PublicKey, 'perform_pair_setup: No public key' accessory_ltpk = response_tlv[1][1] 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[1][1])) try: e25519s.verify(bytes(accessory_sig), bytes(accessory_info)) except AssertionError: raise InvalidSignatureError('step #7') return { 'AccessoryPairingID': response_tlv[0][1].decode(), 'AccessoryLTPK': hexlify(response_tlv[1][1]).decode(), 'iOSPairingId': ios_pairing_id, 'iOSDeviceLTSK': ios_device_ltsk.to_ascii(encoding='hex').decode()[:64], 'iOSDeviceLTPK': hexlify(ios_device_ltpk.to_bytes()).decode() }
def parse_sig_read_response(data, expected_tid): # TODO document me # parse header and check stuff logger.debug('parse sig read response %s', bytes([int(a) for a in data]).hex()) # handle the header data cf = data[0] logger.debug('control field %d', cf) tid = data[1] logger.debug('transaction id %d (expected was %d)', tid, expected_tid) status = data[2] logger.debug('status code %d (%s)', status, HapBleStatusCodes[status]) assert cf == 0x02 assert tid == expected_tid assert status == HapBleStatusCodes.SUCCESS # get body length length = int.from_bytes(data[3:5], byteorder='little') logger.debug('expected body length %d (got %d)', length, len(data[5:])) # parse tlvs and analyse information tlv = TLV.decode_bytes(data[5:]) description = '' characteristic_format = '' characteristic_range = None characteristic_step = None for t in tlv: if t[0] == TLV.kTLVHAPParamCharacteristicType: chr_type = [int(a) for a in t[1]] chr_type.reverse() chr_type = str(uuid.UUID(''.join('%02x' % b for b in chr_type))) if t[0] == TLV.kTLVHAPParamServiceInstanceId: svc_id = int.from_bytes(t[1], byteorder='little') if t[0] == TLV.kTLVHAPParamServiceType: svc_type = [int(a) for a in t[1]] svc_type.reverse() svc_type = str(uuid.UUID(''.join('%02x' % b for b in svc_type))) if t[0] == TLV.kTLVHAPParamHAPCharacteristicPropertiesDescriptor: chr_prop_int = int.from_bytes(t[1], byteorder='little') if t[0] == TLV.kTLVHAPParamGATTUserDescriptionDescriptor: description = t[1].decode() if t[0] == TLV.kTLVHAPParamHAPValidValuesDescriptor: print('valid values', t[1]) if t[0] == TLV.kTLVHAPParamHAPValidValuesRangeDescriptor: print('valid values range', t[1]) if t[0] == TLV.kTLVHAPParamGATTPresentationFormatDescriptor: unit_bytes = t[1][2:4] unit_bytes.reverse() characteristic_format = BleCharacteristicFormats.get(int(t[1][0]), 'unknown') unit = BleCharacteristicUnits.get(int.from_bytes(unit_bytes, byteorder='big'), 'unknown') if t[0] == TLV.kTLVHAPParamGATTValidRange: logger.debug('range: %s', t[1].hex()) lower = None upper = None if characteristic_format == 'int32' or characteristic_format == 'int': (lower, upper) = struct.unpack('ii', t[1]) if characteristic_format == 'uint8': (lower, upper) = struct.unpack('BB', t[1]) if characteristic_format == 'float': (lower, upper) = struct.unpack('ff', t[1]) # TODO include all formats! characteristic_range = (lower, upper) if t[0] == TLV.kTLVHAPParamHAPStepValueDescriptor: characteristic_step = None if characteristic_format == 'int32': characteristic_step = struct.unpack('i', t[1])[0] if characteristic_format == 'uint8': characteristic_step = struct.unpack('B', t[1])[0] # TODO include all formats! # parse permissions # TODO refactor! perms = [] if (chr_prop_int & 0x0001) > 0: perms.append('r') if (chr_prop_int & 0x0002) > 0: perms.append('w') if (chr_prop_int & 0x0004) > 0: perms.append('aad') if (chr_prop_int & 0x0008) > 0: perms.append('tw') if (chr_prop_int & 0x0010) > 0: perms.append('pr') if (chr_prop_int & 0x0020) > 0: perms.append('pw') if (chr_prop_int & 0x0040) > 0: perms.append('hd') if (chr_prop_int & 0x0080) > 0: perms.append('evc') if (chr_prop_int & 0x0100) > 0: perms.append('evd') result = {'description': description, 'perms': perms, 'format': characteristic_format, 'unit': unit, 'range': characteristic_range, 'step': characteristic_step, 'type': chr_type.upper(), 'sid': svc_id, 'service_type': svc_type} logger.debug('result: %s', str(result)) return result
def write(request, expected): return pair_setup_write(TLV.encode_list(request), expected)
def request(self, feature_char, feature_char_id, op, body=None): transaction_id = random.randrange(0, 255) data = bytearray([0x00, op, transaction_id]) data.extend(feature_char_id.to_bytes(length=2, byteorder='little')) if body: logger.debug('body: %s', body) data.extend(body) logger.debug('data: %s', data) cnt_bytes = self.c2a_counter.to_bytes(8, byteorder='little') cipher_and_mac = chacha20_aead_encrypt(bytes(), self.c2a_key, cnt_bytes, bytes([0, 0, 0, 0]), data) cipher_and_mac[0].extend(cipher_and_mac[1]) data = cipher_and_mac[0] logger.debug('cipher and mac %s', cipher_and_mac[0].hex()) result = feature_char.write_value(value=data) logger.debug('write resulted in: %s', result) self.c2a_counter += 1 data = [] while not data or len(data) == 0: time.sleep(1) logger.debug('reading characteristic') data = feature_char.read_value() if not data and not self.device.is_connected(): raise AccessoryDisconnectedError('Characteristic read failed') resp_data = bytearray([b for b in data]) logger.debug('read: %s', bytearray(resp_data).hex()) data = chacha20_aead_decrypt(bytes(), self.a2c_key, self.a2c_counter.to_bytes(8, byteorder='little'), bytes([0, 0, 0, 0]), resp_data) logger.debug('decrypted: %s', bytearray(data).hex()) if not data: return {} # parse header and check stuff logger.debug('parse sig read response %s', bytes([int(a) for a in data]).hex()) # handle the header data cf = data[0] logger.debug('control field %d', cf) tid = data[1] logger.debug('transaction id %d (expected was %d)', tid, transaction_id) status = data[2] logger.debug('status code %d (%s)', status, HapBleStatusCodes[status]) assert cf == 0x02 assert tid == transaction_id if status != HapBleStatusCodes.SUCCESS: raise RequestRejected(status, HapBleStatusCodes[status]) self.a2c_counter += 1 # get body length length = int.from_bytes(data[3:5], byteorder='little') logger.debug('expected body length %d (got %d)', length, len(data[5:])) # parse tlvs and analyse information tlv = TLV.decode_bytes(data[5:]) logger.debug('received TLV: %s', TLV.to_string(tlv)) return dict(tlv)
def test__convert_from_python__tlv__proper_values(self): pairing_data = self._create_pairing_data() pairing = BlePairing(pairing_data) data = TLV.encode_list([(6, bytearray(b'\x03'))]) self.assertEqual(b'\x06\x01\x03', pairing._convert_from_python(1, 10, data))
def request_pin_setup(connection, pin): """ Requests a Pin from device """ 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)]) # # Step #3 ios --> accessory (send SRP verify request) (see page 41) # connection.request('POST', '/pair-setup', request_tlv, headers) resp = connection.getresponse() response_tlv = TLV.decode_bytes(resp.read()) response_tlv = TLV.reorder(response_tlv, [ TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_PublicKey, TLV.kTLVType_Salt ]) assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][ 1] == TLV.M2, 'perform_pair_setup: State not M2' # the errors here can be: # * kTLVError_Unavailable: Device is paired # * kTLVError_MaxTries: More than 100 unsuccessfull attempts # * kTLVError_Busy: There is already a pairing going on # if response_tlv[1][0] == TLV.kTLVType_Error: # error_handler(response_tlv[1][1], 'step 3') assert response_tlv[1][ 0] == TLV.kTLVType_PublicKey, 'perform_pair_setup: Not a public key' assert response_tlv[2][ 0] == TLV.kTLVType_Salt, 'perform_pair_setup: Not a salt' srp_client = SrpClient('Pair-Setup', pin) srp_client.set_salt(response_tlv[2][1]) srp_client.set_server_public_key(response_tlv[1][1]) 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) # response_tlv = TLV.reorder(response_tlv, [TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_Proof]) # assert response_tlv[0][0] == TLV.kTLVType_State and response_tlv[0][1] == TLV.M4, \ # 'perform_pair_setup: State not M4' # if response_tlv[1][0] == TLV.kTLVType_Error: # error_handler(response_tlv[1][1], 'step 5') # assert response_tlv[1][0] == TLV.kTLVType_Proof, 'perform_pair_setup: Not a proof' # if not srp_client.verify_servers_proof(response_tlv[1][1]): # 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_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())