Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pws-cache extension #156

Draft
wants to merge 15 commits into
base: devel
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
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.
  • Loading branch information
robinkrahl committed Apr 17, 2021
commit 6baaa79e502b64b11f2d60e7e42676007f4f812f
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
}