From 446e78b95a2d2b74afb9ab7c39bf135ee5e8ab32 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 27 Aug 2020 13:13:54 +0200 Subject: Add otp-cache extension --- Cargo.lock | 52 +++++++++++++++++ Cargo.toml | 3 + extensions/otp-cache/Cargo.toml | 12 ++++ extensions/otp-cache/src/ext.rs | 110 ++++++++++++++++++++++++++++++++++++ extensions/otp-cache/src/main.rs | 118 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 extensions/otp-cache/Cargo.toml create mode 100644 extensions/otp-cache/src/ext.rs create mode 100644 extensions/otp-cache/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 2611ac2..6363757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" + [[package]] name = "base32" version = "0.4.0" @@ -96,6 +102,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nitrocli-otp-cache" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "structopt", + "toml", + "xdg", +] + [[package]] name = "nitrokey" version = "0.6.0" @@ -258,6 +275,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "serde" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6135c78461981c79497158ef777264c51d9d0f4f3fc3a4d22b915900e42dac6a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c5eaa17d0954cb481cdcfffe9d84fcfa7a1a9f2349271e678677be4c26ae31" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "structopt" version = "0.3.17" @@ -336,6 +373,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + [[package]] name = "unicode-segmentation" version = "1.6.0" @@ -387,3 +433,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" diff --git a/Cargo.toml b/Cargo.toml index 4e477cd..d0359cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,6 @@ version = "1" [dev-dependencies.tempfile] version = "3.1" + +[workspace] +members = ["extensions/*"] diff --git a/extensions/otp-cache/Cargo.toml b/extensions/otp-cache/Cargo.toml new file mode 100644 index 0000000..0d9a171 --- /dev/null +++ b/extensions/otp-cache/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nitrocli-otp-cache" +version = "0.1.0" +authors = ["Robin Krahl "] +edition = "2018" + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +structopt = { version = "0.3.17", default-features = false } +toml = "0.5" +xdg = "2" diff --git a/extensions/otp-cache/src/ext.rs b/extensions/otp-cache/src/ext.rs new file mode 100644 index 0000000..ccebd39 --- /dev/null +++ b/extensions/otp-cache/src/ext.rs @@ -0,0 +1,110 @@ +use std::ffi; +use std::fmt; +use std::process; +use std::str; + +#[derive(Debug, structopt::StructOpt)] +pub struct Args { + #[structopt(long, hidden(true))] + pub nitrocli: String, + + #[structopt(long, hidden(true))] + pub verbosity: Option, +} + +#[derive(Debug)] +pub struct Nitrocli { + cmd: process::Command, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum OtpAlgorithm { + Hotp, + Totp, +} + +impl Args { + pub fn nitrocli(&self) -> Nitrocli { + // TODO: set verbosity args + Nitrocli::new(&self.nitrocli) + } +} + +impl Nitrocli { + pub fn new(nitrocli: &str) -> Nitrocli { + Nitrocli { + cmd: process::Command::new(nitrocli), + } + } + + 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()?; + if output.status.success() { + String::from_utf8(output.stdout).map_err(From::from) + } else { + Err(anyhow::anyhow!( + "nitrocli call failed: {}", + String::from_utf8_lossy(&output.stderr) + )) + } + } +} + +impl fmt::Display for OtpAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + OtpAlgorithm::Hotp => "hotp", + OtpAlgorithm::Totp => "totp", + } + ) + } +} + +impl str::FromStr for OtpAlgorithm { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "hotp" => Ok(OtpAlgorithm::Hotp), + "totp" => Ok(OtpAlgorithm::Totp), + _ => Err(anyhow::anyhow!("Unexpected OTP algorithm: {}", s)), + } + } +} + +impl<'de> serde::Deserialize<'de> for OtpAlgorithm { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + str::FromStr::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom) + } +} + +impl serde::Serialize for OtpAlgorithm { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} diff --git a/extensions/otp-cache/src/main.rs b/extensions/otp-cache/src/main.rs new file mode 100644 index 0000000..b5f0b90 --- /dev/null +++ b/extensions/otp-cache/src/main.rs @@ -0,0 +1,118 @@ +mod ext; + +use std::collections; +use std::fs; +use std::io; +use std::path; + +type Cache = collections::BTreeMap>; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Slot { + name: String, + id: usize, +} + +/// Access Nitrokey OTP slots by name +#[derive(Debug, structopt::StructOpt)] +#[structopt(bin_name = "nitrocli otp-cache")] +struct Args { + /// Update the cached slot data + #[structopt(short, long)] + force_update: bool, + + #[structopt(short, long, global = true, default_value = "totp")] + algorithm: ext::OtpAlgorithm, + + #[structopt(subcommand)] + cmd: Command, + + #[structopt(flatten)] + ext: ext::Args, +} + +#[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 ID + List, +} + +fn main() -> anyhow::Result<()> { + use structopt::StructOpt as _; + + let args = Args::from_args(); + let mut cache = get_cache(&args)?; + let slots = cache.remove(&args.algorithm).unwrap_or_default(); + + match &args.cmd { + Command::Get { name } => match slots.iter().find(|s| &s.name == name) { + Some(slot) => println!("{}", generate_otp(&args, slot.id)?), + None => anyhow::bail!("No OTP slot with the given name!"), + }, + Command::List => { + println!("id\tslot"); + for slot in slots { + println!("{}\t{}", slot.id, slot.name); + } + } + } + + Ok(()) +} + +fn get_cache(args: &Args) -> anyhow::Result { + // TODO: use model and serial number as cache file name + let cache_file = + xdg::BaseDirectories::with_prefix("nitrocli")?.place_cache_file("otp-cache/cache.toml")?; + if args.force_update || !cache_file.is_file() { + let cache = update_cache(args)?; + save_cache(&cache, &cache_file)?; + Ok(cache) + } else { + load_cache(&cache_file) + } +} + +fn update_cache(args: &Args) -> anyhow::Result { + let slots = args.ext.nitrocli().args(&["otp", "status"]).text()?; + let mut cache = Cache::new(); + for line in slots.lines().skip(1) { + let parts: Vec<_> = line.splitn(3, "\t").collect(); + if parts.len() == 3 { + let algorithm: ext::OtpAlgorithm = parts[0].parse()?; + let id: usize = parts[1].parse()?; + let name = parts[2].to_owned(); + cache.entry(algorithm).or_default().push(Slot { name, id }); + } + } + Ok(cache) +} + +fn save_cache(cache: &Cache, path: &path::Path) -> anyhow::Result<()> { + use io::Write as _; + + let mut f = fs::File::create(path)?; + f.write_all(&toml::to_vec(cache)?)?; + Ok(()) +} + +fn load_cache(path: &path::Path) -> anyhow::Result { + let s = fs::read_to_string(path)?; + toml::from_str(&s).map_err(From::from) +} + +fn generate_otp(args: &Args, slot: usize) -> anyhow::Result { + args + .ext + .nitrocli() + .args(&["otp", "get"]) + .arg(slot.to_string()) + .arg("--algorithm") + .arg(args.algorithm.to_string()) + .text() +} -- cgit v1.2.1