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