diff --git a/CHANGELOG.md b/CHANGELOG.md index 4095cf41..e6f02727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Unreleased ---------- - Enabled usage of empty PWS slot fields - Changed error reporting format to make up only a single line +- Added the `--only-aes-key` option to the `reset` command to build a new AES + key without performing a factory reset - Added `NITROCLI_RESOLVED_USB_PATH` environment variable to be used by extensions - Allowed entering of `base32` encoded strings containing spaces diff --git a/doc/nitrocli.1 b/doc/nitrocli.1 index 9b602de6..d3301248 100644 --- a/doc/nitrocli.1 +++ b/doc/nitrocli.1 @@ -1,4 +1,4 @@ -.TH NITROCLI 1 2021-04-14 +.TH NITROCLI 1 2021-04-17 .SH NAME nitrocli \- access Nitrokey devices .SH SYNOPSIS @@ -79,12 +79,16 @@ This command locks the password safe (see the Password safe section). On the Nitrokey Storage, it will also close any active encrypted or hidden volumes (see the Storage section). .TP -.B nitrocli reset +.B nitrocli reset \fR[\fB\-\-only-aes-key\fR] Perform a factory reset on the Nitrokey. This command performs a factory reset on the OpenPGP smart card, clears the flash storage and builds a new AES key. The user PIN is reset to 123456, the admin PIN to 12345678. +If the \fB\-\-only-aes-key\fR option is set, the command does not perform a +full factory reset but only creates a new AES key. +The AES key is for example used to encrypt the password safe. + This command requires the admin PIN. To avoid accidental calls of this command, the user has to enter the PIN even if it has been cached. diff --git a/doc/nitrocli.1.pdf b/doc/nitrocli.1.pdf index c9d9406f..296a8eda 100644 Binary files a/doc/nitrocli.1.pdf and b/doc/nitrocli.1.pdf differ diff --git a/src/args.rs b/src/args.rs index 4b1e21c2..62cf444b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -103,7 +103,7 @@ Command! { /// Accesses the password safe Pws(PwsArgs) => |ctx, args: PwsArgs| args.subcmd.execute(ctx), /// Performs a factory reset - Reset => crate::commands::reset, + Reset(ResetArgs) => |ctx, args: ResetArgs| crate::commands::reset(ctx, args.only_aes_key), /// Prints the status of the connected Nitrokey device Status => crate::commands::status, /// Interacts with the device's unencrypted volume @@ -445,6 +445,13 @@ pub struct PwsStatusArgs { pub all: bool, } +#[derive(Debug, PartialEq, structopt::StructOpt)] +pub struct ResetArgs { + /// Only build a new AES key instead of performing a full factory reset. + #[structopt(long)] + pub only_aes_key: bool, +} + #[derive(Debug, PartialEq, structopt::StructOpt)] pub struct UnencryptedArgs { #[structopt(subcommand)] diff --git a/src/commands.rs b/src/commands.rs index 3a4ff897..92574779 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -513,7 +513,7 @@ pub fn fill(ctx: &mut Context<'_>, attach: bool) -> anyhow::Result<()> { } /// Perform a factory reset. -pub fn reset(ctx: &mut Context<'_>) -> anyhow::Result<()> { +pub fn reset(ctx: &mut Context<'_>, only_aes_key: bool) -> anyhow::Result<()> { with_device(ctx, |ctx, mut device| { let pin_entry = pinentry::PinEntry::from(args::PinType::Admin, &device)?; @@ -522,20 +522,28 @@ pub fn reset(ctx: &mut Context<'_>) -> anyhow::Result<()> { pinentry::clear(&pin_entry).context("Failed to clear cached secret")?; try_with_pin(ctx, &pin_entry, |pin| { - device - .factory_reset(&pin) - .context("Failed to reset to factory settings")?; - // Work around for a timing issue between factory_reset and - // build_aes_key, see - // https://github.com/Nitrokey/nitrokey-storage-firmware/issues/80 - thread::sleep(time::Duration::from_secs(3)); - // Another work around for spurious WrongPassword returns of - // build_aes_key after a factory reset on Pro devices. - // https://github.com/Nitrokey/nitrokey-pro-firmware/issues/57 - let _ = device.get_user_retry_count(); - device - .build_aes_key(nitrokey::DEFAULT_ADMIN_PIN) - .context("Failed to rebuild AES key") + if only_aes_key { + // Similar to the else arm, we have to execute this command to avoid WrongPassword errors + let _ = device.get_user_retry_count(); + device + .build_aes_key(&pin) + .context("Failed to rebuild AES key") + } else { + device + .factory_reset(&pin) + .context("Failed to reset to factory settings")?; + // Work around for a timing issue between factory_reset and + // build_aes_key, see + // https://github.com/Nitrokey/nitrokey-storage-firmware/issues/80 + thread::sleep(time::Duration::from_secs(3)); + // Another work around for spurious WrongPassword returns of + // build_aes_key after a factory reset on Pro devices. + // https://github.com/Nitrokey/nitrokey-pro-firmware/issues/57 + let _ = device.get_user_retry_count(); + device + .build_aes_key(nitrokey::DEFAULT_ADMIN_PIN) + .context("Failed to rebuild AES key") + } }) }) } diff --git a/src/tests/reset.rs b/src/tests/reset.rs index 99342843..78fd13c7 100644 --- a/src/tests/reset.rs +++ b/src/tests/reset.rs @@ -1,6 +1,6 @@ // reset.rs -// Copyright (C) 2019-2020 The Nitrocli Developers +// Copyright (C) 2019-2021 The Nitrocli Developers // SPDX-License-Identifier: GPL-3.0-or-later use nitrokey::Authenticate; @@ -43,3 +43,59 @@ fn reset(model: nitrokey::Model) -> anyhow::Result<()> { Ok(()) } + +#[test_device] +fn reset_only_aes_key(model: nitrokey::Model) -> anyhow::Result<()> { + const NEW_USER_PIN: &str = "654321"; + const NAME: &str = "slotname"; + const LOGIN: &str = "sloglogin"; + const PASSWORD: &str = "slotpassword"; + + let mut ncli = Nitrocli::new().model(model).new_user_pin(NEW_USER_PIN); + + // Change the user PIN + let _ = ncli.handle(&["pin", "set", "user"])?; + + // Add an entry to the PWS + { + let mut manager = nitrokey::force_take()?; + let mut device = manager.connect_model(model)?; + let mut pws = device.get_password_safe(NEW_USER_PIN)?; + pws.write_slot(0, NAME, LOGIN, PASSWORD)?; + } + + // Build AES key + let mut ncli = Nitrocli::new().model(model); + let out = ncli.handle(&["reset", "--only-aes-key"])?; + assert!(out.is_empty()); + + // Check that 1) the password store works, i.e., there is an AES key, + // that 2) we can no longer access the stored data, i.e., the AES has + // been replaced, and that 3) the changed user PIN still works, i.e., + // we did not perform a factory reset. + { + let mut manager = nitrokey::force_take()?; + let mut device = manager.connect_model(model)?; + let pws = device.get_password_safe(NEW_USER_PIN)?; + let slot = pws.get_slot_unchecked(0)?; + + if let Ok(name) = slot.get_name() { + assert_ne!(NAME, &name); + } + if let Ok(login) = slot.get_login() { + assert_ne!(LOGIN, &login); + } + if let Ok(password) = slot.get_password() { + assert_ne!(PASSWORD, &password); + } + } + + // Reset the user PIN for other tests + let mut ncli = ncli + .user_pin(NEW_USER_PIN) + .new_user_pin(nitrokey::DEFAULT_USER_PIN); + let out = ncli.handle(&["pin", "set", "user"])?; + assert!(out.is_empty()); + + Ok(()) +}