Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --usb-path option to select device #122

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`


Expand Down
18 changes: 16 additions & 2 deletions doc/nitrocli.1
Original file line number Diff line number Diff line change
Expand Up @@ -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,
robinkrahl marked this conversation as resolved.
Show resolved Hide resolved
model and serial number (if available).
.SH OPTIONS
.TP
\fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR
Expand All @@ -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.
Expand Down Expand Up @@ -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
Restruict connections to the given USB path (string, default: not set, see
robinkrahl marked this conversation as resolved.
Show resolved Hide resolved
\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).
Expand All @@ -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

Expand Down Expand Up @@ -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).
Expand Down
Binary file modified doc/nitrocli.1.pdf
Binary file not shown.
3 changes: 3 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -75,16 +78,17 @@ 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()
.with_context(|| format!("Nitrokey device not found{}", format_filter(config)))?;

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 \
robinkrahl marked this conversation as resolved.
Show resolved Hide resolved
to select one",
format_filter(config)
);
Ok(device)
Expand Down
5 changes: 5 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
robinkrahl marked this conversation as resolved.
Show resolved Hide resolved
pub usb_path: Option<String>,
/// Whether to bypass the cache for all secrets or not.
#[merge(strategy = merge::bool::overwrite_false)]
#[serde(default)]
Expand Down Expand Up @@ -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;
}
Expand Down
86 changes: 84 additions & 2 deletions src/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later

use std::collections;
use std::ops;
use std::path;

use super::*;
Expand Down Expand Up @@ -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(())
Expand All @@ -142,6 +143,26 @@ fn connect_wrong_serial_number(_model: nitrokey::Model) {
);
}

#[test_device]
fn connect_usb_path(_model: nitrokey::Model) -> anyhow::Result<()> {
let devices = nitrokey::list_devices()?;
for path in devices.iter().map(|d| &d.path) {
let res = Nitrocli::new().handle(&["status", &format!("--usb-path={}", path)]);
assert!(res.is_ok());
robinkrahl marked this conversation as resolved.
Show resolved Hide resolved
}
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()?;
Expand Down Expand Up @@ -172,10 +193,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(())
}