From 4a8c01adb5100fd0397aad239edc5e80d13aca13 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 8 Sep 2020 18:23:30 +0200 Subject: Add --usb-path option to select device This patch adds the --usb-path option as an additional way to filter the Nitrokey device to connect to. While the serial number is a better identifier in theory, the Nitrokey Storage devices do not send their serial number in the USB device descriptor. Having the --usb-path options allows users to select one of multiple Nitrokey Storage devices. While we could directly call the nitrokey::Manager::connect_path function with the specified path, we integrate the --usb-path option into the existing find_device function for consistent error messages and to avoid having to duplicate the --model and --serial-number checks. --- CHANGELOG.md | 2 ++ doc/config.example.toml | 2 ++ doc/nitrocli.1 | 18 ++++++++-- doc/nitrocli.1.pdf | Bin 41970 -> 42218 bytes src/args.rs | 3 ++ src/commands.rs | 10 ++++-- src/config.rs | 5 +++ src/tests/run.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++-- 8 files changed, 125 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 607ee3d..83fc8d9 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 index 73041ae..48a41ff 100644 Binary files a/doc/nitrocli.1.pdf and b/doc/nitrocli.1.pdf differ 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, + /// Sets the USB path of the device to connect to + #[structopt(long, global = true)] + pub usb_path: Option, /// 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::>(); 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 .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 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 6f0cd17..8e7eefb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,8 @@ pub struct Config { #[merge(strategy = merge::vec::overwrite_empty)] #[serde(default, deserialize_with = "deserialize_serial_number_vec")] pub serial_numbers: Vec, + /// The USB path of the device to connect to. + pub usb_path: Option, /// Whether to bypass the cache for all secrets or not. #[merge(strategy = merge::bool::overwrite_false)] #[serde(default)] @@ -74,6 +76,9 @@ impl Config { // TODO: Don't clone. 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(()) @@ -142,6 +143,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()?; @@ -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::>())?; + 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::>()); + 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(()) +} -- cgit v1.2.3