From 6917be7ab3c5a9d47866a45855c836a9cc6f86ff Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 25 Jan 2020 20:33:57 +0100 Subject: Add --serial-number option This patch adds the --serial-number option that allows the user to filter the attached Nitrokey devices by serial number. As the Nitrokey Storage does not include its serial number in the USB device descriptor and as we don't want to connect to it just to query the serial number, this option only works for Nitrokey Storage devices. --- src/args.rs | 10 ++++++++++ src/commands.rs | 25 ++++++++++++++++++++++--- src/config.rs | 25 ++++++++++++++++++++++++- src/main.rs | 1 - src/tests/status.rs | 10 ++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index 3052afa..0d77806 100644 --- a/src/args.rs +++ b/src/args.rs @@ -13,6 +13,16 @@ pub struct Args { /// Selects the device model to connect to #[structopt(short, long, global = true, possible_values = &DeviceModel::all_str())] pub model: Option, + /// Sets the serial number of the device to connect to. Can be set + /// multiple times to allow multiple serial numbers + // TODO: Add short options (avoid collisions). + #[structopt( + long = "serial-number", + global = true, + multiple = true, + number_of_values = 1 + )] + pub serial_numbers: Vec, /// Disables the cache for all secrets. #[structopt(long, global = true)] pub no_cache: bool, diff --git a/src/commands.rs b/src/commands.rs index 05038e0..f718571 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -43,10 +43,22 @@ fn set_log_level(ctx: &mut Context<'_>) { /// Create a filter string from the program configuration. fn format_filter(config: &config::Config) -> String { + let mut filters = Vec::new(); if let Some(model) = config.model { - format!(" (filter: model={})", model.as_ref()) - } else { + filters.push(format!("model={}", model.as_ref())); + } + if !config.serial_numbers.is_empty() { + let serial_numbers = config + .serial_numbers + .iter() + .map(ToString::to_string) + .collect::>(); + filters.push(format!("serial number in [{}]", serial_numbers.join(", "))); + } + if filters.is_empty() { String::new() + } else { + format!(" (filter: {})", filters.join(", ")) } } @@ -56,7 +68,14 @@ fn find_device(config: &config::Config) -> anyhow::Result let nkmodel = config.model.map(nitrokey::Model::from); let mut iter = devices .into_iter() - .filter(|device| nkmodel.is_none() || device.model == nkmodel); + .filter(|device| nkmodel.is_none() || device.model == nkmodel) + .filter(|device| { + config.serial_numbers.is_empty() + || device + .serial_number + .map(|sn| config.serial_numbers.contains(&sn)) + .unwrap_or_default() + }); let device = iter .next() diff --git a/src/config.rs b/src/config.rs index aceda38..6f0cd17 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,9 @@ use std::fs; use std::path; +use std::str::FromStr as _; + +use serde::de::Error as _; use crate::args; @@ -20,10 +23,14 @@ const CONFIG_FILE: &str = "config.toml"; /// The configuration for nitrocli, usually read from configuration /// files and environment variables. -#[derive(Clone, Copy, Debug, Default, PartialEq, merge::Merge, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, merge::Merge, serde::Deserialize)] pub struct Config { /// The model to connect to. pub model: Option, + /// The serial numbers of the device to connect to. + #[merge(strategy = merge::vec::overwrite_empty)] + #[serde(default, deserialize_with = "deserialize_serial_number_vec")] + pub serial_numbers: Vec, /// Whether to bypass the cache for all secrets or not. #[merge(strategy = merge::bool::overwrite_false)] #[serde(default)] @@ -34,6 +41,18 @@ pub struct Config { pub verbosity: u8, } +fn deserialize_serial_number_vec<'de, D>(d: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let strings: Vec = serde::Deserialize::deserialize(d).map_err(D::Error::custom)?; + let result: Result, _> = strings + .iter() + .map(|s| nitrokey::SerialNumber::from_str(s)) + .collect(); + result.map_err(D::Error::custom) +} + impl Config { pub fn load() -> anyhow::Result { use merge::Merge as _; @@ -51,6 +70,10 @@ impl Config { if args.model.is_some() { self.model = args.model; } + if !args.serial_numbers.is_empty() { + // TODO: Don't clone. + self.serial_numbers = args.serial_numbers.clone(); + } if args.no_cache { self.no_cache = true; } diff --git a/src/main.rs b/src/main.rs index 16715f9..baad15c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ // Copyright (C) 2017-2020 The Nitrocli Developers // SPDX-License-Identifier: GPL-3.0-or-later -#![allow(clippy::trivially_copy_pass_by_ref)] #![warn( bad_style, dead_code, diff --git a/src/tests/status.rs b/src/tests/status.rs index 7946929..268e36f 100644 --- a/src/tests/status.rs +++ b/src/tests/status.rs @@ -28,6 +28,16 @@ fn not_found_pro() { assert_eq!(err, "Nitrokey device not found (filter: model=pro)"); } +#[test_device] +fn not_found_by_serial_number() { + let res = Nitrocli::new().handle(&["status", "--model=storage", "--serial-number=deadbeef"]); + let err = res.unwrap_err().to_string(); + assert_eq!( + err, + "Nitrokey device not found (filter: model=storage, serial number in [0xdeadbeef])" + ); +} + #[test_device(pro)] fn output_pro(model: nitrokey::Model) -> anyhow::Result<()> { let re = regex::Regex::new( -- cgit v1.2.3