diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | doc/config.example.toml | 2 | ||||
-rw-r--r-- | doc/nitrocli.1 | 18 | ||||
-rw-r--r-- | doc/nitrocli.1.pdf | bin | 41970 -> 42218 bytes | |||
-rw-r--r-- | src/args.rs | 3 | ||||
-rw-r--r-- | src/commands.rs | 10 | ||||
-rw-r--r-- | src/config.rs | 5 | ||||
-rw-r--r-- | src/tests/run.rs | 92 |
8 files changed, 125 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index fe15243..5fabc07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Unreleased options are set) - Added `--serial-number` option that restricts the serial number of the device to connect to + - Added `--usb-path` option that restricts the USB path of the device to + connect to - Bumped `structopt` dependency to `0.3.17` diff --git a/doc/config.example.toml b/doc/config.example.toml index eefdfa0..82e0ece 100644 --- a/doc/config.example.toml +++ b/doc/config.example.toml @@ -7,6 +7,8 @@ model = "pro" # The serial number of the device to connect to (list of strings, default: # empty). serial_numbers = ["0xf00baa", "deadbeef"] +# The USB path of the device to connect to (string, default: empty). +usb_path = "004:001:00" # 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 8b04de6..2d1e564 100644 --- a/doc/nitrocli.1 +++ b/doc/nitrocli.1 @@ -12,11 +12,13 @@ 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 and \fB\-\-serial-number\fR options to select -the device to connect to. +You can use the \fB\-\-model\fR, \fB\-\-serial-number\fR and \fB\-\-usb-path\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. +Use the \fBlist\fR command to list all attached devices with their USB path, +model, and serial number (if available). .SH OPTIONS .TP \fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR @@ -31,6 +33,9 @@ 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\-\-usb-path \fIusb-path\fR +Restrict connections to the given USB path, see the Device selection section. +.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. @@ -315,6 +320,10 @@ Restrict connections to the given device model (string, default: not set, see Restrict connections to the given serial numbers (list of strings, default: empty, see \fB\-\-serial-number\fR). .TP +.B usb_path +Restrict connections to the given USB path (string, default: not set, see +\fB\-\-usb-path\fR). +.TP .B no_cache If set to true, do not cache any inquired secrets (boolean, default: false, see \fB\-\-no\-cache\fR). @@ -325,6 +334,7 @@ Set the log level (integer, default: 0, see \fB\-\-verbose\fR). The configuration file must use the TOML format, for example: model = "pro" serial_numbers = ["0xf00baa", "deadbeef"] + usb_path = "0001:0006:02" no_cache = false verbosity = 0 @@ -361,6 +371,10 @@ Restrict connections to the given device model (string, default: not set, see Restrict connections to the given list of serial numbers (comma-separated list of strings, default: empty, see \fB\-\-serial-number\fR). .TP +.B NITROCLI_USB_PATH +Restrict connections to the given USB path (string, default: not set, see +\fB\-\-usb-path\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 73041ae..48a41ff 100644 --- a/doc/nitrocli.1.pdf +++ b/doc/nitrocli.1.pdf diff --git a/src/args.rs b/src/args.rs index 0d77806..80abe17 100644 --- a/src/args.rs +++ b/src/args.rs @@ -23,6 +23,9 @@ pub struct Args { number_of_values = 1 )] pub serial_numbers: Vec<nitrokey::SerialNumber>, + /// Sets the USB path of the device to connect to + #[structopt(long, global = true)] + pub usb_path: Option<String>, /// 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 455ff4d..d352ca2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -55,6 +55,9 @@ fn format_filter(config: &config::Config) -> String { .collect::<Vec<_>>(); filters.push(format!("serial number in [{}]", serial_numbers.join(", "))); } + if let Some(path) = &config.usb_path { + filters.push(format!("usb path={}", path)); + } if filters.is_empty() { String::new() } else { @@ -75,7 +78,8 @@ fn find_device(config: &config::Config) -> anyhow::Result<nitrokey::DeviceInfo> .serial_number .map(|sn| config.serial_numbers.contains(&sn)) .unwrap_or_default() - }); + }) + .filter(|device| config.usb_path.is_none() || config.usb_path.as_ref() == Some(&device.path)); let device = iter .next() @@ -83,8 +87,8 @@ fn find_device(config: &config::Config) -> anyhow::Result<nitrokey::DeviceInfo> anyhow::ensure!( iter.next().is_none(), - "Multiple Nitrokey devices found{}. Use the --model and --serial-number options to \ - select one", + "Multiple Nitrokey devices found{}. Use the --model, --serial-number, and --usb-path options \ + to select one", format_filter(config) ); Ok(device) diff --git a/src/config.rs b/src/config.rs index a609189..6bb6efb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,6 +32,8 @@ pub struct Config { #[merge(strategy = merge::vec::overwrite_empty)] #[serde(default, deserialize_with = "deserialize_serial_number_vec")] pub serial_numbers: Vec<nitrokey::SerialNumber>, + /// The USB path of the device to connect to. + pub usb_path: Option<String>, /// Whether to bypass the cache for all secrets or not. #[merge(strategy = merge::bool::overwrite_false)] #[serde(default)] @@ -74,6 +76,9 @@ impl Config { if !args.serial_numbers.is_empty() { self.serial_numbers = args.serial_numbers.clone(); } + if args.usb_path.is_some() { + self.usb_path = args.usb_path.clone(); + } if args.no_cache { self.no_cache = true; } diff --git a/src/tests/run.rs b/src/tests/run.rs index 4f53608..b39b1da 100644 --- a/src/tests/run.rs +++ b/src/tests/run.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later use std::collections; +use std::ops; use std::path; use super::*; @@ -116,7 +117,7 @@ fn connect_multiple(_model: nitrokey::Model) -> anyhow::Result<()> { let err = res.unwrap_err().to_string(); assert_eq!( err, - "Multiple Nitrokey devices found. Use the --model and --serial-number options to select one" + "Multiple Nitrokey devices found. Use the --model, --serial-number, and --usb-path options to select one" ); } Ok(()) @@ -143,6 +144,32 @@ fn connect_wrong_serial_number(_model: nitrokey::Model) { } #[test_device] +fn connect_usb_path(_model: nitrokey::Model) -> anyhow::Result<()> { + for device in nitrokey::list_devices()? { + let res = Nitrocli::new().handle(&["status", &format!("--usb-path={}", device.path)]); + assert!(res.is_ok()); + let res = res?; + if let Some(model) = device.model { + assert!(res.contains(&format!("model: {}\n", model))); + } + if let Some(sn) = device.serial_number { + assert!(res.contains(&format!("serial number: {}\n", sn))); + } + } + Ok(()) +} + +#[test_device] +fn connect_wrong_usb_path(_model: nitrokey::Model) { + let res = Nitrocli::new().handle(&["status", "--usb-path=not-a-path"]); + let err = res.unwrap_err().to_string(); + assert_eq!( + err, + "Nitrokey device not found (filter: usb path=not-a-path)" + ); +} + +#[test_device] fn connect_model(_model: nitrokey::Model) -> anyhow::Result<()> { let devices = nitrokey::list_devices()?; let mut model_counts = collections::BTreeMap::new(); @@ -172,10 +199,71 @@ fn connect_model(_model: nitrokey::Model) -> anyhow::Result<()> { format!( "Multiple Nitrokey devices found (filter: model={}). ", model.to_lowercase() - ) + "Use the --model and --serial-number options to select one" + ) + "Use the --model, --serial-number, and --usb-path options to select one" ); } } Ok(()) } + +#[test_device] +fn connect_usb_path_model_serial(_model: nitrokey::Model) -> anyhow::Result<()> { + let devices = nitrokey::list_devices()?; + for device in devices { + let mut args = Vec::new(); + args.push("status".to_owned()); + args.push(format!("--usb-path={}", device.path)); + if let Some(model) = device.model { + args.push(format!("--model={}", model.to_string().to_lowercase())); + } + if let Some(sn) = device.serial_number { + args.push(format!("--serial-number={}", sn)); + } + + let res = Nitrocli::new().handle(&args.iter().map(ops::Deref::deref).collect::<Vec<_>>())?; + if let Some(model) = device.model { + assert!(res.contains(&format!("model: {}\n", model))); + } + if let Some(sn) = device.serial_number { + assert!(res.contains(&format!("serial number: {}\n", sn))); + } + } + Ok(()) +} + +#[test_device] +fn connect_usb_path_model_wrong_serial(_model: nitrokey::Model) -> anyhow::Result<()> { + let devices = nitrokey::list_devices()?; + for device in devices { + let mut args = Vec::new(); + args.push("status".to_owned()); + args.push(format!("--usb-path={}", device.path)); + if let Some(model) = device.model { + args.push(format!("--model={}", model.to_string().to_lowercase())); + } + args.push("--serial-number=0xdeadbeef".to_owned()); + + let res = Nitrocli::new().handle(&args.iter().map(ops::Deref::deref).collect::<Vec<_>>()); + let err = res.unwrap_err().to_string(); + if let Some(model) = device.model { + assert_eq!( + err, + format!( + "Nitrokey device not found (filter: model={}, serial number in [0xdeadbeef], usb path={})", + model.to_string().to_lowercase(), + device.path + ) + ); + } else { + assert_eq!( + err, + format!( + "Nitrokey device not found (filter: serial number in [0xdeadbeef], usb path={})", + device.path + ) + ); + } + } + Ok(()) +} |