From d07d1d8b212048d652ea0c6470c18f97b1309f8d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 12 Apr 2021 20:27:11 +0200 Subject: [PATCH 1/9] Pass on resolved USB path to extensions To avoid reimplementing the device selection logic in extensions, we introduce a new environment variable NITROCLI_RESOLVED_USB_PATH that is set to the USB path of the single matching Nitrokey device. If no device matches, or if there are multiple matching devices, the variable is not set. --- doc/nitrocli.1 | 8 +++++++- src/commands.rs | 4 ++++ src/main.rs | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/nitrocli.1 b/doc/nitrocli.1 index 832c90d9..934411de 100644 --- a/doc/nitrocli.1 +++ b/doc/nitrocli.1 @@ -427,12 +427,18 @@ The program conveys basic configuration information to any extension being started this way. Specifically, it will set each environment variable as described in the Configuration subsection of the Environment section above, if the corresponding \fBnitrocli\fR program configuration was set. In addition, the -following variable will be set unconditionally: +following variable will be set: .TP .B NITROCLI_BINARY The absolute path to the \fBnitrocli\fR binary through which the extension was invoked. This path may be used to recursively invoke \fBnitrocli\fR to implement certain functionality. +.TP +.B NITROCLI_RESOLVED_USB_PATH +The USB path of the device that \fBnitrocli\fR would connect to based on the +\fB\-\-model\fR, \fB\-\-serial-number\fR and \fB\-\-usb-path\fR options. +If there is no matching Nitrokey device, or if multiple devices match the +options, the environment variable is not set. .P All other variables present in the environment will be passed through to the diff --git a/src/commands.rs b/src/commands.rs index 8a1604b9..3a4ff897 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1170,6 +1170,10 @@ pub fn extension(ctx: &mut Context<'_>, args: Vec) -> anyhow::Res // a cargo test context. let mut cmd = process::Command::new(&ext_path); + if let Ok(device_info) = find_device(&ctx.config) { + let _ = cmd.env(crate::NITROCLI_RESOLVED_USB_PATH, device_info.path); + } + if let Some(model) = ctx.config.model { let _ = cmd.env(crate::NITROCLI_MODEL, model.to_string()); } diff --git a/src/main.rs b/src/main.rs index d1939346..2e7bb4dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,6 +74,7 @@ use structopt::clap::SubCommand; use structopt::StructOpt; const NITROCLI_BINARY: &str = "NITROCLI_BINARY"; +const NITROCLI_RESOLVED_USB_PATH: &str = "NITROCLI_RESOLVED_USB_PATH"; const NITROCLI_MODEL: &str = "NITROCLI_MODEL"; const NITROCLI_USB_PATH: &str = "NITROCLI_USB_PATH"; const NITROCLI_VERBOSITY: &str = "NITROCLI_VERBOSITY"; From 176f751bb36a2b6cb9f7d71b7cd9d5f3ff01cf09 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 12 Apr 2021 20:52:46 +0200 Subject: [PATCH 2/9] Add extension support crate nitrocli-ext This patch adds the extension support crate nitrocli-ext as a workspace member. This crate contains useful methods for extensions written in Rust, providing access to the nitrocli binary and to the nitrokey-rs library. --- Cargo.lock | 9 +++ Cargo.toml | 3 + ext/ext/Cargo.toml | 15 +++++ ext/ext/src/lib.rs | 141 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 ext/ext/Cargo.toml create mode 100644 ext/ext/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a644916e..1d0ea936 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,15 @@ dependencies = [ "toml", ] +[[package]] +name = "nitrocli-ext" +version = "0.1.0" +dependencies = [ + "anyhow", + "directories", + "nitrokey", +] + [[package]] name = "nitrokey" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 6bb42e53..8b888a03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,3 +82,6 @@ version = "1" [dev-dependencies.tempfile] version = "3.1" + +[workspace] +members = ["ext/*"] diff --git a/ext/ext/Cargo.toml b/ext/ext/Cargo.toml new file mode 100644 index 00000000..e2ddcfb6 --- /dev/null +++ b/ext/ext/Cargo.toml @@ -0,0 +1,15 @@ +# Cargo.toml + +# Copyright (C) 2021 The Nitrocli Developers +# SPDX-License-Identifier: GPL-3.0-or-later + +[package] +name = "nitrocli-ext" +version = "0.1.0" +authors = ["Robin Krahl "] +edition = "2018" + +[dependencies] +anyhow = "1" +directories = "3" +nitrokey = "0.9" diff --git a/ext/ext/src/lib.rs b/ext/ext/src/lib.rs new file mode 100644 index 00000000..0b1a8d17 --- /dev/null +++ b/ext/ext/src/lib.rs @@ -0,0 +1,141 @@ +// lib.rs + +// Copyright (C) 2020-2021 The Nitrocli Developers +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::env; +use std::ffi; +use std::process; + +use anyhow::Context as _; + +#[derive(Debug)] +pub struct Context { + pub nitrocli: ffi::OsString, + pub resolved_usb_path: Option, + pub verbosity: Option, + pub project_dirs: directories::ProjectDirs, +} + +impl Context { + pub fn from_env(name: &str) -> anyhow::Result { + let nitrocli = env::var_os("NITROCLI_BINARY") + .context("NITROCLI_BINARY environment variable not present") + .context("Failed to retrieve nitrocli path")?; + + let resolved_usb_path = env::var("NITROCLI_RESOLVED_USB_PATH").ok(); + + let verbosity = env::var_os("NITROCLI_VERBOSITY") + .context("NITROCLI_VERBOSITY environment variable not present") + .context("Failed to retrieve nitrocli verbosity")?; + let verbosity = if verbosity.len() == 0 { + None + } else { + let verbosity = verbosity + .to_str() + .context("Provided verbosity string is not valid UTF-8")?; + let verbosity = u8::from_str_radix(verbosity, 10).context("Failed to parse verbosity")?; + set_log_level(verbosity); + Some(verbosity) + }; + + let project_dirs = directories::ProjectDirs::from("", "", name).with_context(|| { + format!( + "Could not determine the application directories for the {} extension", + name + ) + })?; + + Ok(Self { + nitrocli, + resolved_usb_path, + verbosity, + project_dirs, + }) + } + + pub fn nitrocli(&self) -> Nitrocli { + Nitrocli::from_context(self) + } + + pub fn connect<'mgr>( + &self, + mgr: &'mgr mut nitrokey::Manager, + ) -> anyhow::Result> { + if let Some(usb_path) = &self.resolved_usb_path { + mgr.connect_path(usb_path.to_owned()).map_err(From::from) + } else { + // TODO: Improve error message. Unfortunately, we can’t easily determine whether we have no + // or more than one (matching) device. + Err(anyhow::anyhow!("Could not connect to Nitrokey device")) + } + } +} + +// See src/command.rs in nitrocli core. +fn set_log_level(verbosity: u8) { + let log_lvl = match verbosity { + // The error log level is what libnitrokey uses by default. As such, + // there is no harm in us setting that as well when the user did not + // ask for higher verbosity. + 0 => nitrokey::LogLevel::Error, + 1 => nitrokey::LogLevel::Warning, + 2 => nitrokey::LogLevel::Info, + 3 => nitrokey::LogLevel::DebugL1, + 4 => nitrokey::LogLevel::Debug, + _ => nitrokey::LogLevel::DebugL2, + }; + nitrokey::set_log_level(log_lvl); +} + +#[derive(Debug)] +pub struct Nitrocli { + cmd: process::Command, +} + +impl Nitrocli { + pub fn from_context(ctx: &Context) -> Nitrocli { + let mut cmd = process::Command::new(&ctx.nitrocli); + if let Some(verbosity) = ctx.verbosity { + for _ in 0..verbosity { + cmd.arg("--verbose"); + } + } + Self { cmd } + } + + pub fn arg(&mut self, arg: impl AsRef) -> &mut Nitrocli { + self.cmd.arg(arg); + self + } + + pub fn args(&mut self, args: I) -> &mut Nitrocli + where + I: IntoIterator, + S: AsRef, + { + self.cmd.args(args); + self + } + + pub fn text(&mut self) -> anyhow::Result { + let output = self.cmd.output().context("Failed to invoke nitrocli")?; + // We want additional nitrocli emitted output to be visible to the + // user (typically controlled through -v/--verbose below). Note that + // this means that we will not be able to access this output for + // error reporting purposes. + self.cmd.stderr(process::Stdio::inherit()); + + if output.status.success() { + String::from_utf8(output.stdout).map_err(From::from) + } else { + Err(anyhow::anyhow!("nitrocli call failed")) + } + } + + pub fn spawn(&mut self) -> anyhow::Result<()> { + let mut child = self.cmd.spawn().context("Failed to invoke nitrocli")?; + child.wait().context("Failed to wait on nitrocli")?; + Ok(()) + } +} From f0beb5642ca8cfaab713b60033e8e1711b07166d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 10 Apr 2021 07:15:34 +0200 Subject: [PATCH 3/9] Add otp-cache extension This patch adds the nitrocli-otp-cache extension that caches OTP data. The per-device cache stores the names, OTP algorithms and IDs of the slots It can be used to access the slots by name instead of slot index. --- Cargo.lock | 12 +++ Cargo.toml | 6 +- ext/otp-cache/Cargo.toml | 21 +++++ ext/otp-cache/src/main.rs | 174 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 ext/otp-cache/Cargo.toml create mode 100644 ext/otp-cache/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1d0ea936..bf9cef1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,18 @@ dependencies = [ "nitrokey", ] +[[package]] +name = "nitrocli-otp-cache" +version = "0.1.0" +dependencies = [ + "anyhow", + "nitrocli-ext", + "nitrokey", + "serde", + "structopt", + "toml", +] + [[package]] name = "nitrokey" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 8b888a03..a4c3624d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,12 +39,12 @@ version = "1.0" [dependencies.base32] version = "0.4.0" -[dependencies.envy] -version = "0.4.2" - [dependencies.directories] version = "3" +[dependencies.envy] +version = "0.4.2" + [dependencies.libc] version = "0.2" diff --git a/ext/otp-cache/Cargo.toml b/ext/otp-cache/Cargo.toml new file mode 100644 index 00000000..969960f7 --- /dev/null +++ b/ext/otp-cache/Cargo.toml @@ -0,0 +1,21 @@ +# Cargo.toml + +# Copyright (C) 2020-2021 The Nitrocli Developers +# SPDX-License-Identifier: GPL-3.0-or-later + +[package] +name = "nitrocli-otp-cache" +version = "0.1.0" +authors = ["Robin Krahl "] +edition = "2018" + +[dependencies] +anyhow = "1" +nitrokey = "0.9" +serde = { version = "1", features = ["derive"] } +structopt = { version = "0.3.17", default-features = false } +toml = "0.5" + +[dependencies.nitrocli-ext] +version = "0.1" +path = "../ext" diff --git a/ext/otp-cache/src/main.rs b/ext/otp-cache/src/main.rs new file mode 100644 index 00000000..43c816e6 --- /dev/null +++ b/ext/otp-cache/src/main.rs @@ -0,0 +1,174 @@ +// main.rs + +// Copyright (C) 2020-2021 The Nitrocli Developers +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::fs; +use std::io; +use std::path; + +use anyhow::Context as _; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Cache { + hotp: Vec, + totp: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Slot { + name: String, + id: u8, +} + +/// Access Nitrokey OTP slots by name +#[derive(Debug, structopt::StructOpt)] +#[structopt(bin_name = "nitrocli cache")] +struct Args { + #[structopt(subcommand)] + cmd: Command, +} + +#[derive(Debug, structopt::StructOpt)] +enum Command { + /// Generates a one-time passwords + Get { + /// The name of the OTP slot to generate a OTP from + name: String, + }, + /// Lists the cached slots and their names + List, + /// Updates the cached slot data + Update, +} + +fn main() -> anyhow::Result<()> { + use structopt::StructOpt as _; + + let args = Args::from_args(); + let ctx = nitrocli_ext::Context::from_env("nitrocli-otp-cache")?; + + let mut mgr = nitrokey::take()?; + let device = ctx.connect(&mut mgr)?; + + let serial_number = get_serial_number(&device)?; + let cache_file = ctx + .project_dirs + .cache_dir() + .join(&format!("{}.toml", serial_number)); + + match &args.cmd { + Command::Get { name } => { + drop(device); + drop(mgr); + cmd_get(&ctx, &cache_file, name) + } + Command::List => cmd_list(&cache_file), + Command::Update => cmd_update(&cache_file, &device), + } +} + +fn cmd_get( + ctx: &nitrocli_ext::Context, + cache_file: &path::Path, + slot_name: &str, +) -> anyhow::Result<()> { + let cache = get_cache(cache_file)?; + let totp_slots: Vec<_> = cache.totp.iter().filter(|s| s.name == slot_name).collect(); + let hotp_slots: Vec<_> = cache.hotp.iter().filter(|s| s.name == slot_name).collect(); + if totp_slots.len() + hotp_slots.len() > 1 { + Err(anyhow::anyhow!("Multiple OTP slots with the given name")) + } else if let Some(slot) = totp_slots.first() { + generate_otp(&ctx, "totp", slot.id) + } else if let Some(slot) = hotp_slots.first() { + generate_otp(&ctx, "hotp", slot.id) + } else { + Err(anyhow::anyhow!("No OTP slot with the given name")) + } +} + +fn cmd_list(cache_file: &path::Path) -> anyhow::Result<()> { + let cache = get_cache(&cache_file)?; + println!("alg\tslot\tname"); + for slot in cache.totp { + println!("totp\t{}\t{}", slot.id, slot.name); + } + for slot in cache.hotp { + println!("hotp\t{}\t{}", slot.id, slot.name); + } + Ok(()) +} + +fn cmd_update(cache_file: &path::Path, device: &impl nitrokey::GenerateOtp) -> anyhow::Result<()> { + save_cache(&get_otp_slots(device)?, &cache_file) +} + +fn get_cache(file: &path::Path) -> anyhow::Result { + if !file.is_file() { + anyhow::bail!("There is no cached slot data. Run the update command to initialize the cache."); + } + load_cache(&file) +} + +fn load_cache(path: &path::Path) -> anyhow::Result { + let s = fs::read_to_string(path).context("Failed to read cache file")?; + toml::from_str(&s).context("Failed to parse cache file") +} + +fn save_cache(cache: &Cache, path: &path::Path) -> anyhow::Result<()> { + use io::Write as _; + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).context("Failed to create cache parent directory")?; + } + let mut f = fs::File::create(path).context("Failed to create cache file")?; + let data = toml::to_vec(cache).context("Failed to serialize cache")?; + f.write_all(&data).context("Failed to write cache file")?; + Ok(()) +} + +fn get_serial_number<'a>(device: &impl nitrokey::Device<'a>) -> anyhow::Result { + // TODO: Consider using hidapi serial number (if available) + Ok(device.get_serial_number()?.to_string().to_lowercase()) +} + +fn get_otp_slots_fn(device: &D, f: F) -> anyhow::Result> +where + D: nitrokey::GenerateOtp, + F: Fn(&D, u8) -> Result, +{ + let mut slots = Vec::new(); + let mut slot: u8 = 0; + loop { + let result = f(device, slot); + slot = slot + .checked_add(1) + .context("Encountered integer overflow when iterating OTP slots")?; + match result { + Ok(name) => { + slots.push(Slot { name, id: slot }); + } + Err(nitrokey::Error::LibraryError(nitrokey::LibraryError::InvalidSlot)) => break, + Err(nitrokey::Error::CommandError(nitrokey::CommandError::SlotNotProgrammed)) => {} + Err(err) => return Err(err).context("Failed to check OTP slot"), + } + } + Ok(slots) +} + +fn get_otp_slots(device: &impl nitrokey::GenerateOtp) -> anyhow::Result { + Ok(Cache { + totp: get_otp_slots_fn(device, |device, slot| device.get_totp_slot_name(slot))?, + hotp: get_otp_slots_fn(device, |device, slot| device.get_hotp_slot_name(slot))?, + }) +} + +fn generate_otp(ctx: &nitrocli_ext::Context, algorithm: &str, slot: u8) -> anyhow::Result<()> { + ctx + .nitrocli() + .args(&["otp", "get"]) + .arg(slot.to_string()) + .arg("--algorithm") + .arg(algorithm) + .spawn() +} From 380d328ce6469964509d27d7658b5f3656818a39 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 12 Apr 2021 21:59:41 +0200 Subject: [PATCH 4/9] fixup! Add otp-cache extension --- ext/otp-cache/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/otp-cache/src/main.rs b/ext/otp-cache/src/main.rs index 43c816e6..5c9d8216 100644 --- a/ext/otp-cache/src/main.rs +++ b/ext/otp-cache/src/main.rs @@ -141,9 +141,6 @@ where let mut slot: u8 = 0; loop { let result = f(device, slot); - slot = slot - .checked_add(1) - .context("Encountered integer overflow when iterating OTP slots")?; match result { Ok(name) => { slots.push(Slot { name, id: slot }); @@ -152,6 +149,9 @@ where Err(nitrokey::Error::CommandError(nitrokey::CommandError::SlotNotProgrammed)) => {} Err(err) => return Err(err).context("Failed to check OTP slot"), } + slot = slot + .checked_add(1) + .context("Encountered integer overflow when iterating OTP slots")?; } Ok(slots) } From a2d882570936198de5221258ca8201705a635012 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 13 Apr 2021 21:40:53 +0200 Subject: [PATCH 5/9] fixup! Add otp-cache extension --- ext/otp-cache/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/otp-cache/src/main.rs b/ext/otp-cache/src/main.rs index 5c9d8216..f8f8c6fa 100644 --- a/ext/otp-cache/src/main.rs +++ b/ext/otp-cache/src/main.rs @@ -23,7 +23,7 @@ struct Slot { /// Access Nitrokey OTP slots by name #[derive(Debug, structopt::StructOpt)] -#[structopt(bin_name = "nitrocli cache")] +#[structopt(bin_name = "nitrocli otp-cache")] struct Args { #[structopt(subcommand)] cmd: Command, @@ -77,13 +77,13 @@ fn cmd_get( let totp_slots: Vec<_> = cache.totp.iter().filter(|s| s.name == slot_name).collect(); let hotp_slots: Vec<_> = cache.hotp.iter().filter(|s| s.name == slot_name).collect(); if totp_slots.len() + hotp_slots.len() > 1 { - Err(anyhow::anyhow!("Multiple OTP slots with the given name")) + Err(anyhow::anyhow!("Found multiple OTP slots with the given name")) } else if let Some(slot) = totp_slots.first() { generate_otp(&ctx, "totp", slot.id) } else if let Some(slot) = hotp_slots.first() { generate_otp(&ctx, "hotp", slot.id) } else { - Err(anyhow::anyhow!("No OTP slot with the given name")) + Err(anyhow::anyhow!("Found no OTP slot with the given name")) } } From c33a983b9885ca387307df225adbca23aa38e03d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 13 Apr 2021 21:48:38 +0200 Subject: [PATCH 6/9] fixup! Add extension support crate nitrocli-ext --- ext/ext/src/lib.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ext/ext/src/lib.rs b/ext/ext/src/lib.rs index 0b1a8d17..0c0a859e 100644 --- a/ext/ext/src/lib.rs +++ b/ext/ext/src/lib.rs @@ -5,20 +5,21 @@ use std::env; use std::ffi; +use std::path; use std::process; use anyhow::Context as _; #[derive(Debug)] pub struct Context { - pub nitrocli: ffi::OsString, - pub resolved_usb_path: Option, - pub verbosity: Option, - pub project_dirs: directories::ProjectDirs, + nitrocli: ffi::OsString, + resolved_usb_path: Option, + verbosity: Option, + project_dirs: directories::ProjectDirs, } impl Context { - pub fn from_env(name: &str) -> anyhow::Result { + pub fn from_env() -> anyhow::Result { let nitrocli = env::var_os("NITROCLI_BINARY") .context("NITROCLI_BINARY environment variable not present") .context("Failed to retrieve nitrocli path")?; @@ -39,6 +40,13 @@ impl Context { Some(verbosity) }; + let exe = + env::current_exe().context("Failed to determine the path of the extension executable")?; + let name = exe + .file_name() + .context("Failed to extract the name of the extension executable")? + .to_str() + .context("The name of the extension executable contains non-UTF-8 characters")?; let project_dirs = directories::ProjectDirs::from("", "", name).with_context(|| { format!( "Could not determine the application directories for the {} extension", @@ -70,6 +78,10 @@ impl Context { Err(anyhow::anyhow!("Could not connect to Nitrokey device")) } } + + pub fn cache_dir(&self) -> &path::Path { + self.project_dirs.cache_dir() + } } // See src/command.rs in nitrocli core. From 194a53bbbbcef6a220f8076e2b8f77044d9fab97 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 13 Apr 2021 21:48:57 +0200 Subject: [PATCH 7/9] fixup! Add otp-cache extension --- ext/otp-cache/src/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ext/otp-cache/src/main.rs b/ext/otp-cache/src/main.rs index f8f8c6fa..01f8420a 100644 --- a/ext/otp-cache/src/main.rs +++ b/ext/otp-cache/src/main.rs @@ -46,16 +46,13 @@ fn main() -> anyhow::Result<()> { use structopt::StructOpt as _; let args = Args::from_args(); - let ctx = nitrocli_ext::Context::from_env("nitrocli-otp-cache")?; + let ctx = nitrocli_ext::Context::from_env()?; let mut mgr = nitrokey::take()?; let device = ctx.connect(&mut mgr)?; let serial_number = get_serial_number(&device)?; - let cache_file = ctx - .project_dirs - .cache_dir() - .join(&format!("{}.toml", serial_number)); + let cache_file = ctx.cache_dir().join(&format!("{}.toml", serial_number)); match &args.cmd { Command::Get { name } => { @@ -77,7 +74,9 @@ fn cmd_get( let totp_slots: Vec<_> = cache.totp.iter().filter(|s| s.name == slot_name).collect(); let hotp_slots: Vec<_> = cache.hotp.iter().filter(|s| s.name == slot_name).collect(); if totp_slots.len() + hotp_slots.len() > 1 { - Err(anyhow::anyhow!("Found multiple OTP slots with the given name")) + Err(anyhow::anyhow!( + "Found multiple OTP slots with the given name" + )) } else if let Some(slot) = totp_slots.first() { generate_otp(&ctx, "totp", slot.id) } else if let Some(slot) = hotp_slots.first() { From 20fb38c9324d183db3df83dccd00eef86fe46462 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 13 Apr 2021 21:59:38 +0200 Subject: [PATCH 8/9] fixup! Add extension support crate nitrocli-ext --- ext/ext/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ext/ext/src/lib.rs b/ext/ext/src/lib.rs index 0c0a859e..1bcb5750 100644 --- a/ext/ext/src/lib.rs +++ b/ext/ext/src/lib.rs @@ -107,13 +107,9 @@ pub struct Nitrocli { impl Nitrocli { pub fn from_context(ctx: &Context) -> Nitrocli { - let mut cmd = process::Command::new(&ctx.nitrocli); - if let Some(verbosity) = ctx.verbosity { - for _ in 0..verbosity { - cmd.arg("--verbose"); - } + Self { + cmd: process::Command::new(&ctx.nitrocli), } - Self { cmd } } pub fn arg(&mut self, arg: impl AsRef) -> &mut Nitrocli { From f13953fdcda2a4e8acfae7a73c0a2ed3d8fccb20 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 16 Apr 2021 14:30:33 +0200 Subject: [PATCH 9/9] fixup! Add otp-cache extension --- ext/otp-cache/src/main.rs | 63 ++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/ext/otp-cache/src/main.rs b/ext/otp-cache/src/main.rs index 01f8420a..956b8d58 100644 --- a/ext/otp-cache/src/main.rs +++ b/ext/otp-cache/src/main.rs @@ -22,24 +22,31 @@ struct Slot { } /// Access Nitrokey OTP slots by name +/// +/// This command caches the names of the OTP slots on a Nitrokey device and makes it possible to +/// generate a one-time password from a slot with a given name without knowing its index. It only +/// queries the names of the OTP slots if there is no cached data or if the --force-update option +/// is set. The cache includes the Nitrokey’s serial number so that it is possible to use it with +/// multiple devices. #[derive(Debug, structopt::StructOpt)] #[structopt(bin_name = "nitrocli otp-cache")] struct Args { + /// Always query the slot data even if it is already cached + #[structopt(short, long)] + force_update: bool, #[structopt(subcommand)] cmd: Command, } #[derive(Debug, structopt::StructOpt)] enum Command { - /// Generates a one-time passwords + /// Generates a one-time password Get { /// The name of the OTP slot to generate a OTP from name: String, }, /// Lists the cached slots and their names List, - /// Updates the cached slot data - Update, } fn main() -> anyhow::Result<()> { @@ -48,29 +55,15 @@ fn main() -> anyhow::Result<()> { let args = Args::from_args(); let ctx = nitrocli_ext::Context::from_env()?; - let mut mgr = nitrokey::take()?; - let device = ctx.connect(&mut mgr)?; - - let serial_number = get_serial_number(&device)?; - let cache_file = ctx.cache_dir().join(&format!("{}.toml", serial_number)); - + let cache = get_cache(&ctx, args.force_update)?; match &args.cmd { - Command::Get { name } => { - drop(device); - drop(mgr); - cmd_get(&ctx, &cache_file, name) - } - Command::List => cmd_list(&cache_file), - Command::Update => cmd_update(&cache_file, &device), + Command::Get { name } => cmd_get(&ctx, &cache, name)?, + Command::List => cmd_list(&cache), } + Ok(()) } -fn cmd_get( - ctx: &nitrocli_ext::Context, - cache_file: &path::Path, - slot_name: &str, -) -> anyhow::Result<()> { - let cache = get_cache(cache_file)?; +fn cmd_get(ctx: &nitrocli_ext::Context, cache: &Cache, slot_name: &str) -> anyhow::Result<()> { let totp_slots: Vec<_> = cache.totp.iter().filter(|s| s.name == slot_name).collect(); let hotp_slots: Vec<_> = cache.hotp.iter().filter(|s| s.name == slot_name).collect(); if totp_slots.len() + hotp_slots.len() > 1 { @@ -86,27 +79,29 @@ fn cmd_get( } } -fn cmd_list(cache_file: &path::Path) -> anyhow::Result<()> { - let cache = get_cache(&cache_file)?; +fn cmd_list(cache: &Cache) { println!("alg\tslot\tname"); - for slot in cache.totp { + for slot in &cache.totp { println!("totp\t{}\t{}", slot.id, slot.name); } - for slot in cache.hotp { + for slot in &cache.hotp { println!("hotp\t{}\t{}", slot.id, slot.name); } - Ok(()) } -fn cmd_update(cache_file: &path::Path, device: &impl nitrokey::GenerateOtp) -> anyhow::Result<()> { - save_cache(&get_otp_slots(device)?, &cache_file) -} +fn get_cache(ctx: &nitrocli_ext::Context, force_update: bool) -> anyhow::Result { + let mut mgr = nitrokey::take().context("Failed to obtain Nitrokey manager instance")?; + let device = ctx.connect(&mut mgr)?; + let serial_number = get_serial_number(&device)?; + let cache_file = ctx.cache_dir().join(&format!("{}.toml", serial_number)); -fn get_cache(file: &path::Path) -> anyhow::Result { - if !file.is_file() { - anyhow::bail!("There is no cached slot data. Run the update command to initialize the cache."); + if cache_file.is_file() && !force_update { + load_cache(&cache_file) + } else { + let cache = get_otp_slots(&device)?; + save_cache(&cache, &cache_file)?; + Ok(cache) } - load_cache(&file) } fn load_cache(path: &path::Path) -> anyhow::Result {