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()); +  } +} | 
