diff --git a/ykman/_cli/config.py b/ykman/_cli/config.py index 8584cdb4..322fe892 100644 --- a/ykman/_cli/config.py +++ b/ykman/_cli/config.py @@ -195,13 +195,13 @@ def set_lock_code(ctx, lock_code, new_lock_code, clear, generate, force): new_lock_code = click_prompt( "Enter your new lock code", hide_input=True, confirmation_prompt=True ) - set_code = _parse_lock_code(ctx, new_lock_code) + set_code = _parse_lock_code(new_lock_code) # Get the current lock code to use if info.is_locked: if not lock_code: lock_code = click_prompt("Enter your current lock code", hide_input=True) - use_code = _parse_lock_code(ctx, lock_code) + use_code = _parse_lock_code(lock_code) else: if lock_code: raise CliFail( @@ -224,6 +224,21 @@ def set_lock_code(ctx, lock_code, new_lock_code, clear, generate, force): raise CliFail("Failed to set the lock code.") +def _get_lock_code(is_locked, lock_code, force): + if force and is_locked and not lock_code: + raise CliFail("Configuration is locked - supply the --lock-code option.") + if lock_code and not is_locked: + raise CliFail("Configuration is not locked - remove the --lock-code option.") + + if is_locked and not lock_code: + lock_code = prompt_lock_code() + + if lock_code: + lock_code = _parse_lock_code(lock_code) + + return lock_code + + def _configure_applications( ctx, config, @@ -234,8 +249,6 @@ def _configure_applications( lock_code, force, ): - _require_config(ctx) - info = ctx.obj["info"] supported = info.supported_capabilities.get(transport) enabled = info.config.enabled_capabilities.get(transport) @@ -244,7 +257,7 @@ def _configure_applications( raise CliFail(f"{transport} not supported on this YubiKey.") if enable & disable: - ctx.fail("Invalid options.") + raise CliFail("Invalid options.") unsupported = ~supported & (enable | disable) if unsupported: @@ -258,7 +271,7 @@ def _configure_applications( if transport == TRANSPORT.USB: if sum(CAPABILITY) & new_enabled == 0: - ctx.fail(f"Can not disable all applications over {transport}.") + raise CliFail(f"Can not disable all applications over {transport}.") reboot = enabled.usb_interfaces != new_enabled.usb_interfaces else: @@ -267,11 +280,7 @@ def _configure_applications( if reboot: changes.append("The YubiKey will reboot") - is_locked = info.is_locked - - if force and is_locked and not lock_code: - raise CliFail("Configuration is locked - supply the --lock-code option.") - if lock_code and not is_locked: + if lock_code and not info.is_locked: raise CliFail("Configuration is not locked - remove the --lock-code option.") click.echo(f"{transport} configuration changes:") @@ -279,14 +288,10 @@ def _configure_applications( click.echo(f" {change}") force or click.confirm("Proceed?", abort=True, err=True) - if is_locked and not lock_code: - lock_code = prompt_lock_code() - - if lock_code: - lock_code = _parse_lock_code(ctx, lock_code) - config.enabled_capabilities = {transport: new_enabled} + lock_code = _get_lock_code(info.is_locked, lock_code, force) + app = ctx.obj["session"] try: app.write_device_config( @@ -369,25 +374,27 @@ def usb( """ _require_config(ctx) - if not ( - list_enabled - or enable_all - or enable - or disable - or touch_eject - or no_touch_eject - or autoeject_timeout - or chalresp_timeout + if not any( + [ + list_enabled, + enable_all, + enable, + disable, + touch_eject, + no_touch_eject, + autoeject_timeout, + chalresp_timeout, + ] ): - ctx.fail("No configuration options chosen.") + raise CliFail("No configuration options chosen.") if touch_eject and no_touch_eject: - ctx.fail("Invalid options.") + raise CliFail("Invalid options.") if list_enabled: _list_apps(ctx, TRANSPORT.USB) - config = DeviceConfig({}, autoeject_timeout, chalresp_timeout, None) + config = DeviceConfig({}, autoeject_timeout, chalresp_timeout) changes = [] info = ctx.obj["info"] @@ -453,20 +460,53 @@ def usb( metavar="HEX", help="current application configuration lock code", ) -def nfc(ctx, enable, disable, enable_all, disable_all, list_enabled, lock_code, force): +@click.option( + "-R", + "--restrict", + is_flag=True, + help="Disable NFC for transport, re-enabled by USB power", +) +def nfc( + ctx, + enable, + disable, + enable_all, + disable_all, + list_enabled, + lock_code, + restrict, + force, +): """ Enable or disable applications over NFC. """ + info = ctx.obj["info"] + if TRANSPORT.NFC not in info.supported_capabilities: + raise CliFail("This YubiKey does not support NFC.") + _require_config(ctx) - if not (list_enabled or enable_all or enable or disable_all or disable): - ctx.fail("No configuration options chosen.") + if not any([list_enabled, enable_all, enable, disable_all, disable, restrict]): + raise CliFail("No configuration options chosen.") if list_enabled: _list_apps(ctx, TRANSPORT.NFC) config = DeviceConfig({}, None, None, None) - info = ctx.obj["info"] + + if restrict: + if info.version < (5, 7): + raise CliFail("NFC restriction requires YubiKey 5.7 or later.") + + config.nfc_restricted = True + lock_code = _get_lock_code(info.is_locked, lock_code, force) + ctx.obj["session"].write_device_config(config, False, lock_code) + click.echo( + "YubiKey NFC disabled. It will be re-enabled automatically the next time " + "it is connected to USB power." + ) + ctx.exit() + changes = [] nfc_supported = info.supported_capabilities.get(TRANSPORT.NFC) @@ -511,17 +551,19 @@ def _list_apps(ctx, transport): def _ensure_not_invalid_options(ctx, enable, disable): if enable & disable: - ctx.fail("Invalid options.") + raise CliFail("Invalid options.") -def _parse_lock_code(ctx, lock_code): +def _parse_lock_code(lock_code): try: lock_code = bytes.fromhex(lock_code) if lock_code and len(lock_code) != 16: - ctx.fail("Lock code must be exactly 16 bytes (32 hexadecimal digits) long.") + raise CliFail( + "Lock code must be exactly 16 bytes (32 hexadecimal digits) long." + ) return lock_code except Exception: - ctx.fail("Lock code has the wrong format.") + raise CliFail("Lock code has the wrong format.") # MODE @@ -539,7 +581,7 @@ def _parse_mode_string(ctx, param, mode): mode_int = int(mode) return Mode.from_code(mode_int) except IndexError: - ctx.fail(f"Invalid mode: {mode_int}") + raise CliFail(f"Invalid mode: {mode_int}") except ValueError: pass # Not a numeric mode, parse string @@ -560,7 +602,7 @@ def _parse_mode_string(ctx, param, mode): if t: interfaces |= _parse_interface_string(t) except ValueError: - ctx.fail(f"Invalid mode string: {mode}") + raise CliFail(f"Invalid mode string: {mode}") return Mode(interfaces) @@ -633,7 +675,7 @@ def mode(ctx, mode, touch_eject, autoeject_timeout, chalresp_timeout, force): if mode.interfaces != USB_INTERFACE.CCID: if touch_eject: - ctx.fail("--touch-eject can only be used when setting CCID-only mode") + raise CliFail("--touch-eject can only be used when setting CCID-only mode") if not force: if mode == my_mode: