Exemplo n.º 1
0
    def write_http(request, expected):
        logging.debug('write message: %s',
                      TLV.to_string(TLV.decode_bytes(request)))
        headers = {'Content-Type': 'application/pairing+tlv8'}

        connection.request('POST', '/pair-verify', request, headers)
        resp = connection.getresponse()
        response_tlv = TLV.decode_bytes(resp.read(), expected)
        logging.debug('response: %s', TLV.to_string(response_tlv))
        return response_tlv
Exemplo n.º 2
0
 def write_http(request, expected):
     logging.debug('write message: %s', TLV.to_string(TLV.decode_bytes(request)))
     connection.putrequest('POST', '/pair-verify', skip_accept_encoding=True)
     connection.putheader('Content-Type', 'application/pairing+tlv8')
     connection.putheader('Content-Length', len(request))
     connection.endheaders(request)
     resp = connection.getresponse()
     response_tlv = TLV.decode_bytes(resp.read(), expected)
     logging.debug('response: %s', TLV.to_string(response_tlv))
     return response_tlv
Exemplo n.º 3
0
    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')
Exemplo n.º 4
0
    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)
        ])

        # decode is required because post needs a string representation
        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()
Exemplo n.º 5
0
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
Exemplo n.º 6
0
    def test_filter(self):
        example = bytes(bytearray.fromhex('060103' + '010203'))
        expected = [
            [6, bytearray(b'\x03')],
        ]

        data = TLV.decode_bytes(example, expected=[6])
        self.assertListEqual(data, expected)
Exemplo n.º 7
0
    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
Exemplo n.º 8
0
    def test_long_values_decode_bytes_to_list(self):
        example = bytes(
            bytearray.fromhex('060103' +
                              ('09FF' + 255 * '61' + '092D' + 45 * '61') +
                              '010568656c6c6f'))
        expected = [[6, bytearray(b'\x03')], [9, bytearray(300 * b'a')],
                    [1, bytearray(b'hello')]]

        data = TLV.decode_bytes(example)
        self.assertListEqual(data, expected)
Exemplo n.º 9
0
    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 = TLV.encode_list([(TLV.kTLVType_State, TLV.M1),
                                       (TLV.kTLVType_Method, TLV.ListPairings)
                                       ])
        try:
            response = self.session.sec_http.post('/pairings',
                                                  request_tlv.decode())
            data = response.read()
        except (AccessoryDisconnectedError, EncryptionError):
            self.session.close()
            self.session = None
            raise
        data = TLV.decode_bytes(data)

        if not (data[0][0] == TLV.kTLVType_State and data[0][1] == TLV.M2):
            raise UnknownError('unexpected data received: ' + str(data))
        elif data[1][0] == TLV.kTLVType_Error and data[1][
                1] == TLV.kTLVError_Authentication:
            raise UnpairedError('Must be paired')
        else:
            tmp = []
            r = {}
            for d in data[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
Exemplo n.º 10
0
    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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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
Exemplo n.º 13
0
    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')
Exemplo n.º 14
0
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())
Exemplo n.º 15
0
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_impl 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
    :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
    """
    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)
    #
    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_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)
    #
    response_tlv = TLV.reorder(
        response_tlv,
        [TLV.kTLVType_State, TLV.kTLVType_Error, TLV.kTLVType_EncryptedData])
    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()
    }
Exemplo n.º 16
0
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
Exemplo n.º 17
0
    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)