diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | doc/config.example.toml | 3 | ||||
-rw-r--r-- | doc/nitrocli.1 | 26 | ||||
-rw-r--r-- | doc/nitrocli.1.pdf | bin | 40972 -> 41970 bytes | |||
-rw-r--r-- | src/args.rs | 10 | ||||
-rw-r--r-- | src/commands.rs | 25 | ||||
-rw-r--r-- | src/config.rs | 25 | ||||
-rw-r--r-- | src/main.rs | 1 | ||||
-rw-r--r-- | src/tests/status.rs | 10 |
9 files changed, 93 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index fc288ee..29a9faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Unreleased - Reworked connection handling for multiple attached Nitrokey devices: - Fail if multiple attached devices match the filter options (or no filter options are set) + - Added `--serial-number` option that restricts the serial number of the + device to connect to 0.3.4 diff --git a/doc/config.example.toml b/doc/config.example.toml index a427749..eefdfa0 100644 --- a/doc/config.example.toml +++ b/doc/config.example.toml @@ -4,6 +4,9 @@ # The model to connect to (string, "pro" or "storage", default: not set). model = "pro" +# The serial number of the device to connect to (list of strings, default: +# empty). +serial_numbers = ["0xf00baa", "deadbeef"] # Do not cache secrets (boolean, default: false). no_cache = true # The log level (integer, default: 0). diff --git a/doc/nitrocli.1 b/doc/nitrocli.1 index 680af3b..8b04de6 100644 --- a/doc/nitrocli.1 +++ b/doc/nitrocli.1 @@ -12,16 +12,25 @@ It can be used to access the encrypted volume, the one-time password generator, and the password safe. .SS Device selection Per default, \fBnitrocli\fR connects to any attached Nitrokey device. -You can use the \fB\-\-model\fR option to select the device to connect to. -\fBnitrocli\fR fails if more than one attached Nitrokey device matches -this filter or if multiple Nitrokey devices are attached and this option -is not set. +You can use the \fB\-\-model\fR and \fB\-\-serial-number\fR options to select +the device to connect to. +\fBnitrocli\fR fails if more than one attached Nitrokey device matches this +filter or if multiple Nitrokey devices are attached and none of the filter +options is set. .SH OPTIONS .TP \fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR Restrict connections to the given device model, see the Device selection section. .TP +\fB\-\-serial-number \fIserial-number\fR +Restrict connections to the given serial number, see the Device selection +section. +\fIserial-number\fR must be a hex string with an optional 0x prefix. +This option can be set multiple times to allow any of the given serial numbers. +Nitrokey Storage devices never match this restriction as they do not expose +their serial number in the USB device descriptor. +.TP \fB\-\-no\-cache\fR If this option is set, nitrocli will not cache any inquired secrets using \fBgpg\-agent\fR(1) but ask for them each time they are needed. @@ -302,6 +311,10 @@ The following values can be set in the configuration file: Restrict connections to the given device model (string, default: not set, see \fB\-\-model\fR). .TP +.B serial_numbers +Restrict connections to the given serial numbers (list of strings, default: +empty, see \fB\-\-serial-number\fR). +.TP .B no_cache If set to true, do not cache any inquired secrets (boolean, default: false, see \fB\-\-no\-cache\fR). @@ -311,6 +324,7 @@ Set the log level (integer, default: 0, see \fB\-\-verbose\fR). .P The configuration file must use the TOML format, for example: model = "pro" + serial_numbers = ["0xf00baa", "deadbeef"] no_cache = false verbosity = 0 @@ -343,6 +357,10 @@ configuration (see the Config file section): Restrict connections to the given device model (string, default: not set, see \fB\-\-model\fR). .TP +.B NITROCLI_SERIAL_NUMBERS +Restrict connections to the given list of serial numbers (comma-separated list +of strings, default: empty, see \fB\-\-serial-number\fR). +.TP .B NITROCLI_NO_CACHE If set to true, do not cache any inquired secrets (boolean, default: false, see \fB\-\-no\-cache\fR). diff --git a/doc/nitrocli.1.pdf b/doc/nitrocli.1.pdf Binary files differindex 015f379..73041ae 100644 --- a/doc/nitrocli.1.pdf +++ b/doc/nitrocli.1.pdf 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( |