From 88b676f8ec19f608be577bfffe26f99b9fdc3ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 1 Sep 2023 11:41:11 +0200 Subject: [PATCH 1/3] Add support for factory reset command --- pynitrokey/cli/nk3/__init__.py | 8 ++++++ pynitrokey/nk3/admin_app.py | 52 ++++++++++++++++++++++++++++++++++ pynitrokey/nk3/device.py | 3 ++ 3 files changed, 63 insertions(+) diff --git a/pynitrokey/cli/nk3/__init__.py b/pynitrokey/cli/nk3/__init__.py index 67120599..86a5dd60 100644 --- a/pynitrokey/cli/nk3/__init__.py +++ b/pynitrokey/cli/nk3/__init__.py @@ -503,6 +503,14 @@ def version(ctx: Context) -> None: local_print(version) +@nk3.command() +@click.pass_obj +def factory_reset(ctx: Context) -> None: + """Factory reset all functionality of the device""" + with ctx.connect_device() as device: + device.factory_reset() + + @nk3.command() @click.pass_obj def wink(ctx: Context) -> None: diff --git a/pynitrokey/nk3/admin_app.py b/pynitrokey/nk3/admin_app.py index d2265978..8129cd44 100644 --- a/pynitrokey/nk3/admin_app.py +++ b/pynitrokey/nk3/admin_app.py @@ -1,4 +1,5 @@ import enum +import sys from dataclasses import dataclass from enum import Enum, IntFlag from typing import Optional @@ -6,6 +7,7 @@ from fido2 import cbor from fido2.ctap import CtapError +from pynitrokey.helpers import local_critical, local_print from pynitrokey.nk3.device import Command, Nitrokey3Device from .device import VERSION_LEN @@ -18,6 +20,7 @@ class AdminCommand(Enum): TEST_SE050 = 0x81 GET_CONFIG = 0x82 SET_CONFIG = 0x83 + FACTORY_RESET = 0x84 @enum.unique @@ -57,6 +60,35 @@ class Status: variant: Optional[Variant] = None +@enum.unique +class FactoryResetStatus(Enum): + SUCCESS = 0 + NOT_CONFIRMED = 0x01 + APP_NOT_ALLOWED = 0x02 + APP_FAILED_PARSE = 0x03 + + @classmethod + def from_int(cls, i: int) -> Optional["FactoryResetStatus"]: + for status in FactoryResetStatus: + if status.value == i: + return status + return None + + @classmethod + def check(cls, i: int, msg: str) -> None: + status = FactoryResetStatus.from_int(i) + if status != FactoryResetStatus.SUCCESS: + if status is None: + raise Exception(f"Unknown error {i:x}") + if status == FactoryResetStatus.NOT_CONFIRMED: + error = "Operation was not confirmed with touch" + elif status == FactoryResetStatus.APP_NOT_ALLOWED: + error = "The application does not support factory reset through nitropy" + elif status == FactoryResetStatus.APP_FAILED_PARSE: + error = "The application name must be utf-8" + local_critical(f"{msg}: {error}", support_hint=False) + + @enum.unique class ConfigStatus(Enum): SUCCESS = 0 @@ -148,3 +180,23 @@ def set_config(self, key: str, value: str) -> None: reply = self._call(AdminCommand.SET_CONFIG, data=request, response_len=1) assert reply ConfigStatus.check(reply[0], "Failed to set config value") + + def factory_reset(self) -> None: + try: + local_print( + "Please touch the device to confirm the operation", file=sys.stderr + ) + reply = self._call(AdminCommand.FACTORY_RESET, response_len=1) + if reply is None: + local_critical( + "Factory reset is not supported by the firmware version on the device", + support_hint=False, + ) + return + except OSError as e: + if e.errno == 5: + self.device.logger.debug("ignoring OSError after reboot", exc_info=e) + return + else: + raise e + FactoryResetStatus.check(reply[0], "Failed to factory reset the device") diff --git a/pynitrokey/nk3/device.py b/pynitrokey/nk3/device.py index 99886e9e..a7defe12 100644 --- a/pynitrokey/nk3/device.py +++ b/pynitrokey/nk3/device.py @@ -114,6 +114,9 @@ def uuid(self) -> Optional[Uuid]: def version(self) -> Version: return self.admin.version() + def factory_reset(self) -> None: + self.admin.factory_reset() + def wink(self) -> None: self.device.wink() From ab56d7e3a66b09af63dfa827ff6a2ab8e7665066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Wed, 15 Nov 2023 18:06:13 +0100 Subject: [PATCH 2/3] Implement per-app factory reset --- pynitrokey/cli/nk3/__init__.py | 13 +++++++++++++ pynitrokey/nk3/admin_app.py | 16 ++++++++++++++++ pynitrokey/nk3/device.py | 3 +++ 3 files changed, 32 insertions(+) diff --git a/pynitrokey/cli/nk3/__init__.py b/pynitrokey/cli/nk3/__init__.py index 86a5dd60..1b75ab0d 100644 --- a/pynitrokey/cli/nk3/__init__.py +++ b/pynitrokey/cli/nk3/__init__.py @@ -511,6 +511,19 @@ def factory_reset(ctx: Context) -> None: device.factory_reset() +# We consciously do not allow resetting the admin app +APPLICATIONS_CHOICE = click.Choice(["fido", "opcard", "secrets", "piv", "webcrypt"]) + + +@nk3.command() +@click.pass_obj +@click.argument("application", type=APPLICATIONS_CHOICE, required=True) +def factory_reset_app(ctx: Context, application: str) -> None: + """Factory reset all functionality of an application""" + with ctx.connect_device() as device: + device.factory_reset_app(application) + + @nk3.command() @click.pass_obj def wink(ctx: Context) -> None: diff --git a/pynitrokey/nk3/admin_app.py b/pynitrokey/nk3/admin_app.py index 8129cd44..a9ba4baf 100644 --- a/pynitrokey/nk3/admin_app.py +++ b/pynitrokey/nk3/admin_app.py @@ -21,6 +21,7 @@ class AdminCommand(Enum): GET_CONFIG = 0x82 SET_CONFIG = 0x83 FACTORY_RESET = 0x84 + FACTORY_RESET_APP = 0x85 @enum.unique @@ -200,3 +201,18 @@ def factory_reset(self) -> None: else: raise e FactoryResetStatus.check(reply[0], "Failed to factory reset the device") + + def factory_reset_app(self, application: str) -> None: + local_print("Please touch the device to confirm the operation", file=sys.stderr) + reply = self._call( + AdminCommand.FACTORY_RESET_APP, + data=application.encode("ascii"), + response_len=1, + ) + if reply is None: + local_critical( + "Application Factory reset is not supported by the firmware version on the device", + support_hint=False, + ) + return + FactoryResetStatus.check(reply[0], "Failed to factory reset the device") diff --git a/pynitrokey/nk3/device.py b/pynitrokey/nk3/device.py index a7defe12..f73d41f9 100644 --- a/pynitrokey/nk3/device.py +++ b/pynitrokey/nk3/device.py @@ -117,6 +117,9 @@ def version(self) -> Version: def factory_reset(self) -> None: self.admin.factory_reset() + def factory_reset_app(self, app: str) -> None: + self.admin.factory_reset_app(app) + def wink(self) -> None: self.device.wink() From b469fb2dc5fb0f44f2a06dfa380c9fff623ff058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Fri, 17 Nov 2023 10:18:59 +0100 Subject: [PATCH 3/3] Add experimental flag --- pynitrokey/cli/nk3/__init__.py | 21 +++++++++++++++++++-- pynitrokey/cli/nk3/secrets.py | 13 ------------- pynitrokey/helpers.py | 13 +++++++++++++ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/pynitrokey/cli/nk3/__init__.py b/pynitrokey/cli/nk3/__init__.py index 1b75ab0d..715a5ce3 100644 --- a/pynitrokey/cli/nk3/__init__.py +++ b/pynitrokey/cli/nk3/__init__.py @@ -21,6 +21,7 @@ from pynitrokey.helpers import ( DownloadProgressBar, Retries, + check_experimental_flag, local_print, require_windows_admin, ) @@ -505,8 +506,16 @@ def version(ctx: Context) -> None: @nk3.command() @click.pass_obj -def factory_reset(ctx: Context) -> None: +@click.option( + "--experimental", + default=False, + is_flag=True, + help="Allow to execute experimental features", + hidden=True, +) +def factory_reset(ctx: Context, experimental: bool) -> None: """Factory reset all functionality of the device""" + check_experimental_flag(experimental) with ctx.connect_device() as device: device.factory_reset() @@ -518,8 +527,16 @@ def factory_reset(ctx: Context) -> None: @nk3.command() @click.pass_obj @click.argument("application", type=APPLICATIONS_CHOICE, required=True) -def factory_reset_app(ctx: Context, application: str) -> None: +@click.option( + "--experimental", + default=False, + is_flag=True, + help="Allow to execute experimental features", + hidden=True, +) +def factory_reset_app(ctx: Context, application: str, experimental: bool) -> None: """Factory reset all functionality of an application""" + check_experimental_flag(experimental) with ctx.connect_device() as device: device.factory_reset_app(application) diff --git a/pynitrokey/cli/nk3/secrets.py b/pynitrokey/cli/nk3/secrets.py index 92ec0d22..f66d2fb7 100644 --- a/pynitrokey/cli/nk3/secrets.py +++ b/pynitrokey/cli/nk3/secrets.py @@ -385,19 +385,6 @@ def abort_if_not_supported(cond: bool, name: str = "") -> None: raise click.Abort() -def check_experimental_flag(experimental: bool) -> None: - """Helper function to show common warning for the experimental features""" - if not experimental: - local_print(" ") - local_print( - "This feature is experimental, which means it was not tested thoroughly.\n" - "Note: data stored with it can be lost in the next firmware update.\n" - "Please pass --experimental switch to force running it anyway." - ) - local_print(" ") - raise click.Abort() - - def ask_to_touch_if_needed() -> None: """Helper function to show common request for the touch if device signalizes it""" local_print("Please touch the device if it blinks", file=sys.stderr) diff --git a/pynitrokey/helpers.py b/pynitrokey/helpers.py index e8302289..622cf13d 100644 --- a/pynitrokey/helpers.py +++ b/pynitrokey/helpers.py @@ -426,3 +426,16 @@ def check_pynitrokey_version() -> None: if not confirm("Do you still want to continue?", default=False): raise click.Abort() + + +def check_experimental_flag(experimental: bool) -> None: + """Helper function to show common warning for the experimental features""" + if not experimental: + local_print(" ") + local_print( + "This feature is experimental, which means it was not tested thoroughly.\n" + "Note: data stored with it can be lost in the next firmware update.\n" + "Please pass --experimental switch to force running it anyway." + ) + local_print(" ") + raise click.Abort()