1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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()
}
|