diff options
author | Robin Krahl <robin.krahl@ireas.org> | 2018-12-23 02:08:47 +0100 |
---|---|---|
committer | Daniel Mueller <deso@posteo.net> | 2018-12-24 18:15:28 -0800 |
commit | 1630c7872631f5f7e5bab599121df1fed26e47da (patch) | |
tree | d831a9029f970731534cfdef9c05bb9196fe096a | |
parent | 86b3170b1dd4c1955e540f5c914a317302754be1 (diff) | |
download | nitrocli-1630c7872631f5f7e5bab599121df1fed26e47da.tar.gz nitrocli-1630c7872631f5f7e5bab599121df1fed26e47da.tar.bz2 |
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.
-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()); + } +} |