aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs10
-rw-r--r--src/commands.rs25
-rw-r--r--src/config.rs25
-rw-r--r--src/main.rs1
-rw-r--r--src/tests/status.rs10
5 files changed, 66 insertions, 5 deletions
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<DeviceModel>,
+ /// 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<nitrokey::SerialNumber>,
/// 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::<Vec<_>>();
+ 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<nitrokey::DeviceInfo>
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<args::DeviceModel>,
+ /// 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<nitrokey::SerialNumber>,
/// 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<Vec<nitrokey::SerialNumber>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ let strings: Vec<String> = serde::Deserialize::deserialize(d).map_err(D::Error::custom)?;
+ let result: Result<Vec<_>, _> = strings
+ .iter()
+ .map(|s| nitrokey::SerialNumber::from_str(s))
+ .collect();
+ result.map_err(D::Error::custom)
+}
+
impl Config {
pub fn load() -> anyhow::Result<Self> {
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(