Skip to content

Commit

Permalink
Add pws-cache extension
Browse files Browse the repository at this point in the history
This patch adds the pws-cache core extension that allows accessing the
PWS slots by their name instead of the slot index.

Fixes d-e-s-o#155.
  • Loading branch information
robinkrahl committed Apr 17, 2021
1 parent 01b8b4f commit 6baaa79
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Unreleased
----------
- Introduced extension support crate, `nitrocli-ext`
- Introduced `otp-cache` core extension
- Introduced `otp-cache` and `pws-cache` core extensions
- Enabled usage of empty PWS slot fields
- Changed error reporting format to make up only a single line
- Added `NITROCLI_RESOLVED_USB_PATH` environment variable to be used by
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions ext/pws-cache/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Cargo.toml

# Copyright (C) 2020-2021 The Nitrocli Developers
# SPDX-License-Identifier: GPL-3.0-or-later

[package]
name = "nitrocli-pws-cache"
version = "0.1.0"
authors = ["Robin Krahl <[email protected]>"]
edition = "2018"

[dependencies]
anyhow = "1"
nitrokey = "0.9"
serde = { version = "1", features = ["derive"] }
structopt = { version = "0.3.21", default-features = false }
toml = "0.5"

[dependencies.nitrocli-ext]
version = "0.1"
path = "../ext"
197 changes: 197 additions & 0 deletions ext/pws-cache/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// main.rs

// Copyright (C) 2020-2021 The Nitrocli Developers
// SPDX-License-Identifier: GPL-3.0-or-later

use std::fs;
use std::io::Write as _;
use std::path;

use anyhow::Context as _;

use structopt::StructOpt as _;

// TODO: query from user
const USER_PIN: &str = "123456";

#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
struct Cache {
slots: Vec<Slot>,
}

impl Cache {
pub fn find_slot(&self, name: &str) -> anyhow::Result<u8> {
let slots = self
.slots
.iter()
.filter(|s| s.name == name)
.collect::<Vec<_>>();
if slots.len() > 1 {
Err(anyhow::anyhow!(
"Found multiple PWS slots with the given name"
))
} else if let Some(slot) = slots.first() {
Ok(slot.id)
} else {
Err(anyhow::anyhow!("Found no PWS slot with the given name"))
}
}
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Slot {
name: String,
id: u8,
}

/// Access Nitrokey PWS slots by name
///
/// This command caches the names of the PWS slots on a Nitrokey device
/// and makes it possible to fetch a login or a password from a slot
/// with a given name without knowing its index. It only queries the
/// names of the PWS slots if there is no cached data or if the
/// `--force-update` option is set. The cache includes the Nitrokey's
/// serial number so that it is possible to use it with multiple
/// devices.
#[derive(Debug, structopt::StructOpt)]
#[structopt(bin_name = "nitrocli pws-cache")]
struct Args {
/// Always query the slot data even if it is already cached
#[structopt(short, long)]
force_update: bool,
#[structopt(subcommand)]
cmd: Command,
}

#[derive(Debug, structopt::StructOpt)]
enum Command {
/// Fetches the login and the password from a PWS slot
Get(GetArgs),
/// Fetches the login from a PWS slot
GetLogin(GetArgs),
/// Fetches the password from a PWS slot
GetPassword(GetArgs),
/// Lists the cached slots and their names
List,
}

#[derive(Debug, structopt::StructOpt)]
struct GetArgs {
/// The name of the PWS slot to fetch
name: String,
}

fn main() -> anyhow::Result<()> {
let args = Args::from_args();
let ctx = nitrocli_ext::Context::from_env()?;

let cache = get_cache(&ctx, args.force_update)?;
match &args.cmd {
Command::Get(args) => cmd_get(&ctx, &cache, &args.name)?,
Command::GetLogin(args) => cmd_get_login(&ctx, &cache, &args.name)?,
Command::GetPassword(args) => cmd_get_password(&ctx, &cache, &args.name)?,
Command::List => cmd_list(&cache),
}
Ok(())
}

fn cmd_get(ctx: &nitrocli_ext::Context, cache: &Cache, slot_name: &str) -> anyhow::Result<()> {
let slot = cache.find_slot(slot_name)?;
prepare_pws_get(ctx, slot)
.arg("--login")
.arg("--password")
.spawn()
}

fn cmd_get_login(
ctx: &nitrocli_ext::Context,
cache: &Cache,
slot_name: &str,
) -> anyhow::Result<()> {
let slot = cache.find_slot(slot_name)?;
prepare_pws_get(ctx, slot)
.arg("--login")
.arg("--quiet")
.spawn()
}

fn cmd_get_password(
ctx: &nitrocli_ext::Context,
cache: &Cache,
slot_name: &str,
) -> anyhow::Result<()> {
let slot = cache.find_slot(slot_name)?;
prepare_pws_get(ctx, slot)
.arg("--password")
.arg("--quiet")
.spawn()
}

fn cmd_list(cache: &Cache) {
println!("slot\tname");
for slot in &cache.slots {
println!("{}\t{}", slot.id, slot.name);
}
}

fn get_cache(ctx: &nitrocli_ext::Context, force_update: bool) -> anyhow::Result<Cache> {
let mut mgr = nitrokey::take().context("Failed to obtain Nitrokey manager instance")?;
let mut device = ctx.connect(&mut mgr)?;
let serial_number = get_serial_number(&device)?;
let cache_file = ctx.cache_dir().join(&format!("{}.toml", serial_number));

if cache_file.is_file() && !force_update {
load_cache(&cache_file)
} else {
let cache = get_pws_slots(&mut device)?;
save_cache(&cache, &cache_file)?;
Ok(cache)
}
}

fn load_cache(path: &path::Path) -> anyhow::Result<Cache> {
let s = fs::read_to_string(path).context("Failed to read cache file")?;
toml::from_str(&s).context("Failed to parse cache file")
}

fn save_cache(cache: &Cache, path: &path::Path) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context("Failed to create cache parent directory")?;
}
let mut f = fs::File::create(path).context("Failed to create cache file")?;
let data = toml::to_vec(cache).context("Failed to serialize cache")?;
f.write_all(&data).context("Failed to write cache file")?;
Ok(())
}

fn get_serial_number<'a>(device: &impl nitrokey::Device<'a>) -> anyhow::Result<String> {
// TODO: Consider using hidapi serial number (if available)
Ok(device.get_serial_number()?.to_string().to_lowercase())
}

fn get_pws_slots<'a>(device: &mut impl nitrokey::GetPasswordSafe<'a>) -> anyhow::Result<Cache> {
let pws = device
.get_password_safe(USER_PIN)
.context("Failed to open password safe")?;
let slots = pws
.get_slots()
.context("Failed to query password safe slots")?;
let mut cache = Cache::default();
for slot in slots {
if let Some(slot) = slot {
let id = slot.index();
let name = slot
.get_name()
.with_context(|| format!("Failed to query name for password slot {}", id))?;
cache.slots.push(Slot { name, id });
}
}
Ok(cache)
}

fn prepare_pws_get(ctx: &nitrocli_ext::Context, slot: u8) -> nitrocli_ext::Nitrocli {
let mut ncli = ctx.nitrocli();
let _ = ncli.args(&["pws", "get"]);
let _ = ncli.arg(slot.to_string());
ncli
}

0 comments on commit 6baaa79

Please sign in to comment.