Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --format option and base32 support for otp set #49

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nitrocli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Unreleased
level
- Added the `-m` and `--model` options to restrict connections to
a device model
- Added the `-f` and `--format` options for the `otp set` subcommand to choose
the secret format, deprecating the `--ascii` option
- Adjust release build compile options to optimize binary for size
- Bumped `nitrokey` dependency to `0.2.3`
- Bumped `rand` dependency to `0.6.1`
Expand Down
7 changes: 7 additions & 0 deletions nitrocli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions nitrocli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ incremental = false
version = "0.2.2"
path = "../argparse"

[dependencies.base32]
version = "0.4.0"

[dependencies.libc]
version = "0.2"
path = "../libc"
Expand Down
27 changes: 19 additions & 8 deletions nitrocli/doc/nitrocli.1
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,21 @@ This command might require the user PIN (see the Configuration section).
\fBnitrocli otp set \fIslot name secret \
\fR[\fB\-a\fR|\fB\-\-algorithm \fIalgorithm\fR] \
[\fB\-d\fR|\fB\-\-digits \fIdigits\fR] [\fB\-c\fR|\fB\-\-counter \fIcounter\fR] \
[\fB\-t\fR|\fB\-\-time-window \fItime window\fR] [\fB\-\-ascii\fR]
[\fB\-t\fR|\fB\-\-time-window \fItime window\fR] \
[\fB-f\fR|\fB\-\-format ascii\fR|\fBbase32\fR|\fBhex\fR]
Configure a one-time password slot.
\fIslot\fR is the number of the slot to configure.
\fIname\fR is the name of the slot (may not be empty).
\fIsecret\fR is the secret value to store in that slot.
If \fB\-\-ascii\fR is set, each character of the given secret is interpreted as
the ASCII code of one byte.
Otherwise, every two characters are interpreted as the hexadecimal value of one
byte.

The \fB\-\-format\fR option specifies the format of the secret.
If it is set to \fBascii\fR, each character of the given secret is interpreted
as the ASCII code of one byte.
If it is set to \fBbase32\fR, the secret is interpreted as a base32 string
according to RFC 4648.
If it is set to \fBhex\fR, every two characters are interpreted as the
hexadecimal value of one byte.
The default value is \fBhex\fR.

\fIalgorithm\fR is the OTP algorithm to use.
Possible values are \fBhotp\fR for the HOTP algorithm according to RFC 4226 and
Expand Down Expand Up @@ -222,9 +228,14 @@ Configure a one-time password slot with a hexadecimal secret representation:
$ \fBnitrocli otp set 0 test\-rfc6238 3132333435363738393031323334353637383930 \-\-algorithm totp \-\-digits 8\fR
.P
Configure a one-time password slot with an ASCII secret representation:
$ \fBnitrocli otp set 0 test\-rfc4226 12345678901234567890 \-\-ascii \-\-algorithm hotp\fR
$ \fBnitrocli otp set 1 test\-foobar foobar \-\-ascii \-\-algorithm hotp\fR
$ \fBnitrocli otp set 0 test\-rfc6238 12345678901234567890 \-\-ascii \-\-algorithm totp \-\-digits 8\fR
$ \fBnitrocli otp set 0 test\-rfc4226 12345678901234567890 \-\-format ascii \-\-algorithm hotp\fR
$ \fBnitrocli otp set 1 test\-foobar foobar \-\-format ascii \-\-algorithm hotp\fR
$ \fBnitrocli otp set 0 test\-rfc6238 12345678901234567890 \-\-format ascii \-\-algorithm totp \-\-digits 8\fR
.P
Configure a one-time password slot with a base32 secret representation:
$ \fBnitrocli otp set 0 test\-rfc4226 gezdgnbvgy3tqojqgezdgnbvgy3tqojq \-\-format base32 \-\-algorithm hotp\fR
$ \fBnitrocli otp set 1 test\-foobar mzxw6ytboi====== \-\-format base32 \-\-algorithm hotp\fR
$ \fBnitrocli otp set 0 test\-rfc6238 gezdgnbvgy3tqojqgezdgnbvgy3tqojq \-\-format base32 \-\-algorithm totp \-\-digits 8\fR
.P
Generate a one-time password:
$ \fBnitrocli otp get 0 \-\-algorithm hotp\fR
Expand Down
Binary file modified nitrocli/doc/nitrocli.1.pdf
Binary file not shown.
56 changes: 54 additions & 2 deletions nitrocli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,40 @@ impl From<OtpMode> for nitrokey::OtpMode {
}
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OtpSecretFormat {
Ascii,
Base32,
Hex,
}

impl fmt::Display for OtpSecretFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match *self {
OtpSecretFormat::Ascii => "ascii",
OtpSecretFormat::Base32 => "base32",
OtpSecretFormat::Hex => "hex",
}
)
}
}

impl str::FromStr for OtpSecretFormat {
type Err = ();

fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s {
"ascii" => Ok(OtpSecretFormat::Ascii),
"base32" => Ok(OtpSecretFormat::Base32),
"hex" => Ok(OtpSecretFormat::Hex),
_ => Err(()),
}
}
}

#[derive(Debug)]
enum PinCommand {
Clear,
Expand Down Expand Up @@ -700,6 +734,7 @@ pub fn otp_set(ctx: &ExecCtx, args: Vec<String>) -> Result<()> {
let mut counter: u64 = 0;
let mut time_window: u16 = 30;
let mut ascii = false;
let mut secret_format: Option<OtpSecretFormat> = None;
let mut parser = argparse::ArgumentParser::new();
parser.set_description("Configures a one-time password slot");
let _ =
Expand Down Expand Up @@ -740,11 +775,28 @@ pub fn otp_set(ctx: &ExecCtx, args: Vec<String>) -> Result<()> {
let _ = parser.refer(&mut ascii).add_option(
&["--ascii"],
argparse::StoreTrue,
"Interpret the given secret as an ASCII string of the secret",
"Interpret the given secret as an ASCII string of the secret (deprecated, use --format instead)"
);
let _ = parser.refer(&mut secret_format).add_option(
&["-f", "--format"],
argparse::StoreOption,
"The format of the secret (ascii|base32|hex)",
);
parse(&parser, args)?;
drop(parser);

if ascii {
if secret_format.is_some() {
return Err(Error::Error(
"The --format and the --ascii option cannot be used at the same time".to_string(),
));
}

println!("Warning: The --ascii option is deprecated. Please use --format ascii instead.");
secret_format = Some(OtpSecretFormat::Ascii);
}
let secret_format = secret_format.unwrap_or(OtpSecretFormat::Hex);

let data = nitrokey::OtpSlotData {
number: slot,
name,
Expand All @@ -753,7 +805,7 @@ pub fn otp_set(ctx: &ExecCtx, args: Vec<String>) -> Result<()> {
use_enter: false,
token_id: None,
};
commands::otp_set(ctx, data, algorithm, counter, time_window, ascii)
commands::otp_set(ctx, data, algorithm, counter, time_window, secret_format)
}

/// Clear an OTP slot.
Expand Down
43 changes: 26 additions & 17 deletions nitrocli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,41 +430,50 @@ pub fn otp_get(
Ok(())
}

/// Format a bytes vector as a hex string.
fn format_bytes(bytes: &[u8]) -> String {
bytes
.iter()
.map(|c| format!("{:x}", c))
.collect::<Vec<String>>()
.join("")
}

/// 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> {
fn prepare_ascii_secret(secret: &str) -> Result<String> {
if secret.is_ascii() {
Ok(
secret
.as_bytes()
.iter()
.map(|c| format!("{:x}", c))
.collect::<Vec<String>>()
.join(""),
)
Ok(format_bytes(&secret.as_bytes()))
} else {
Err(Error::Error(
"The given secret is not an ASCII string despite --ascii being set".to_string(),
"The given secret is not an ASCII string despite --format ascii being set".to_string(),
))
}
}

/// Prepare a base32 secret string for libnitrokey.
fn prepare_base32_secret(secret: &str) -> Result<String> {
base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret)
.map(|vec| format_bytes(&vec))
.ok_or_else(|| Error::Error("Could not parse base32 secret".to_string()))
}

/// Configure a one-time password slot on the Nitrokey device.
pub fn otp_set(
ctx: &args::ExecCtx,
data: nitrokey::OtpSlotData,
algorithm: args::OtpAlgorithm,
counter: u64,
time_window: u16,
ascii: bool,
secret_format: args::OtpSecretFormat,
) -> Result<()> {
let secret = if ascii {
prepare_secret(&data.secret)?
} else {
data.secret
let secret = match secret_format {
args::OtpSecretFormat::Ascii => prepare_ascii_secret(&data.secret)?,
args::OtpSecretFormat::Base32 => prepare_base32_secret(&data.secret)?,
args::OtpSecretFormat::Hex => data.secret,
};
let data = nitrokey::OtpSlotData { secret, ..data };
let device = authenticate_admin(get_device(ctx)?)?;
Expand Down Expand Up @@ -698,7 +707,7 @@ mod tests {

#[test]
fn prepare_secret_ascii() {
let result = prepare_secret("12345678901234567890");
let result = prepare_ascii_secret("12345678901234567890");
assert_eq!(
"3132333435363738393031323334353637383930".to_string(),
result.unwrap()
Expand All @@ -707,7 +716,7 @@ mod tests {

#[test]
fn prepare_secret_non_ascii() {
let result = prepare_secret("Österreich");
let result = prepare_ascii_secret("Österreich");
assert!(result.is_err());
}
}