diff --git a/pynitrokey/cli/nk3/__init__.py b/pynitrokey/cli/nk3/__init__.py index ab41d9af..1ae140e1 100644 --- a/pynitrokey/cli/nk3/__init__.py +++ b/pynitrokey/cli/nk3/__init__.py @@ -471,6 +471,29 @@ def status(ctx: Context) -> None: local_print(f"Variant: {status.variant.name}") +@nk3.command() +@click.pass_obj +@click.argument("key") +def get_config(ctx: Context, key: str) -> None: + """Query a config value.""" + with ctx.connect_device() as device: + admin = AdminApp(device) + value = admin.get_config(key) + print(value) + + +@nk3.command() +@click.pass_obj +@click.argument("key") +@click.argument("value") +def set_config(ctx: Context, key: str, value: str) -> None: + """Query a config value.""" + with ctx.connect_device() as device: + admin = AdminApp(device) + admin.set_config(key, value) + print(f"Updated configuration {key}.") + + @nk3.command() @click.pass_obj def version(ctx: Context) -> None: diff --git a/pynitrokey/nk3/admin_app.py b/pynitrokey/nk3/admin_app.py index 6ebc5154..d2265978 100644 --- a/pynitrokey/nk3/admin_app.py +++ b/pynitrokey/nk3/admin_app.py @@ -3,6 +3,7 @@ from enum import Enum, IntFlag from typing import Optional +from fido2 import cbor from fido2.ctap import CtapError from pynitrokey.nk3.device import Command, Nitrokey3Device @@ -15,6 +16,8 @@ class AdminCommand(Enum): STATUS = 0x80 TEST_SE050 = 0x81 + GET_CONFIG = 0x82 + SET_CONFIG = 0x83 @enum.unique @@ -54,6 +57,35 @@ class Status: variant: Optional[Variant] = None +@enum.unique +class ConfigStatus(Enum): + SUCCESS = 0 + READ_FAILED = 1 + WRITE_FAILED = 2 + DESERIALIZATION_FAILED = 3 + SERIALIZATION_FAILED = 4 + INVALID_KEY = 5 + INVALID_VALUE = 6 + DATA_TOO_LONG = 7 + + @classmethod + def from_int(cls, i: int) -> Optional["ConfigStatus"]: + for status in ConfigStatus: + if status.value == i: + return status + return None + + @classmethod + def check(cls, i: int, msg: str) -> None: + status = ConfigStatus.from_int(i) + if status != ConfigStatus.SUCCESS: + if status: + error = str(status) + else: + error = f"unknown error {i:x}" + raise Exception(f"{msg}: {error}") + + class AdminApp: def __init__(self, device: Nitrokey3Device) -> None: self.device = device @@ -103,3 +135,16 @@ def version(self) -> Version: def se050_tests(self) -> Optional[bytes]: return self._call(AdminCommand.TEST_SE050) + + def get_config(self, key: str) -> str: + reply = self._call(AdminCommand.GET_CONFIG, data=key.encode()) + if not reply or len(reply) < 1: + raise ValueError("The device returned an empty response") + ConfigStatus.check(reply[0], "Failed to get config value") + return reply[1:].decode() + + def set_config(self, key: str, value: str) -> None: + request = cbor.encode({"key": key, "value": value}) + reply = self._call(AdminCommand.SET_CONFIG, data=request, response_len=1) + assert reply + ConfigStatus.check(reply[0], "Failed to set config value")