aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--doc/config.example.toml3
-rw-r--r--doc/nitrocli.126
-rw-r--r--doc/nitrocli.1.pdfbin40972 -> 41970 bytes
-rw-r--r--src/args.rs10
-rw-r--r--src/commands.rs25
-rw-r--r--src/config.rs25
-rw-r--r--src/main.rs1
-rw-r--r--src/tests/status.rs10
9 files changed, 93 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc288ee..29a9faa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ Unreleased
- Reworked connection handling for multiple attached Nitrokey devices:
- Fail if multiple attached devices match the filter options (or no filter
options are set)
+ - Added `--serial-number` option that restricts the serial number of the
+ device to connect to
0.3.4
diff --git a/doc/config.example.toml b/doc/config.example.toml
index a427749..eefdfa0 100644
--- a/doc/config.example.toml
+++ b/doc/config.example.toml
@@ -4,6 +4,9 @@
# The model to connect to (string, "pro" or "storage", default: not set).
model = "pro"
+# The serial number of the device to connect to (list of strings, default:
+# empty).
+serial_numbers = ["0xf00baa", "deadbeef"]
# 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 680af3b..8b04de6 100644
--- a/doc/nitrocli.1
+++ b/doc/nitrocli.1
@@ -12,16 +12,25 @@ 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 option 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 this option
-is not set.
+You can use the \fB\-\-model\fR and \fB\-\-serial-number\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.
.SH OPTIONS
.TP
\fB\-m\fR, \fB\-\-model pro\fR|\fBstorage\fR
Restrict connections to the given device model, see the Device selection
section.
.TP
+\fB\-\-serial-number \fIserial-number\fR
+Restrict connections to the given serial number, see the Device selection
+section.
+\fIserial-number\fR must be a hex string with an optional 0x prefix.
+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\-\-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.
@@ -302,6 +311,10 @@ The following values can be set in the configuration file:
Restrict connections to the given device model (string, default: not set, see
\fB\-\-model\fR).
.TP
+.B serial_numbers
+Restrict connections to the given serial numbers (list of strings, default:
+empty, see \fB\-\-serial-number\fR).
+.TP
.B no_cache
If set to true, do not cache any inquired secrets (boolean, default: false,
see \fB\-\-no\-cache\fR).
@@ -311,6 +324,7 @@ Set the log level (integer, default: 0, see \fB\-\-verbose\fR).
.P
The configuration file must use the TOML format, for example:
model = "pro"
+ serial_numbers = ["0xf00baa", "deadbeef"]
no_cache = false
verbosity = 0
@@ -343,6 +357,10 @@ configuration (see the Config file section):
Restrict connections to the given device model (string, default: not set, see
\fB\-\-model\fR).
.TP
+.B NITROCLI_SERIAL_NUMBERS
+Restrict connections to the given list of serial numbers (comma-separated list
+of strings, default: empty, see \fB\-\-serial-number\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 015f379..73041ae 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 3052afa..0d77806 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -13,6 +13,16 @@ pub struct Args {
/// Selects the device model to connect to
#[structopt(short, long, global = true, possible_values = &DeviceModel::all_str())]
pub model: Option<DeviceModel>,
+ /// Sets the serial number of the device to connect to. Can be set
+ /// multiple times to allow multiple serial numbers
+ // TODO: Add short options (avoid collisions).
+ #[structopt(
+ long = "serial-number",
+ global = true,
+ multiple = true,
+ number_of_values = 1
+ )]
+ pub serial_numbers: Vec<nitrokey::SerialNumber>,
/// 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 05038e0..f718571 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -43,10 +43,22 @@ fn set_log_level(ctx: &mut Context<'_>) {
/// Create a filter string from the program configuration.
fn format_filter(config: &config::Config) -> String {
+ let mut filters = Vec::new();
if let Some(model) = config.model {
- format!(" (filter: model={})", model.as_ref())
- } else {
+ filters.push(format!("model={}", model.as_ref()));
+ }
+ if !config.serial_numbers.is_empty() {
+ let serial_numbers = config
+ .serial_numbers
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<_>>();
+ filters.push(format!("serial number in [{}]", serial_numbers.join(", ")));
+ }
+ if filters.is_empty() {
String::new()
+ } else {
+ format!(" (filter: {})", filters.join(", "))
}
}
@@ -56,7 +68,14 @@ fn find_device(config: &config::Config) -> anyhow::Result<nitrokey::DeviceInfo>
let nkmodel = config.model.map(nitrokey::Model::from);
let mut iter = devices
.into_iter()
- .filter(|device| nkmodel.is_none() || device.model == nkmodel);
+ .filter(|device| nkmodel.is_none() || device.model == nkmodel)
+ .filter(|device| {
+ config.serial_numbers.is_empty()
+ || device
+ .serial_number
+ .map(|sn| config.serial_numbers.contains(&sn))
+ .unwrap_or_default()
+ });
let device = iter
.next()
diff --git a/src/config.rs b/src/config.rs
index aceda38..6f0cd17 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -5,6 +5,9 @@
use std::fs;
use std::path;
+use std::str::FromStr as _;
+
+use serde::de::Error as _;
use crate::args;
@@ -20,10 +23,14 @@ const CONFIG_FILE: &str = "config.toml";
/// The configuration for nitrocli, usually read from configuration
/// files and environment variables.
-#[derive(Clone, Copy, Debug, Default, PartialEq, merge::Merge, serde::Deserialize)]
+#[derive(Clone, Debug, Default, PartialEq, merge::Merge, serde::Deserialize)]
pub struct Config {
/// The model to connect to.
pub model: Option<args::DeviceModel>,
+ /// The serial numbers of the device to connect to.
+ #[merge(strategy = merge::vec::overwrite_empty)]
+ #[serde(default, deserialize_with = "deserialize_serial_number_vec")]
+ pub serial_numbers: Vec<nitrokey::SerialNumber>,
/// Whether to bypass the cache for all secrets or not.
#[merge(strategy = merge::bool::overwrite_false)]
#[serde(default)]
@@ -34,6 +41,18 @@ pub struct Config {
pub verbosity: u8,
}
+fn deserialize_serial_number_vec<'de, D>(d: D) -> Result<Vec<nitrokey::SerialNumber>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ let strings: Vec<String> = serde::Deserialize::deserialize(d).map_err(D::Error::custom)?;
+ let result: Result<Vec<_>, _> = strings
+ .iter()
+ .map(|s| nitrokey::SerialNumber::from_str(s))
+ .collect();
+ result.map_err(D::Error::custom)
+}
+
impl Config {
pub fn load() -> anyhow::Result<Self> {
use merge::Merge as _;
@@ -51,6 +70,10 @@ impl Config {
if args.model.is_some() {
self.model = args.model;
}
+ if !args.serial_numbers.is_empty() {
+ // TODO: Don't clone.
+ self.serial_numbers = args.serial_numbers.clone();
+ }
if args.no_cache {
self.no_cache = true;
}
diff --git a/src/main.rs b/src/main.rs
index 16715f9..baad15c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,6 @@
// Copyright (C) 2017-2020 The Nitrocli Developers
// SPDX-License-Identifier: GPL-3.0-or-later
-#![allow(clippy::trivially_copy_pass_by_ref)]
#![warn(
bad_style,
dead_code,
diff --git a/src/tests/status.rs b/src/tests/status.rs
index 7946929..268e36f 100644
--- a/src/tests/status.rs
+++ b/src/tests/status.rs
@@ -28,6 +28,16 @@ fn not_found_pro() {
assert_eq!(err, "Nitrokey device not found (filter: model=pro)");
}
+#[test_device]
+fn not_found_by_serial_number() {
+ let res = Nitrocli::new().handle(&["status", "--model=storage", "--serial-number=deadbeef"]);
+ let err = res.unwrap_err().to_string();
+ assert_eq!(
+ err,
+ "Nitrokey device not found (filter: model=storage, serial number in [0xdeadbeef])"
+ );
+}
+
#[test_device(pro)]
fn output_pro(model: nitrokey::Model) -> anyhow::Result<()> {
let re = regex::Regex::new(