Example #1
0
class Controller(object):
    _descriptor = None
    _dev_info = {}
    _key = None

    def __init__(self):
        self.settings = Settings('oath')

        # Wrap all args and return values as JSON.
        for f in dir(self):
            if not f.startswith('_'):
                func = getattr(self, f)
                if isinstance(func, types.MethodType):
                    setattr(self, f, as_json(func))

    def count_devices(self):
        return len(get_descriptors())

    def refresh(self, otp_mode=False):
        descriptors = get_descriptors()
        if len(descriptors) != 1:
            self._descriptor = None
            return None

        desc = descriptors[0]
        if desc.fingerprint != (
                self._descriptor.fingerprint if self._descriptor else None) \
                or not otp_mode and not self._dev_info.get('version'):
            try:
                dev = desc.open_device(
                    TRANSPORT.OTP if otp_mode else TRANSPORT.CCID)
                if otp_mode:
                    version = None
                else:
                    controller = OathController(dev.driver)
                    version = controller.version
            except Exception:
                return None
            self._descriptor = desc
            self._dev_info = {
                'name': dev.device_name,
                'version': version,
                'serial': dev.serial or '',
                'enabled': [c.name for c in CAPABILITY if c & dev.enabled],
                'connections':
                [t.name for t in TRANSPORT if t & dev.capabilities]
            }

        return self._dev_info

    def _unlock(self, controller):
        if controller.locked:
            keys = self.settings.get('keys', {})
            if self._key is not None:
                controller.validate(self._key)
            elif controller.id in keys:
                controller.validate(a2b_hex(keys[controller.id]))
            else:
                return False
        return True

    def refresh_credentials(self, timestamp):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            controller = OathController(dev.driver)
            self._unlock(controller)
            entries = controller.calculate_all(timestamp)
            return [
                pair_to_dict(cred, code) for (cred, code) in entries
                if not cred.is_hidden
            ]
        except Exception:
            return []

    def calculate(self, credential, timestamp):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            controller = OathController(dev.driver)
            self._unlock(controller)
        except Exception:
            return None
        code = controller.calculate(cred_from_dict(credential), timestamp)
        return code_to_dict(code)

    def calculate_slot_mode(self, slot, digits, timestamp):
        try:
            code = self._read_slot_code(slot,
                                        digits,
                                        timestamp,
                                        wait_for_touch=True)
            return pair_to_dict(
                Credential(self._slot_name(slot), OATH_TYPE.TOTP, True), code)
        except YkpersError as e:
            if e.errno == 4:
                logger.debug(
                    'Time out error, user probably did not touch the device.')
            else:
                logger.error('Failed to calculate code in slot mode',
                             exc_info=e)
        except Exception as e:
            logger.error('Failed to calculate code in slot mode', exc_info=e)
        return None

    def refresh_slot_credentials(self, slots, digits, timestamp):
        result = []
        if slots[0]:
            entry = self._read_slot_cred(1, digits[0], timestamp)
            if entry:
                result.append(entry)
        if slots[1]:
            entry = self._read_slot_cred(2, digits[1], timestamp)
            if entry:
                result.append(entry)
        return [pair_to_dict(cred, code) for (cred, code) in result]

    def _read_slot_cred(self, slot, digits, timestamp):
        try:
            code = self._read_slot_code(slot,
                                        digits,
                                        timestamp,
                                        wait_for_touch=False)
            return (Credential(self._slot_name(slot), OATH_TYPE.TOTP,
                               False), code)
        except YkpersError as e:
            if e.errno == 11:
                return (Credential(self._slot_name(slot), OATH_TYPE.TOTP,
                                   True), None)
        except Exception as e:
            return (Credential(str(e).encode(), OATH_TYPE.TOTP, True), None)
        return None

    def _read_slot_code(self, slot, digits, timestamp, wait_for_touch):
        dev = self._descriptor.open_device(TRANSPORT.OTP)
        code = dev.driver.calculate(slot,
                                    challenge=timestamp,
                                    totp=True,
                                    digits=int(digits),
                                    wait_for_touch=wait_for_touch)
        valid_from = timestamp - (timestamp % 30)
        valid_to = valid_from + 30
        return Code(code, valid_from, valid_to)

    def _slot_name(self, slot):
        return "YubiKey Slot {}".format(slot).encode('utf-8')

    def needs_validation(self):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            return not self._unlock(OathController(dev.driver))
        except Exception:
            return True

    def get_oath_id(self):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            return OathController(dev.driver).id
        except Exception:
            return None

    def provide_password(self, password, remember=False):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._key = controller.derive_key(password)
        try:
            controller.validate(self._key)
        except Exception:
            return False
        if remember:
            keys = self.settings.setdefault('keys', {})
            keys[controller.id] = b2a_hex(self._key).decode()
            self.settings.write()
        return True

    def set_password(self, new_password, remember=False):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._unlock(controller)
        keys = self.settings.setdefault('keys', {})
        if new_password is not None:
            self._key = controller.set_password(new_password)
            if remember:
                keys[controller.id] = b2a_hex(self._key).decode()
            elif controller.id in keys:
                del keys[controller.id]
        else:
            controller.clear_password()
            del keys[controller.id]
            self._key = None
        self.settings.write()

    def add_credential(self, name, secret, issuer, oath_type, algo, digits,
                       period, touch):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._unlock(controller)
        try:
            secret = parse_b32_key(secret)
        except Exception as e:
            return str(e)
        try:
            controller.put(
                CredentialData(secret, issuer, name, OATH_TYPE[oath_type],
                               ALGO[algo], int(digits), int(period), 0, touch))
        except APDUError as e:
            # NEO doesn't return a no space error if full,
            # but a command aborted error. Assume it's because of
            # no space in this context.
            if e.sw in (SW.NO_SPACE, SW.COMMAND_ABORTED):
                return 'No space'
            else:
                raise

    def add_slot_credential(self, slot, key, touch):
        dev = self._descriptor.open_device(TRANSPORT.OTP)
        key = parse_b32_key(key)
        try:
            dev.driver.program_chalresp(int(slot), key, touch)
        except Exception as e:
            return str(e)

    def delete_slot_credential(self, slot):
        dev = self._descriptor.open_device(TRANSPORT.OTP)
        dev.driver.zap_slot(slot)

    def delete_credential(self, credential):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._unlock(controller)
        controller.delete(cred_from_dict(credential))

    def parse_qr(self, screenshot):
        data = b64decode(screenshot['data'])
        image = PixelImage(data, screenshot['width'], screenshot['height'])
        for qr in qrparse.parse_qr_codes(image, 2):
            return credential_data_to_dict(
                CredentialData.from_uri(qrdecode.decode_qr_data(qr)))

    def reset(self):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        controller.reset()

    def slot_status(self):
        dev = self._descriptor.open_device(TRANSPORT.OTP)
        return list(dev.driver.slot_status)
Example #2
0
    class Controller(object):

        _descs = []
        _descs_fps = []
        _devs = []
        _devices = []
        _state = 0

        _current_serial = None
        _current_derived_key = None

        _reader_filter = None
        _readers = []

        def __init__(self):
            self.settings = Settings("oath")

            # Wrap all args and return values as JSON.
            for f in dir(self):
                if not f.startswith("_"):
                    func = getattr(self, f)
                    if isinstance(func, types.MethodType):
                        setattr(self, f, as_json(catch_error(func)))

        def _open_oath(self):
            if self._reader_filter:
                dev = self._get_dev_from_reader()
                if dev:
                    return OathContextManager(dev.open_connection(SmartCardConnection))
                else:
                    raise ValueError("no_device_custom_reader")

            return OathContextManager(
                connect_to_device(self._current_serial, [SmartCardConnection])[0]
            )

        def _open_otp(self):
            return OtpContextManager(
                connect_to_device(self._current_serial, [OtpConnection])[0]
            )

        def _descriptors_changed(self):
            old_state = self._state
            _, self._state = scan_devices()
            return self._state != old_state

            old_descs = self._descs[:]
            old_descs_fps = self._descs_fps[:]
            self._descs = get_descriptors()
            self._descs_fps = [desc.fingerprint for desc in self._descs]
            descs_changed = old_descs_fps != self._descs_fps
            n_descs_changed = len(self._descs) != len(old_descs)
            return n_descs_changed or descs_changed

        def check_descriptors(self):
            old_state = self._state
            self._devs, self._state = scan_devices()
            return success({"needToRefresh": self._state != old_state})

        def _readers_changed(self, filter):
            old_readers = self._readers
            self._readers = []
            for dev in list_ccid(filter):
                try:
                    with dev.open_connection(SmartCardConnection):
                        self._readers.append(dev)
                except Exception:
                    pass
            readers_changed = len(self._readers) != len(old_readers)
            return readers_changed

        def check_readers(self, filter):
            return success({"needToRefresh": self._readers_changed(filter)})

        def _get_dev_from_reader(self):
            readers = list_ccid(self._reader_filter)
            if len(readers) == 1:
                dev = readers[0]
                return dev
            return None

        def _get_devices(self, otp_mode=False):
            res = []
            for dev, info in list_all_devices():
                usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB]
                interfaces_enabled = []
                if CAPABILITY.OTP & usb_enabled:
                    interfaces_enabled.append("OTP")
                if (CAPABILITY.U2F | CAPABILITY.FIDO2) & usb_enabled:
                    interfaces_enabled.append("FIDO")
                if (
                    CAPABILITY.OATH | CAPABILITY.PIV | CAPABILITY.OPENPGP
                ) & usb_enabled:
                    interfaces_enabled.append("CCID")
                if otp_mode:
                    selectable = "OTP" in interfaces_enabled
                    has_password = False
                else:
                    selectable = "CCID" in interfaces_enabled
                    if selectable:
                        with connect_to_device(info.serial, [SmartCardConnection])[
                            0
                        ] as conn:
                            oath = OathSession(conn)
                            has_password = oath.locked
                    else:
                        has_password = False
                res.append(
                    {
                        "name": get_name(info, dev.pid.get_type()),
                        "version": ".".join(str(d) for d in info.version),
                        "serial": info.serial or "",
                        "usbInterfacesEnabled": interfaces_enabled,
                        "hasPassword": has_password,
                        "selectable": selectable,
                        "validated": not has_password,
                    }
                )
            return res

        def refresh_devices(self, otp_mode=False, reader_filter=None):
            self._devices = []

            if not otp_mode and reader_filter:
                self._reader_filter = reader_filter
                dev = self._get_dev_from_reader()
                if dev:
                    with dev.open_connection(SmartCardConnection) as conn:
                        info = read_info(dev.pid, conn)
                        try:
                            oath = OathSession(conn)
                            has_password = oath.locked
                            selectable = True
                        except ApplicationNotAvailableError:
                            selectable = False
                            has_password = False

                    usb_enabled = info.config.enabled_capabilities[TRANSPORT.USB]
                    interfaces_enabled = []
                    if CAPABILITY.OTP & usb_enabled:
                        interfaces_enabled.append("OTP")
                    if (CAPABILITY.U2F | CAPABILITY.FIDO2) & usb_enabled:
                        interfaces_enabled.append("FIDO")
                    if (
                        CAPABILITY.OATH | CAPABILITY.PIV | CAPABILITY.OPENPGP
                    ) & usb_enabled:
                        interfaces_enabled.append("CCID")

                    self._devices.append(
                        {
                            "name": get_name(
                                info, dev.pid.get_type() if dev.pid else None
                            ),
                            "version": ".".join(str(d) for d in info.version),
                            "serial": info.serial or "",
                            "usbInterfacesEnabled": interfaces_enabled,
                            "hasPassword": has_password,
                            "selectable": selectable,
                            "validated": True,  # not has_password
                        }
                    )
                    return success({"devices": self._devices})
                else:
                    return success({"devices": []})
            else:
                self._reader_filter = None
                # Forget current serial and derived key if no descriptors
                # Return empty list of devices
                if not self._devs:
                    self._current_serial = None
                    self._current_derived_key = None
                    return success({"devices": []})

                self._devices = self._get_devices(otp_mode)

                # If no current serial, or current serial seems removed,
                # select the first serial found.
                if not self._current_serial or (
                    self._current_serial not in [dev["serial"] for dev in self._devices]
                ):
                    for dev in self._devices:
                        if dev["serial"]:
                            self._current_serial = dev["serial"]
                            break
                return success({"devices": self._devices})

        def select_current_serial(self, serial):
            self._current_serial = serial
            self._current_derived_key = None
            return success()

        def ccid_calculate_all(self, timestamp):
            with self._open_oath() as oath_controller:
                self._unlock(oath_controller)
                entries = oath_controller.calculate_all(timestamp)
                return success(
                    {
                        "entries": [
                            pair_to_dict(cred, code)
                            for (cred, code) in entries.items()
                            if not is_hidden(cred)
                        ]
                    }
                )

        def ccid_calculate(self, credential, timestamp):
            with self._open_oath() as oath_controller:
                self._unlock(oath_controller)
                code = oath_controller.calculate_code(
                    cred_from_dict(credential), timestamp
                )
                return success({"credential": credential, "code": code_to_dict(code)})

        def ccid_add_credential(
            self,
            name,
            secret,
            issuer,
            oath_type,
            algo,
            digits,
            period,
            touch,
            overwrite=False,
        ):
            secret = parse_b32_key(secret)
            with self._open_oath() as oath_controller:
                try:
                    self._unlock(oath_controller)
                    cred_data = CredentialData(
                        name,
                        OATH_TYPE[oath_type],
                        HASH_ALGORITHM[algo],
                        secret,
                        int(digits),
                        int(period),
                        0,
                        issuer,
                    )
                    if not overwrite:
                        key = cred_data.get_id()
                        if key in [
                            cred.id for cred in oath_controller.list_credentials()
                        ]:
                            return failure("credential_already_exists")
                    oath_controller.put_credential(cred_data, touch)
                except ApduError as e:
                    # NEO doesn't return a no space error if full,
                    # but a command aborted error. Assume it's because of
                    # no space in this context.
                    if e.sw in (SW.NO_SPACE, SW.COMMAND_ABORTED):
                        return failure("no_space")
                    else:
                        raise
                return success()

        def ccid_validate(self, password, remember=False):
            with self._open_oath() as oath_controller:
                key = oath_controller.derive_key(password)
                try:
                    oath_controller.validate(key)
                    self._current_derived_key = key
                    if remember:
                        keys = self.settings.setdefault("keys", {})
                        keys[oath_controller.device_id] = b2a_hex(
                            self._current_derived_key
                        ).decode()
                        self.settings.write()
                    return success()
                except ApduError as e:
                    if e.sw == SW.INCORRECT_PARAMETERS:
                        return failure("validate_failed")

        def _otp_get_code_or_touch(self, slot, digits, timestamp, wait_for_touch=False):
            with self._open_otp() as session:
                # Check that slot is not empty
                if not session.get_config_state().is_configured(slot):
                    raise CommandError("not programmed")

                challenge = time_challenge(timestamp)

                try:
                    event = Event()

                    def on_keepalive(status):
                        if (
                            not hasattr(on_keepalive, "prompted")
                            and status == 2
                            and not wait_for_touch
                        ):
                            on_keepalive.prompted = True
                            event.set()

                    response = session.calculate_hmac_sha1(
                        slot, challenge, event, on_keepalive
                    )
                    code = format_oath_code(response, int(digits))
                    return code, False
                except TimeoutError:
                    return None, hasattr(on_keepalive, "prompted")

        def otp_calculate_all(self, slot1_digits, slot2_digits, timestamp):
            valid_from = timestamp - (timestamp % 30)
            valid_to = valid_from + 30
            entries = []

            def calc(slot, digits, label):
                try:
                    code, touch = self._otp_get_code_or_touch(slot, digits, timestamp)
                    entries.append(
                        {
                            "credential": cred_to_dict(
                                Credential(
                                    "",
                                    label.encode(),
                                    None,
                                    label,
                                    OATH_TYPE.TOTP,
                                    30,
                                    touch,
                                )
                            ),
                            "code": code_to_dict(Code(code, valid_from, valid_to))
                            if code
                            else None,
                        }
                    )
                except CommandError:
                    pass

            if slot1_digits:
                calc(1, slot1_digits, "Slot 1")

            if slot2_digits:
                calc(2, slot2_digits, "Slot 2")

            return success({"entries": entries})

        def otp_calculate(self, slot, digits, credential, timestamp):
            valid_from = timestamp - (timestamp % 30)
            valid_to = valid_from + 30
            code, _ = self._otp_get_code_or_touch(
                slot, digits, timestamp, wait_for_touch=True
            )
            return success(
                {
                    "credential": credential,
                    "code": code_to_dict(Code(code, valid_from, valid_to)),
                }
            )

        def otp_slot_status(self):
            with self._open_otp() as otp_controller:
                state = otp_controller.get_config_state()
            return success({"status": (state.is_configured(1), state.is_configured(2))})

        def otp_add_credential(self, slot, key, touch):
            key = parse_b32_key(key)
            with self._open_otp() as otp_controller:
                otp_controller.put_configuration(
                    int(slot), HmacSha1SlotConfiguration(key).require_touch(touch)
                )

            return success()

        def otp_delete_credential(self, slot):
            with self._open_otp() as otp_controller:
                otp_controller.delete_slot(slot)
            return success()

        def _unlock(self, controller):
            if controller.locked:
                keys = self.settings.get("keys", {})
                if self._current_derived_key is not None:
                    controller.validate(self._current_derived_key)
                elif controller.device_id in keys:
                    controller.validate(a2b_hex(keys[controller.device_id]))
                else:
                    return failure("failed_to_unlock_key")

        def ccid_delete_credential(self, credential):
            with self._open_oath() as oath_controller:
                self._unlock(oath_controller)
                oath_controller.delete_credential(cred_from_dict(credential).id)
                return success()

        def ccid_reset(self):
            with self._open_oath() as oath_controller:
                oath_controller.reset()
                return success()

        def ccid_clear_local_passwords(self):
            self.settings.setdefault("keys", {})
            del self.settings["keys"]
            self.settings.write()
            return success()

        def ccid_remove_password(self):
            with self._open_oath() as oath_controller:
                self._unlock(oath_controller)
                oath_controller.unset_key()
                self._current_derived_key = None
                keys = self.settings.setdefault("keys", {})
                if oath_controller.device_id in keys:
                    del keys[oath_controller.device_id]
                self.settings.write()
                return success()

        def ccid_set_password(self, new_password, remember=False):
            with self._open_oath() as oath_controller:
                self._unlock(oath_controller)
                keys = self.settings.setdefault("keys", {})
                key = oath_controller.derive_key(new_password)
                oath_controller.set_key(key)
                self._current_derived_key = key
                if remember:
                    keys[oath_controller.device_id] = b2a_hex(
                        self._current_derived_key
                    ).decode()
                elif oath_controller.device_id in keys:
                    del keys[oath_controller.device_id]
                self.settings.write()
                return success()

        def get_connected_readers(self):
            return success({"readers": [reader.name for reader in list_readers()]})

        def parse_qr(self, screenshot):
            data = b64decode(screenshot["data"])
            image = PixelImage(data, screenshot["width"], screenshot["height"])
            for qr in qrparse.parse_qr_codes(image, 2):
                try:
                    return success(
                        credential_data_to_dict(
                            CredentialData.parse_uri(qrdecode.decode_qr_data(qr))
                        )
                    )
                except Exception as e:
                    logger.error("Failed to parse uri", exc_info=e)
                    return failure("failed_to_parse_uri")
            return failure("no_credential_found")
Example #3
0
class Controller(object):

    _descs = []
    _descs_fps = []
    _devices = []

    _current_serial = None
    _current_derived_key = None

    _reader_filter = None
    _readers = []

    def __init__(self):

        self.settings = Settings('oath')

        # Wrap all args and return values as JSON.
        for f in dir(self):
            if not f.startswith('_'):
                func = getattr(self, f)
                if isinstance(func, types.MethodType):
                    setattr(self, f, as_json(catch_error(func)))

    def _open_oath(self):
        if self._reader_filter:
            dev = self._get_dev_from_reader()
            if dev:
                return OathContextManager(dev)
            else:
                raise ValueError('no_device_custom_reader')

        return OathContextManager(
            open_device(TRANSPORT.CCID, serial=self._current_serial))

    def _open_otp(self):
        return OtpContextManager(
            open_device(TRANSPORT.OTP, serial=self._current_serial))

    def _descriptors_changed(self):
        old_descs = self._descs[:]
        old_descs_fps = self._descs_fps[:]
        self._descs = get_descriptors()
        self._descs_fps = [desc.fingerprint for desc in self._descs]
        descs_changed = (old_descs_fps != self._descs_fps)
        n_descs_changed = len(self._descs) != len(old_descs)
        return n_descs_changed or descs_changed

    def check_descriptors(self):
        return success({
            'needToRefresh': self._descriptors_changed()
        })

    def _readers_changed(self, filter):
        old_readers = self._readers
        self._readers = list(open_ccid(filter))
        readers_changed = len(self._readers) != len(old_readers)
        return readers_changed

    def check_readers(self, filter):
        return success({
            'needToRefresh': self._readers_changed(filter)
        })

    def _get_dev_from_reader(self):
        readers = list(open_ccid(self._reader_filter))
        if len(readers) == 1:
            drv = readers[0]
            return YubiKey(Descriptor.from_driver(drv), drv)
        return None

    def _get_devices(self, otp_mode=False):
        res = []
        descs_to_match = self._descs[:]
        handled_serials = set()
        time.sleep(0.5)  # Let macOS take time to see the reader
        for transport in [TRANSPORT.CCID, TRANSPORT.OTP, TRANSPORT.FIDO]:
            if not descs_to_match:
                return res
            for dev in list_devices(transport):
                if not descs_to_match:
                    return res
                serial = dev.serial
                selectable = dev.mode.has_transport(
                    TRANSPORT.OTP if otp_mode else TRANSPORT.CCID)

                if selectable and not otp_mode and transport == TRANSPORT.CCID:
                    controller = OathController(dev.driver)
                    has_password = controller.locked
                else:
                    has_password = False

                if serial not in handled_serials:
                    handled_serials.add(serial)
                    matches = [
                        d for d in descs_to_match if (
                            d.key_type, d.mode) == (
                                dev.driver.key_type, dev.driver.mode)]
                    if len(matches) > 0:
                        matching_descriptor = matches[0]
                        res.append({
                            'name': dev.device_name,
                            'version': '.'.join(
                                str(x) for x in dev.version
                                ) if dev.version else '',
                            'serial': serial or '',
                            'usbInterfacesEnabled': str(
                                dev.mode).split('+'),
                            'hasPassword': has_password,
                            'selectable': selectable,
                            'validated': not has_password
                        })
                        descs_to_match.remove(matching_descriptor)
        return res

    def refresh_devices(self, otp_mode=False, reader_filter=None):
        self._devices = []

        if not otp_mode and reader_filter:
            self._reader_filter = reader_filter
            dev = self._get_dev_from_reader()
            if dev:
                controller = OathController(dev.driver)
                has_password = controller.locked
                self._devices.append({
                    'name': dev.device_name,
                    'version': '.'.join(
                        str(x) for x in dev.version
                        ) if dev.version else '',
                    'serial': dev.serial or '',
                    'usbInterfacesEnabled': str(dev.mode).split('+'),
                    'hasPassword': has_password,
                    'selectable': True,
                    'validated': True
                })
                return success({'devices': self._devices})
            else:
                return success({'devices': []})
        else:
            self._reader_filter = None
            # Forget current serial and derived key if no descriptors
            # Return empty list of devices
            if not self._descs:
                self._current_serial = None
                self._current_derived_key = None
                return success({'devices': []})

            self._devices = self._get_devices(otp_mode)

            # If no current serial, or current serial seems removed,
            # select the first serial found.
            if not self._current_serial or (
                    self._current_serial not in [
                        dev['serial'] for dev in self._devices]):
                for dev in self._devices:
                    if dev['serial']:
                        self._current_serial = dev['serial']
                        break
            return success({'devices': self._devices})

    def select_current_serial(self, serial):
        self._current_serial = serial
        self._current_derived_key = None
        return success()

    def ccid_calculate_all(self, timestamp):
        with self._open_oath() as oath_controller:
            self._unlock(oath_controller)
            entries = oath_controller.calculate_all(timestamp)
            return success(
                {
                    'entries': [
                        pair_to_dict(
                            cred, code) for (
                                cred, code) in entries if not cred.is_hidden]
                }
            )

    def ccid_calculate(self, credential, timestamp):
        with self._open_oath() as oath_controller:
            self._unlock(oath_controller)
            code = oath_controller.calculate(
                cred_from_dict(credential), timestamp)
            return success({
                'credential': credential,
                'code': code_to_dict(code)
            })

    def ccid_add_credential(
            self, name, secret, issuer, oath_type,
            algo, digits, period, touch, overwrite=False):
        secret = parse_b32_key(secret)
        with self._open_oath() as oath_controller:
            try:
                self._unlock(oath_controller)
                cred_data = CredentialData(
                    secret, issuer, name, OATH_TYPE[oath_type], ALGO[algo],
                    int(digits), int(period), 0, touch
                )
                if not overwrite:
                    key = cred_data.make_key()
                    if key in [cred.key for cred in oath_controller.list()]:
                        return failure('credential_already_exists')
                oath_controller.put(cred_data)
            except APDUError as e:
                # NEO doesn't return a no space error if full,
                # but a command aborted error. Assume it's because of
                # no space in this context.
                if e.sw in (SW.NO_SPACE, SW.COMMAND_ABORTED):
                    return failure('no_space')
                else:
                    raise
            return success()

    def ccid_validate(self, password, remember=False):
        with self._open_oath() as oath_controller:
            key = oath_controller.derive_key(password)
            try:
                oath_controller.validate(key)
                self._current_derived_key = key
                if remember:
                    keys = self.settings.setdefault('keys', {})
                    keys[oath_controller.id] = b2a_hex(
                        self._current_derived_key).decode()
                    self.settings.write()
                return success()
            except APDUError as e:
                if e.sw == SW.INCORRECT_PARAMETERS:
                    return failure('validate_failed')

    def _otp_get_code_or_touch(self, slot, digits, timestamp, wait_for_touch=False):
        code = None
        touch = False
        with self._open_otp() as otp_controller:
            try:
                code = otp_controller.calculate(
                    slot, challenge=timestamp, totp=True,
                    digits=int(digits), wait_for_touch=wait_for_touch)
            except YkpersError as e:
                if e.errno == 11:  # Operation would block, touch credential
                    touch = True
                else:
                    raise
            return code, touch

    def otp_calculate_all(
            self, slot1_digits, slot2_digits, timestamp):
        valid_from = timestamp - (timestamp % 30)
        valid_to = valid_from + 30
        entries = []

        def calc(slot, digits, label):
            try:
                code, touch = self._otp_get_code_or_touch(
                    slot, digits, timestamp)
                entries.append({
                    'credential': cred_to_dict(
                        Credential(label.encode(), OATH_TYPE.TOTP, touch)),
                    'code': code_to_dict(
                        Code(code, valid_from, valid_to)) if code else None
                })
            except YkpersError as e:
                if e.errno == 4:
                    pass
                else:
                    raise

        if slot1_digits:
            calc(1, slot1_digits, "Slot 1")

        if slot2_digits:
            calc(2, slot2_digits, "Slot 2")

        return success({'entries': entries})

    def otp_calculate(self, slot, digits, credential, timestamp):
        valid_from = timestamp - (timestamp % 30)
        valid_to = valid_from + 30
        code, _ = self._otp_get_code_or_touch(slot, digits, timestamp, wait_for_touch=True)
        return success({
            'credential': credential,
            'code': code_to_dict(Code(code, valid_from, valid_to))
        })

    def otp_slot_status(self):
        with self._open_otp() as otp_controller:
            return success({'status': otp_controller.slot_status})

    def otp_add_credential(self, slot, key, touch):
        key = parse_b32_key(key)
        with self._open_otp() as otp_controller:
            otp_controller.program_chalresp(int(slot), key, touch)
        return success()

    def otp_delete_credential(self, slot):
        with self._open_otp() as otp_controller:
            otp_controller.zap_slot(slot)
        return success()

    def _unlock(self, controller):
        if controller.locked:
            keys = self.settings.get('keys', {})
            if self._current_derived_key is not None:
                controller.validate(self._current_derived_key)
            elif controller.id in keys:
                controller.validate(a2b_hex(keys[controller.id]))
            else:
                return failure('failed_to_unlock_key')

    def ccid_delete_credential(self, credential):
        with self._open_oath() as oath_controller:
            self._unlock(oath_controller)
            oath_controller.delete(cred_from_dict(credential))
            return success()

    def ccid_reset(self):
        with self._open_oath() as oath_controller:
            oath_controller.reset()
            return success()

    def ccid_remove_password(self):
        with self._open_oath() as oath_controller:
            self._unlock(oath_controller)
            oath_controller.clear_password()
            self._current_derived_key = None
            keys = self.settings.setdefault('keys', {})
            if oath_controller.id in keys:
                del keys[oath_controller.id]
            self.settings.write()
            return success()

    def ccid_set_password(self, new_password, remember=False):
        with self._open_oath() as oath_controller:
            self._unlock(oath_controller)
            keys = self.settings.setdefault('keys', {})
            self._current_derived_key = \
                oath_controller.set_password(new_password)
            if remember:
                keys[oath_controller.id] = b2a_hex(
                    self._current_derived_key).decode()
            elif oath_controller.id in keys:
                del keys[oath_controller.id]
            self.settings.write()
            return success()

    def get_connected_readers(self):
        return success({'readers': [str(reader) for reader in list_readers()]})

    def parse_qr(self, screenshot):
        data = b64decode(screenshot['data'])
        image = PixelImage(data, screenshot['width'], screenshot['height'])
        for qr in qrparse.parse_qr_codes(image, 2):
            try:
                return success(
                    credential_data_to_dict(
                        CredentialData.from_uri(qrdecode.decode_qr_data(qr))))
            except Exception as e:
                logger.error('Failed to parse uri', exc_info=e)
                return failure('failed_to_parse_uri')
        return failure('no_credential_found')
Example #4
0
class Controller(object):
    _descriptor = None
    _dev_info = {}
    _key = None

    def __init__(self):
        self.settings = Settings('oath')

        # Wrap all args and return values as JSON.
        for f in dir(self):
            if not f.startswith('_'):
                func = getattr(self, f)
                if isinstance(func, types.MethodType):
                    setattr(self, f, as_json(func))

    def count_devices(self):
        return len(get_descriptors())

    def refresh(self, otp_mode=False):
        descriptors = get_descriptors()
        if len(descriptors) != 1:
            self._descriptor = None
            return None

        desc = descriptors[0]

        unmatched_otp_mode = otp_mode and not desc.mode.has_transport(
            TRANSPORT.OTP)
        unmatched_ccid_mode = not otp_mode and not desc.mode.has_transport(
            TRANSPORT.CCID)

        if unmatched_otp_mode or unmatched_ccid_mode:
            return {
                'transports': [
                    t.name for t in TRANSPORT.split(desc.mode.transports)
                ],
                'usable': False,
            }

        if desc.fingerprint != (
                self._descriptor.fingerprint if self._descriptor else None) \
                or not otp_mode and not self._dev_info.get('version'):
            try:
                dev = desc.open_device(TRANSPORT.OTP if otp_mode
                                       else TRANSPORT.CCID)
                if otp_mode:
                    version = None
                else:
                    controller = OathController(dev.driver)
                    version = controller.version
            except Exception as e:
                logger.debug('Failed to refresh YubiKey', exc_info=e)
                return None

            self._descriptor = desc
            self._dev_info = {
                'usable': True,
                'name': dev.device_name,
                'version': version,
                'serial': dev.serial or '',
                'usb_interfaces_supported': [
                    t.name for t in TRANSPORT
                    if t & dev.config.usb_supported],
                'usb_interfaces_enabled': str(dev.mode).split('+')
            }

        return self._dev_info

    def _unlock(self, controller):
        if controller.locked:
            keys = self.settings.get('keys', {})
            if self._key is not None:
                controller.validate(self._key)
            elif controller.id in keys:
                controller.validate(a2b_hex(keys[controller.id]))
            else:
                return False
        return True

    def clear_key(self):
        self._key = None

    def refresh_credentials(self, timestamp):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            controller = OathController(dev.driver)
            self._unlock(controller)
            entries = controller.calculate_all(timestamp)
            return [pair_to_dict(cred, code) for (cred, code) in entries
                    if not cred.is_hidden]
        except Exception:
            return []

    def calculate(self, credential, timestamp):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            controller = OathController(dev.driver)
            self._unlock(controller)
        except Exception:
            return None
        code = controller.calculate(cred_from_dict(credential), timestamp)
        return code_to_dict(code)

    def calculate_slot_mode(self, slot, digits, timestamp):
        try:
            code = self._read_slot_code(
                slot, digits, timestamp, wait_for_touch=True)
            return pair_to_dict(Credential(self._slot_name(slot),
                                           OATH_TYPE.TOTP, True), code)
        except YkpersError as e:
            if e.errno == 4:
                logger.debug(
                    'Time out error, user probably did not touch the device.')
            else:
                logger.error(
                    'Failed to calculate code in slot mode', exc_info=e)
        except Exception as e:
            logger.error('Failed to calculate code in slot mode', exc_info=e)
        return None

    def refresh_slot_credentials(self, slots, digits, timestamp):
        result = []
        if slots[0]:
            entry = self._read_slot_cred(1, digits[0], timestamp)
            if entry:
                result.append(entry)
        if slots[1]:
            entry = self._read_slot_cred(2, digits[1], timestamp)
            if entry:
                result.append(entry)
        return [pair_to_dict(cred, code) for (cred, code) in result]

    def _read_slot_cred(self, slot, digits, timestamp):
        try:
            code = self._read_slot_code(
                slot, digits, timestamp, wait_for_touch=False)
            return (Credential(self._slot_name(slot), OATH_TYPE.TOTP, False),
                    code)
        except YkpersError as e:
            if e.errno == 11:
                return (Credential(self._slot_name(slot), OATH_TYPE.TOTP, True
                                   ), None)
        except Exception as e:
            return (Credential(str(e).encode(), OATH_TYPE.TOTP, True), None)
        return None

    def _read_slot_code(self, slot, digits, timestamp, wait_for_touch):
        with self._descriptor.open_device(TRANSPORT.OTP) as dev:
            controller = OtpController(dev.driver)
            code = controller.calculate(
                slot, challenge=timestamp, totp=True, digits=int(digits),
                wait_for_touch=wait_for_touch)
            valid_from = timestamp - (timestamp % 30)
            valid_to = valid_from + 30
            return Code(code, valid_from, valid_to)

    def _slot_name(self, slot):
        return "YubiKey Slot {}".format(slot).encode('utf-8')

    def needs_validation(self):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            return not self._unlock(OathController(dev.driver))
        except Exception:
            return True

    def get_oath_id(self):
        try:
            dev = self._descriptor.open_device(TRANSPORT.CCID)
            return OathController(dev.driver).id
        except Exception:
            return None

    def provide_password(self, password, remember=False):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._key = controller.derive_key(password)
        try:
            controller.validate(self._key)
        except Exception:
            return False
        if remember:
            keys = self.settings.setdefault('keys', {})
            keys[controller.id] = b2a_hex(self._key).decode()
            self.settings.write()
        return True

    def set_password(self, new_password, remember=False):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._unlock(controller)
        keys = self.settings.setdefault('keys', {})
        if new_password is not None:
            self._key = controller.set_password(new_password)
            if remember:
                keys[controller.id] = b2a_hex(self._key).decode()
            elif controller.id in keys:
                del keys[controller.id]
        else:
            controller.clear_password()
            del keys[controller.id]
            self._key = None
        self.settings.write()

    def add_credential(
            self, name, secret, issuer, oath_type, algo, digits,
            period, touch):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._unlock(controller)
        try:
            secret = parse_b32_key(secret)
        except Exception as e:
            return str(e)
        try:
            controller.put(CredentialData(
                secret, issuer, name, OATH_TYPE[oath_type], ALGO[algo],
                int(digits), int(period), 0, touch
            ))
        except APDUError as e:
            # NEO doesn't return a no space error if full,
            # but a command aborted error. Assume it's because of
            # no space in this context.
            if e.sw in (SW.NO_SPACE, SW.COMMAND_ABORTED):
                return 'No space'
            else:
                raise

    def add_slot_credential(self, slot, key, touch):
        try:
            key = parse_b32_key(key)
            with self._descriptor.open_device(TRANSPORT.OTP) as dev:
                controller = OtpController(dev.driver)
                controller.program_chalresp(int(slot), key, touch)
                return {'success': True, 'error': None}
        except Exception as e:
            if str(e) == 'Incorrect padding':
                return {'success': False, 'error': 'wrong padding'}
            if str(e) == 'key lengths >20 bytes not supported':
                return {'success': False, 'error': 'too large key'}
            return {'success': False, 'error': str(e)}

    def delete_slot_credential(self, slot):
        with self._descriptor.open_device(TRANSPORT.OTP) as dev:
            controller = OtpController(dev.driver)
            controller.zap_slot(slot)

    def delete_credential(self, credential):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        self._unlock(controller)
        controller.delete(cred_from_dict(credential))

    def parse_qr(self, screenshot):
        data = b64decode(screenshot['data'])
        image = PixelImage(data, screenshot['width'], screenshot['height'])
        for qr in qrparse.parse_qr_codes(image, 2):
            return credential_data_to_dict(
                CredentialData.from_uri(qrdecode.decode_qr_data(qr)))

    def reset(self):
        dev = self._descriptor.open_device(TRANSPORT.CCID)
        controller = OathController(dev.driver)
        controller.reset()

    def slot_status(self):
        with self._descriptor.open_device(TRANSPORT.OTP) as dev:
            controller = OtpController(dev.driver)
            return list(controller.slot_status)