Skip to content

Commit 6baaa79

Browse files
committed
Add pws-cache extension
This patch adds the pws-cache core extension that allows accessing the PWS slots by their name instead of the slot index. Fixes #155.
1 parent 01b8b4f commit 6baaa79

File tree

4 files changed

+231
-1
lines changed

4 files changed

+231
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Unreleased
22
----------
33
- Introduced extension support crate, `nitrocli-ext`
4-
- Introduced `otp-cache` core extension
4+
- Introduced `otp-cache` and `pws-cache` core extensions
55
- Enabled usage of empty PWS slot fields
66
- Changed error reporting format to make up only a single line
77
- Added `NITROCLI_RESOLVED_USB_PATH` environment variable to be used by

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/pws-cache/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Cargo.toml
2+
3+
# Copyright (C) 2020-2021 The Nitrocli Developers
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
[package]
7+
name = "nitrocli-pws-cache"
8+
version = "0.1.0"
9+
authors = ["Robin Krahl <[email protected]>"]
10+
edition = "2018"
11+
12+
[dependencies]
13+
anyhow = "1"
14+
nitrokey = "0.9"
15+
serde = { version = "1", features = ["derive"] }
16+
structopt = { version = "0.3.21", default-features = false }
17+
toml = "0.5"
18+
19+
[dependencies.nitrocli-ext]
20+
version = "0.1"
21+
path = "../ext"

ext/pws-cache/src/main.rs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// main.rs
2+
3+
// Copyright (C) 2020-2021 The Nitrocli Developers
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
use std::fs;
7+
use std::io::Write as _;
8+
use std::path;
9+
10+
use anyhow::Context as _;
11+
12+
use structopt::StructOpt as _;
13+
14+
// TODO: query from user
15+
const USER_PIN: &str = "123456";
16+
17+
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
18+
struct Cache {
19+
slots: Vec<Slot>,
20+
}
21+
22+
impl Cache {
23+
pub fn find_slot(&self, name: &str) -> anyhow::Result<u8> {
24+
let slots = self
25+
.slots
26+
.iter()
27+
.filter(|s| s.name == name)
28+
.collect::<Vec<_>>();
29+
if slots.len() > 1 {
30+
Err(anyhow::anyhow!(
31+
"Found multiple PWS slots with the given name"
32+
))
33+
} else if let Some(slot) = slots.first() {
34+
Ok(slot.id)
35+
} else {
36+
Err(anyhow::anyhow!("Found no PWS slot with the given name"))
37+
}
38+
}
39+
}
40+
41+
#[derive(Debug, serde::Deserialize, serde::Serialize)]
42+
struct Slot {
43+
name: String,
44+
id: u8,
45+
}
46+
47+
/// Access Nitrokey PWS slots by name
48+
///
49+
/// This command caches the names of the PWS slots on a Nitrokey device
50+
/// and makes it possible to fetch a login or a password from a slot
51+
/// with a given name without knowing its index. It only queries the
52+
/// names of the PWS slots if there is no cached data or if the
53+
/// `--force-update` option is set. The cache includes the Nitrokey's
54+
/// serial number so that it is possible to use it with multiple
55+
/// devices.
56+
#[derive(Debug, structopt::StructOpt)]
57+
#[structopt(bin_name = "nitrocli pws-cache")]
58+
struct Args {
59+
/// Always query the slot data even if it is already cached
60+
#[structopt(short, long)]
61+
force_update: bool,
62+
#[structopt(subcommand)]
63+
cmd: Command,
64+
}
65+
66+
#[derive(Debug, structopt::StructOpt)]
67+
enum Command {
68+
/// Fetches the login and the password from a PWS slot
69+
Get(GetArgs),
70+
/// Fetches the login from a PWS slot
71+
GetLogin(GetArgs),
72+
/// Fetches the password from a PWS slot
73+
GetPassword(GetArgs),
74+
/// Lists the cached slots and their names
75+
List,
76+
}
77+
78+
#[derive(Debug, structopt::StructOpt)]
79+
struct GetArgs {
80+
/// The name of the PWS slot to fetch
81+
name: String,
82+
}
83+
84+
fn main() -> anyhow::Result<()> {
85+
let args = Args::from_args();
86+
let ctx = nitrocli_ext::Context::from_env()?;
87+
88+
let cache = get_cache(&ctx, args.force_update)?;
89+
match &args.cmd {
90+
Command::Get(args) => cmd_get(&ctx, &cache, &args.name)?,
91+
Command::GetLogin(args) => cmd_get_login(&ctx, &cache, &args.name)?,
92+
Command::GetPassword(args) => cmd_get_password(&ctx, &cache, &args.name)?,
93+
Command::List => cmd_list(&cache),
94+
}
95+
Ok(())
96+
}
97+
98+
fn cmd_get(ctx: &nitrocli_ext::Context, cache: &Cache, slot_name: &str) -> anyhow::Result<()> {
99+
let slot = cache.find_slot(slot_name)?;
100+
prepare_pws_get(ctx, slot)
101+
.arg("--login")
102+
.arg("--password")
103+
.spawn()
104+
}
105+
106+
fn cmd_get_login(
107+
ctx: &nitrocli_ext::Context,
108+
cache: &Cache,
109+
slot_name: &str,
110+
) -> anyhow::Result<()> {
111+
let slot = cache.find_slot(slot_name)?;
112+
prepare_pws_get(ctx, slot)
113+
.arg("--login")
114+
.arg("--quiet")
115+
.spawn()
116+
}
117+
118+
fn cmd_get_password(
119+
ctx: &nitrocli_ext::Context,
120+
cache: &Cache,
121+
slot_name: &str,
122+
) -> anyhow::Result<()> {
123+
let slot = cache.find_slot(slot_name)?;
124+
prepare_pws_get(ctx, slot)
125+
.arg("--password")
126+
.arg("--quiet")
127+
.spawn()
128+
}
129+
130+
fn cmd_list(cache: &Cache) {
131+
println!("slot\tname");
132+
for slot in &cache.slots {
133+
println!("{}\t{}", slot.id, slot.name);
134+
}
135+
}
136+
137+
fn get_cache(ctx: &nitrocli_ext::Context, force_update: bool) -> anyhow::Result<Cache> {
138+
let mut mgr = nitrokey::take().context("Failed to obtain Nitrokey manager instance")?;
139+
let mut device = ctx.connect(&mut mgr)?;
140+
let serial_number = get_serial_number(&device)?;
141+
let cache_file = ctx.cache_dir().join(&format!("{}.toml", serial_number));
142+
143+
if cache_file.is_file() && !force_update {
144+
load_cache(&cache_file)
145+
} else {
146+
let cache = get_pws_slots(&mut device)?;
147+
save_cache(&cache, &cache_file)?;
148+
Ok(cache)
149+
}
150+
}
151+
152+
fn load_cache(path: &path::Path) -> anyhow::Result<Cache> {
153+
let s = fs::read_to_string(path).context("Failed to read cache file")?;
154+
toml::from_str(&s).context("Failed to parse cache file")
155+
}
156+
157+
fn save_cache(cache: &Cache, path: &path::Path) -> anyhow::Result<()> {
158+
if let Some(parent) = path.parent() {
159+
fs::create_dir_all(parent).context("Failed to create cache parent directory")?;
160+
}
161+
let mut f = fs::File::create(path).context("Failed to create cache file")?;
162+
let data = toml::to_vec(cache).context("Failed to serialize cache")?;
163+
f.write_all(&data).context("Failed to write cache file")?;
164+
Ok(())
165+
}
166+
167+
fn get_serial_number<'a>(device: &impl nitrokey::Device<'a>) -> anyhow::Result<String> {
168+
// TODO: Consider using hidapi serial number (if available)
169+
Ok(device.get_serial_number()?.to_string().to_lowercase())
170+
}
171+
172+
fn get_pws_slots<'a>(device: &mut impl nitrokey::GetPasswordSafe<'a>) -> anyhow::Result<Cache> {
173+
let pws = device
174+
.get_password_safe(USER_PIN)
175+
.context("Failed to open password safe")?;
176+
let slots = pws
177+
.get_slots()
178+
.context("Failed to query password safe slots")?;
179+
let mut cache = Cache::default();
180+
for slot in slots {
181+
if let Some(slot) = slot {
182+
let id = slot.index();
183+
let name = slot
184+
.get_name()
185+
.with_context(|| format!("Failed to query name for password slot {}", id))?;
186+
cache.slots.push(Slot { name, id });
187+
}
188+
}
189+
Ok(cache)
190+
}
191+
192+
fn prepare_pws_get(ctx: &nitrocli_ext::Context, slot: u8) -> nitrocli_ext::Nitrocli {
193+
let mut ncli = ctx.nitrocli();
194+
let _ = ncli.args(&["pws", "get"]);
195+
let _ = ncli.arg(slot.to_string());
196+
ncli
197+
}

0 commit comments

Comments
 (0)