aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2020-09-08 18:23:30 +0200
committerDaniel Mueller <deso@posteo.net>2020-09-09 08:55:50 -0700
commit4a8c01adb5100fd0397aad239edc5e80d13aca13 (patch)
tree8bd215f32d2fee5b185efbcd36888c6fe44ffaea
parent16f6b3ba0c3535efd1b9288ea1980cdd281b6565 (diff)
downloadnitrocli-4a8c01adb5100fd0397aad239edc5e80d13aca13.tar.gz
nitrocli-4a8c01adb5100fd0397aad239edc5e80d13aca13.tar.bz2
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.
-rw-r--r--CHANGELOG.md2
-rw-r--r--doc/config.example.toml2
-rw-r--r--doc/nitrocli.118
-rw-r--r--doc/nitrocli.1.pdfbin41970 -> 42218 bytes
-rw-r--r--src/args.rs3
-rw-r--r--src/commands.rs10
-rw-r--r--src/config.rs5
-rw-r--r--src/tests/run.rs92
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
--- a/doc/nitrocli.1.pdf
+++ b/doc/nitrocli.1.pdf
Binary files 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<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 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<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 {
// 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(())
@@ -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(())
+}