aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock52
-rw-r--r--Cargo.toml3
-rw-r--r--extensions/otp-cache/Cargo.toml12
-rw-r--r--extensions/otp-cache/src/ext.rs110
-rw-r--r--extensions/otp-cache/src/main.rs118
5 files changed, 295 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2611ac2..6363757 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10,6 +10,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -97,6 +103,17 @@ dependencies = [
]
[[package]]
+name = "nitrocli-otp-cache"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "serde",
+ "structopt",
+ "toml",
+ "xdg",
+]
+
+[[package]]
name = "nitrokey"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -259,6 +276,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -337,6 +374,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 <robin.krahl@ireas.org>"]
+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<String>,
+}
+
+#[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<ffi::OsStr>) -> &mut Nitrocli {
+ self.cmd.arg(arg);
+ self
+ }
+
+ pub fn args<I, S>(&mut self, args: I) -> &mut Nitrocli
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<ffi::OsStr>,
+ {
+ self.cmd.args(args);
+ self
+ }
+
+ pub fn text(&mut self) -> anyhow::Result<String> {
+ 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<OtpAlgorithm, Self::Err> {
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<ext::OtpAlgorithm, Vec<Slot>>;
+
+#[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<Cache> {
+ // 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<Cache> {
+ 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<Cache> {
+ let s = fs::read_to_string(path)?;
+ toml::from_str(&s).map_err(From::from)
+}
+
+fn generate_otp(args: &Args, slot: usize) -> anyhow::Result<String> {
+ args
+ .ext
+ .nitrocli()
+ .args(&["otp", "get"])
+ .arg(slot.to_string())
+ .arg("--algorithm")
+ .arg(args.algorithm.to_string())
+ .text()
+}