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 all commits
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
2 changes: 2 additions & 0 deletions doc/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
22 changes: 18 additions & 4 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 All @@ -54,8 +59,8 @@ Print the nitrocli version and exit.
.TP
.B nitrocli list \fR[\fB-n\fR|\fB\-\-no-connect\fR]
List all attached Nitrokey devices.
This command prints a list of the device path, the model and the serial number
of all attached Nitrokey devices.
This command prints a list of the USB path, the model and the serial number of
all attached Nitrokey devices.
To access the serial number of a Nitrokey Storage device, \fBnitrocli\fR has to
connect to it.
To omit the serial number of Nitrokey Storage devices instead of connecting to
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
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).
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
12 changes: 8 additions & 4 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 \
to select one",
format_filter(config)
);
Ok(device)
Expand Down Expand Up @@ -421,7 +425,7 @@ pub fn list(ctx: &mut Context<'_>, no_connect: bool) -> anyhow::Result<()> {
if device_infos.is_empty() {
println!(ctx, "No Nitrokey device connected")?;
} else {
println!(ctx, "device path\tmodel\tserial number")?;
println!(ctx, "USB path\tmodel\tserial number")?;
let mut manager =
nitrokey::take().context("Failed to acquire access to Nitrokey device manager")?;

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.
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
2 changes: 1 addition & 1 deletion src/tests/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn not_connected() -> anyhow::Result<()> {
#[test_device]
fn connected(model: nitrokey::Model) -> anyhow::Result<()> {
let re = regex::Regex::new(
r#"^device path\tmodel\tserial number
r#"^USB path\tmodel\tserial number
([[:^space:]]+\t(Pro|Storage|unknown)\t0x[[:xdigit:]]+
)+$"#,
)
Expand Down
92 changes: 90 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,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());
robinkrahl marked this conversation as resolved.
Show resolved Hide resolved
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()?;
Expand Down Expand Up @@ -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(())
}