Beispiel #1
0
def log_transferred_value(text: str, aid: int,
                          characteristic: AbstractCharacteristic, value,
                          filtered_value):
    """
    Logs the transfer of a value between controller and acccessory or vice versa. For characteristics
    of type TLV8, a decoder is used if available else a deep decode is done.

    :param text: a `str` to express which direction of transfer takes place
    :param aid: the accessory id
    :param characteristic: the characteristic for which the transfer takes place
    :param value: the value that was transferred
    """
    iid = int(characteristic.iid)
    debug_value = value
    filtered_debug_value = filtered_value
    characteristic_name = CharacteristicsTypes.get_short(characteristic.type)
    if characteristic.format == CharacteristicFormats.tlv8:
        bytes_value = base64.b64decode(value)
        filtered_bytes_value = base64.b64decode(filtered_value)
        decoder = decoder_loader.load(characteristic.type)
        if decoder:
            try:
                debug_value = tlv8.format_string(decoder(bytes_value))
                filtered_debug_value = tlv8.format_string(
                    decoder(filtered_bytes_value))
            except Exception as e:
                logging.error('problem decoding', e)
        else:
            debug_value = tlv8.format_string(tlv8.deep_decode(bytes_value))
            filtered_debug_value = tlv8.format_string(
                tlv8.deep_decode(filtered_bytes_value))
    logging.info(
        '%s %s.%s (type %s / %s): \n\toriginal value: %s\n\tfiltered value: %s'
        % (text, aid, iid, characteristic.type, characteristic_name,
           debug_value, filtered_debug_value))
Beispiel #2
0
    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
Beispiel #3
0
 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
Beispiel #4
0
 def test_format_string(self):
     data = [
         tlv8.Entry(1, 3.141),
         tlv8.Entry(2, [
             tlv8.Entry(3, 'hello'),
             tlv8.Entry(4, 'world'),
         ]),
         tlv8.Entry(1, 2)
     ]
     print(tlv8.format_string(data))
Beispiel #5
0
    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 test_entrylist_format_string(self):
        el = tlv8.EntryList([
            tlv8.Entry(1, 1),
            tlv8.Entry(2, tlv8.EntryList([
                tlv8.Entry(4, 4),
                tlv8.Entry(5, 5)
            ])),
            tlv8.Entry(3, 3),
        ])
        result = tlv8.format_string(el)
        expected = """[
  <1, 1>,
  <2, [
    <4, 4>,
    <5, 5>,
  ]>,
  <3, 3>,
]"""
        self.assertEqual(result, expected)
Beispiel #7
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 = tlv8.decode(data[5:])
        logger.debug('received TLV: %s', tlv8.format_string(tlv))
        return tlv
    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
Beispiel #9
0
        print(json.dumps(data, indent=4, cls=tlv8.JsonEncoder))

    if args.output == 'compact':
        for accessory in data:
            aid = accessory['aid']
            for service in accessory['services']:
                s_type = service['type']
                s_iid = service['iid']
                print('{aid}.{iid}: >{stype}<'.format(aid=aid, iid=s_iid, stype=ServicesTypes.get_short(s_type)))

                for characteristic in service['characteristics']:
                    c_iid = characteristic['iid']
                    value = characteristic.get('value', '')
                    c_type = characteristic['type']
                    c_format = characteristic['format']
                    # we need to get the entry list from the decoder into a string and reformat it for better
                    # readability.
                    if args.decode and c_format in [CharacteristicFormats.tlv8] and isinstance(value, tlv8.EntryList):
                        value = tlv8.format_string(value)
                        value = '      '.join(value.splitlines(keepends=True))
                        value = '\n      ' + value
                    perms = ','.join(characteristic['perms'])
                    desc = characteristic.get('description', '')
                    c_type = CharacteristicsTypes.get_short(c_type)
                    print('  {aid}.{iid}: ({description}) >{ctype}< [{perms}]'.format(aid=aid,
                                                                                      iid=c_iid,
                                                                                      ctype=c_type,
                                                                                      perms=perms,
                                                                                      description=desc))
                    print('    Value: {value}'.format(value=value))
Beispiel #10
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 = tlv8.encode([
            tlv8.Entry(TlvTypes.State, States.M1),
            tlv8.Entry(TlvTypes.Method, Methods.RemovePairing),
            tlv8.Entry(TlvTypes.Identifier, pairingIdToDelete.encode())
        ])

        if connection_type == 'IP':
            if not IP_TRANSPORT_SUPPORTED:
                raise TransportNotSupportedError('IP')
            session = IpSession(pairing_data)
            response = session.post('/pairings',
                                    request_tlv,
                                    content_type='application/pairing+tlv8')
            session.close()
            data = response.read()
            data = tlv8.decode(
                data, {
                    TlvTypes.State: tlv8.DataType.INTEGER,
                    TlvTypes.Error: tlv8.DataType.INTEGER
                })
        elif connection_type == 'BLE':
            if not BLE_TRANSPORT_SUPPORTED:
                raise TransportNotSupportedError('BLE')
            inner = tlv8.encode([
                tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse,
                           bytearray(b'\x01')),
                tlv8.Entry(AdditionalParameterTypes.Value, 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 = tlv8.decode(
                response.first_by_id(AdditionalParameterTypes.Value).data, {
                    TlvTypes.State: tlv8.DataType.INTEGER,
                    TlvTypes.Error: tlv8.DataType.INTEGER
                })
        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', tlv8.format_string(data))
        state = data.first_by_id(TlvTypes.State).data
        if len(data) == 1 and state == States.M2:
            if not pairingId:
                del self.pairings[alias]
        else:
            error = data.first_by_id(TlvTypes.Error)
            if error and error.data == Errors.Authentication:
                raise AuthenticationError(
                    'Remove pairing failed: missing authentication')
            else:
                raise UnknownError('Remove pairing failed: unknown error')