From 1630c7872631f5f7e5bab599121df1fed26e47da Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 23 Dec 2018 02:08:47 +0100 Subject: Implement the otp set subcommand This patch implements the `otp set` subcommand that configures an OTP slot. There are two ways to specify an OTP secret: as a hexadecimal string (that means that every two characters are interpreted as a hexadecimal representation of one byte of the secret) or as an ASCII string (that means that the ASCII code of every character is interpreted as one byte of the secret). As the HOTP RFC mentions both representations, this implementation supports both. --- nitrocli/src/args.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++- nitrocli/src/commands.rs | 67 +++++++++++++++++++++++++++- 2 files changed, 177 insertions(+), 2 deletions(-) diff --git a/nitrocli/src/args.rs b/nitrocli/src/args.rs index f4035e6..c27fbc2 100644 --- a/nitrocli/src/args.rs +++ b/nitrocli/src/args.rs @@ -84,12 +84,14 @@ impl str::FromStr for Command { #[derive(Debug)] enum OtpCommand { Get, + Set, } impl OtpCommand { fn execute(&self, args: Vec) -> Result<()> { match *self { OtpCommand::Get => otp_get(args), + OtpCommand::Set => otp_set(args), } } } @@ -101,6 +103,7 @@ impl fmt::Display for OtpCommand { "{}", match *self { OtpCommand::Get => "get", + OtpCommand::Set => "set", } ) } @@ -112,6 +115,7 @@ impl str::FromStr for OtpCommand { fn from_str(s: &str) -> std::result::Result { match s { "get" => Ok(OtpCommand::Get), + "set" => Ok(OtpCommand::Set), _ => Err(()), } } @@ -148,6 +152,46 @@ impl str::FromStr for OtpAlgorithm { } } +#[derive(Clone, Copy, Debug)] +enum OtpMode { + SixDigits, + EightDigits, +} + +impl fmt::Display for OtpMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match *self { + OtpMode::SixDigits => "6", + OtpMode::EightDigits => "8", + } + ) + } +} + +impl str::FromStr for OtpMode { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + match s { + "6" => Ok(OtpMode::SixDigits), + "8" => Ok(OtpMode::EightDigits), + _ => Err(()), + } + } +} + +impl From for nitrokey::OtpMode { + fn from(mode: OtpMode) -> Self { + match mode { + OtpMode::SixDigits => nitrokey::OtpMode::SixDigits, + OtpMode::EightDigits => nitrokey::OtpMode::EightDigits, + } + } +} + fn parse(parser: &argparse::ArgumentParser<'_>, args: Vec) -> Result<()> { if let Err(err) = parser.parse(args, &mut io::stdout(), &mut io::stderr()) { Err(Error::ArgparseError(err)) @@ -201,7 +245,7 @@ fn otp(args: Vec) -> Result<()> { let _ = parser.refer(&mut subcommand).required().add_argument( "subcommand", argparse::Store, - "The subcommand to execute (get)", + "The subcommand to execute (get|set)", ); let _ = parser.refer(&mut subargs).add_argument( "arguments", @@ -238,6 +282,72 @@ fn otp_get(args: Vec) -> Result<()> { commands::otp_get(slot, algorithm) } +/// Configure a one-time password slot on the Nitrokey device. +pub fn otp_set(args: Vec) -> Result<()> { + let mut slot: u8 = 0; + let mut algorithm = OtpAlgorithm::Totp; + let mut name = "".to_owned(); + let mut secret = "".to_owned(); + let mut digits = OtpMode::SixDigits; + let mut counter: u64 = 0; + let mut time_window: u16 = 30; + let mut ascii = false; + let mut parser = argparse::ArgumentParser::new(); + parser.set_description("Configures a one-time password slot"); + let _ = + parser + .refer(&mut slot) + .required() + .add_argument("slot", argparse::Store, "The OTP slot to use"); + let _ = parser.refer(&mut algorithm).add_option( + &["-a", "--algorithm"], + argparse::Store, + "The OTP algorithm to use (hotp or totp, default: totp", + ); + let _ = parser.refer(&mut name).required().add_argument( + "name", + argparse::Store, + "The name of the slot", + ); + let _ = parser.refer(&mut secret).required().add_argument( + "secret", + argparse::Store, + "The secret to store on the slot as a hexadecimal string (unless --ascii is set)", + ); + let _ = parser.refer(&mut digits).add_option( + &["-d", "--digits"], + argparse::Store, + "The number of digits to use for the one-time password (6 or 8, default: 6)", + ); + let _ = parser.refer(&mut counter).add_option( + &["-c", "--counter"], + argparse::Store, + "The counter value for HOTP (default: 0)", + ); + let _ = parser.refer(&mut time_window).add_option( + &["-t", "--time-window"], + argparse::Store, + "The time window for TOTP (default: 30)", + ); + let _ = parser.refer(&mut ascii).add_option( + &["--ascii"], + argparse::StoreTrue, + "Interpret the given secret as an ASCII string of the secret", + ); + parse(&parser, args)?; + drop(parser); + + let data = nitrokey::OtpSlotData { + number: slot, + name, + secret, + mode: nitrokey::OtpMode::from(digits), + use_enter: false, + token_id: None, + }; + commands::otp_set(data, algorithm, counter, time_window, ascii) +} + /// 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 a82734e..8dc8c42 100644 --- a/nitrocli/src/commands.rs +++ b/nitrocli/src/commands.rs @@ -19,6 +19,7 @@ use std::result; +use nitrokey::ConfigureOtp; use nitrokey::Device; use nitrokey::GenerateOtp; @@ -73,7 +74,6 @@ where } /// Authenticate the given device with the admin PIN. -#[allow(unused)] fn authenticate_admin(device: T) -> Result> where T: Device, @@ -276,3 +276,68 @@ pub fn otp_get(slot: u8, algorithm: args::OtpAlgorithm) -> Result<()> { println!("{}", otp); Ok(()) } + +/// Prepare an ASCII secret string for libnitrokey. +/// +/// libnitrokey expects secrets as hexadecimal strings. This function transforms an ASCII string +/// into a hexadecimal string or returns an error if the given string contains non-ASCII +/// characters. +fn prepare_secret(secret: &str) -> Result { + if secret.is_ascii() { + Ok( + secret + .as_bytes() + .iter() + .map(|c| format!("{:x}", c)) + .collect::>() + .join(""), + ) + } else { + Err(Error::Error( + "The given secret is not an ASCII string despite --ascii being set".to_string(), + )) + } +} + +/// Configure a one-time password slot on the Nitrokey device. +pub fn otp_set( + data: nitrokey::OtpSlotData, + algorithm: args::OtpAlgorithm, + counter: u64, + time_window: u16, + ascii: bool, +) -> Result<()> { + let secret = if ascii { + prepare_secret(&data.secret)? + } else { + data.secret + }; + let data = nitrokey::OtpSlotData { secret, ..data }; + let device = authenticate_admin(get_device()?)?; + match algorithm { + args::OtpAlgorithm::Hotp => device.write_hotp_slot(data, counter), + args::OtpAlgorithm::Totp => device.write_totp_slot(data, time_window), + } + .map_err(|err| get_error("Could not write OTP slot", &err))?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn prepare_secret_ascii() { + let result = prepare_secret("12345678901234567890"); + assert_eq!( + "3132333435363738393031323334353637383930".to_string(), + result.unwrap() + ); + } + + #[test] + fn prepare_secret_non_ascii() { + let result = prepare_secret("Österreich"); + assert!(result.is_err()); + } +} -- cgit v1.2.3