Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for full device factory reset command #472

Merged
merged 3 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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