diff options
-rw-r--r-- | nitrocli/src/args.rs | 112 | ||||
-rw-r--r-- | 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<String>) -> 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<Self, Self::Err> { 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<Self, Self::Err> { + match s { + "6" => Ok(OtpMode::SixDigits), + "8" => Ok(OtpMode::EightDigits), + _ => Err(()), + } + } +} + +impl From<OtpMode> 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<String>) -> 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<String>) -> 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<String>) -> Result<()> { commands::otp_get(slot, algorithm) } +/// Configure a one-time password slot on the Nitrokey device. +pub fn otp_set(args: Vec<String>) -> 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<String>) -> Result<(Command, Vec<String>)> { 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<T>(device: T) -> Result<nitrokey::Admin<T>> 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<String> { + if secret.is_ascii() { + Ok( + secret + .as_bytes() + .iter() + .map(|c| format!("{:x}", c)) + .collect::<Vec<String>>() + .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()); + } +} |