Skip to content

Commit

Permalink
Merge pull request #472 from Nitrokey/factory-reset
Browse files Browse the repository at this point in the history
Add support for full device factory reset command
  • Loading branch information
sosthene-nitrokey authored Nov 30, 2023
2 parents da46d49 + b469fb2 commit cae3fc3
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 13 deletions.
38 changes: 38 additions & 0 deletions pynitrokey/cli/nk3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pynitrokey.helpers import (
DownloadProgressBar,
Retries,
check_experimental_flag,
local_print,
require_windows_admin,
)
Expand Down Expand Up @@ -503,6 +504,43 @@ def version(ctx: Context) -> None:
local_print(version)


@nk3.command()
@click.pass_obj
@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()


# 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)
@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)


@nk3.command()
@click.pass_obj
def wink(ctx: Context) -> None:
Expand Down
13 changes: 0 additions & 13 deletions pynitrokey/cli/nk3/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions pynitrokey/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
68 changes: 68 additions & 0 deletions pynitrokey/nk3/admin_app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import enum
import sys
from dataclasses import dataclass
from enum import Enum, IntFlag
from typing import Optional

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
Expand All @@ -18,6 +20,8 @@ class AdminCommand(Enum):
TEST_SE050 = 0x81
GET_CONFIG = 0x82
SET_CONFIG = 0x83
FACTORY_RESET = 0x84
FACTORY_RESET_APP = 0x85


@enum.unique
Expand Down Expand Up @@ -57,6 +61,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
Expand Down Expand Up @@ -148,3 +181,38 @@ 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")

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")
6 changes: 6 additions & 0 deletions pynitrokey/nk3/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ def uuid(self) -> Optional[Uuid]:
def version(self) -> Version:
return self.admin.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()

Expand Down

0 comments on commit cae3fc3

Please sign in to comment.