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)
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")
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')
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)