From fc4a8e12af694a40fe17bcebddd9e4617075400f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 30 Dec 2018 18:39:31 +0100 Subject: Implement the pin unblock subcommand This patch implements the pin unblock command that unblocks and resets the user PIN. The name unblock is chosen over libnitrokey's unlock to be consistent with the GnuPG terminology and to avoid confusion with the unrelated lock command. --- nitrocli/README.md | 3 +++ nitrocli/doc/nitrocli.1 | 17 +++++++++++++++++ nitrocli/src/args.rs | 15 ++++++++++++++- nitrocli/src/commands.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ nitrocli/src/pinentry.rs | 10 +++++++++- 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/nitrocli/README.md b/nitrocli/README.md index 68e4da6..ed5e4e4 100644 --- a/nitrocli/README.md +++ b/nitrocli/README.md @@ -15,6 +15,8 @@ The following commands are currently supported: - config: Access the Nitrokey's configuration - get: Read the current configuration. - set: Change the configuration. +- pin: Change the Nitrokey’s PINs + - unblock: Unblock and reset the user PIN. - storage: Work with the Nitrokey's storage. - open: Open the encrypted volume. The user PIN needs to be entered. - close: Close the encrypted volume. @@ -26,6 +28,7 @@ The following commands are currently supported: - clear: Delete an OTP slot. - pin: Manage the Nitrokey's PINs. - clear: Remove the user and admin PIN from gpg-agent's cache. + - unblock: Unblock and reset the user PIN. ### *Note:* ---------------------------------------------------------------------- diff --git a/nitrocli/doc/nitrocli.1 b/nitrocli/doc/nitrocli.1 index ef56b22..bec9a15 100644 --- a/nitrocli/doc/nitrocli.1 +++ b/nitrocli/doc/nitrocli.1 @@ -124,11 +124,28 @@ PIN must have at least six, the admin PIN at least eight characters. The user PIN is required for commands such as \fBotp get\fR (depending on the configuration) and for all \fBpws\fR commands. The admin PIN is usually required to change the device configuration. +.P +Each PIN has a retry counter that is decreased with every wrong PIN entry and +reset if the PIN was entered correctly. +The initial retry counter is three. +If the retry counter for the user PIN is zero, you can use the +\fBpin unblock\fR command to unblock and reset the user PIN. +If the retry counter for the admin PIN is zero, you have to perform a factory +reset using \fBgpg\fR(1). +Use the \fBstatus\fR command to check the retry counters. .TP .B nitrocli pin clear Clear the PINs cached by the other commands. +.TP +.B nitrocli pin unblock +Unblock and reset the user PIN. +This command requires the admin PIN. +The admin PIN cannot be unblocked. +This operation is equivalent to the unblock PIN option provided by \fBgpg\fR(1) +(using the \fB\-\-change\-pin\fR option). + .SH EXAMPLES .SS One-time passwords Configure a one-time password slot with a hexadecimal secret representation: diff --git a/nitrocli/src/args.rs b/nitrocli/src/args.rs index edfd811..4341235 100644 --- a/nitrocli/src/args.rs +++ b/nitrocli/src/args.rs @@ -278,12 +278,14 @@ impl From for nitrokey::OtpMode { #[derive(Debug)] enum PinCommand { Clear, + Unblock, } impl PinCommand { fn execute(&self, args: Vec) -> Result<()> { match *self { PinCommand::Clear => pin_clear(args), + PinCommand::Unblock => pin_unblock(args), } } } @@ -295,6 +297,7 @@ impl fmt::Display for PinCommand { "{}", match *self { PinCommand::Clear => "clear", + PinCommand::Unblock => "unblock", } ) } @@ -306,6 +309,7 @@ impl str::FromStr for PinCommand { fn from_str(s: &str) -> result::Result { match s { "clear" => Ok(PinCommand::Clear), + "unblock" => Ok(PinCommand::Unblock), _ => Err(()), } } @@ -689,7 +693,7 @@ fn pin(args: Vec) -> Result<()> { let _ = parser.refer(&mut subcommand).required().add_argument( "subcommand", argparse::Store, - "The subcommand to execute (clear)", + "The subcommand to execute (clear|unblock)", ); let _ = parser.refer(&mut subargs).add_argument( "arguments", @@ -713,6 +717,15 @@ fn pin_clear(args: Vec) -> Result<()> { commands::pin_clear() } +/// Unblock and reset the user PIN. +fn pin_unblock(args: Vec) -> Result<()> { + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Unblocks and resets the user PIN"); + parse(&parser, args)?; + + commands::pin_unblock() +} + /// Parse the command-line arguments and return the selected command and /// the remaining arguments for the command. fn parse_arguments(args: Vec) -> Result<(Command, Vec)> { diff --git a/nitrocli/src/commands.rs b/nitrocli/src/commands.rs index 7f25415..e3e2a14 100644 --- a/nitrocli/src/commands.rs +++ b/nitrocli/src/commands.rs @@ -474,6 +474,50 @@ pub fn pin_clear() -> Result<()> { Ok(()) } +fn check_pin(pintype: pinentry::PinType, pin: &str) -> Result<()> { + let minimum_length = match pintype { + pinentry::PinType::Admin => 8, + pinentry::PinType::User => 6, + }; + if pin.len() < minimum_length { + Err(Error::Error(format!( + "The PIN must be at least {} characters long", + minimum_length + ))) + } else { + Ok(()) + } +} + +fn choose_pin(pintype: pinentry::PinType) -> Result { + pinentry::clear_passphrase(pintype)?; + let new_pin = pinentry::inquire_passphrase(pintype, pinentry::Mode::Choose, None)?; + pinentry::clear_passphrase(pintype)?; + let new_pin = String::from_utf8(new_pin)?; + check_pin(pintype, &new_pin)?; + + let confirm_pin = pinentry::inquire_passphrase(pintype, pinentry::Mode::Confirm, None)?; + pinentry::clear_passphrase(pintype)?; + let confirm_pin = String::from_utf8(confirm_pin)?; + + if new_pin != confirm_pin { + Err(Error::Error("Entered PINs do not match".to_string())) + } else { + Ok(new_pin) + } +} + +/// Unblock and reset the user PIN. +pub fn pin_unblock() -> Result<()> { + let device = get_device()?; + let user_pin = choose_pin(pinentry::PinType::User)?; + try_with_passphrase( + pinentry::PinType::Admin, + "Could not unblock the user PIN", + |admin_pin| device.unlock_user_pin(&admin_pin, &user_pin), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/nitrocli/src/pinentry.rs b/nitrocli/src/pinentry.rs index 90986be..33b5266 100644 --- a/nitrocli/src/pinentry.rs +++ b/nitrocli/src/pinentry.rs @@ -51,9 +51,13 @@ impl PinType { fn description(self, mode: Mode) -> &'static str { match self { PinType::Admin => match mode { + Mode::Choose => "Please enter a new admin PIN", + Mode::Confirm => "Please confirm the new admin PIN", Mode::Query => "Please enter the admin PIN", }, PinType::User => match mode { + Mode::Choose => "Please enter a new user PIN", + Mode::Confirm => "Please confirm the new user PIN", Mode::Query => "Please enter the user PIN", }, } @@ -67,13 +71,17 @@ impl PinType { /// quality bar is shown. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Mode { + /// Let the user choose a new PIN. + Choose, + /// Let the user confirm the previously chosen PIN. + Confirm, /// Query an existing PIN. Query, } impl Mode { fn show_quality_bar(self) -> bool { - false + self == Mode::Choose } } -- cgit v1.2.3