From c74b8b3ea8dc4fe7c6891ae120540f8da5623227 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 29 Dec 2019 12:51:28 +0100 Subject: Add list_devices function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds support for libnitrokey’s NK_list_devices function by introducing the top-level list_devices function. It returns a vector of DeviceInfo structs with information about all connected Nitrokey devices. --- CHANGELOG.md | 1 + TODO.md | 2 -- src/lib.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/device.rs | 40 +++++++++++++++++++++++++++++---- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3798540..15c76b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ SPDX-License-Identifier: CC0-1.0 - Implement connection by path: - Add the `Error::UnsupportedDeviceError` variant. - Add the `DeviceInfo` struct. + - Add the `list_devices` function. # v0.4.0 (2020-01-02) - Remove the `test-pro` and `test-storage` features. diff --git a/TODO.md b/TODO.md index 8adea87..e3708f9 100644 --- a/TODO.md +++ b/TODO.md @@ -8,8 +8,6 @@ SPDX-License-Identifier: CC0-1.0 - `NK_get_SD_usage_data` - `NK_get_progress_bar_value` - `NK_get_status` -- waiting for [libnitrokey issue 166][] - - `NK_list_devices` - - `NK_free_device_info` - `NK_connect_with_path` - Clear passwords from memory. - Lock password safe in `PasswordSafe::drop()` (see [nitrokey-storage-firmware diff --git a/src/lib.rs b/src/lib.rs index dad4a20..5600d24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,8 +109,10 @@ mod otp; mod pws; mod util; +use std::convert::TryInto as _; use std::fmt; use std::marker; +use std::ptr::NonNull; use std::sync; use nitrokey_sys; @@ -126,6 +128,8 @@ pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; pub use crate::pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; pub use crate::util::LogLevel; +use crate::util::get_last_result; + /// The default admin PIN for all Nitrokey devices. pub const DEFAULT_ADMIN_PIN: &str = "12345678"; /// The default user PIN for all Nitrokey devices. @@ -416,6 +420,70 @@ pub fn force_take() -> Result, Error> { } } +/// List all connected Nitrokey devices. +/// +/// This functions returns a vector with [`DeviceInfo`][] structs that contain information about +/// all connected Nitrokey devices. It will even list unsupported models, although you cannot +/// connect to them. +/// +/// # Errors +/// +/// - [`NotConnected`][] if a Nitrokey device has been disconnected during enumeration +/// - [`Utf8Error`][] if the USB path or the serial number returned by libnitrokey are invalid +/// UTF-8 strings +/// +/// # Example +/// +/// ``` +/// let devices = nitrokey::list_devices()?; +/// if devices.is_empty() { +/// println!("No connected Nitrokey devices found."); +/// } else { +/// println!("model\tpath\tserial number"); +/// for device in devices { +/// match device.model { +/// Some(model) => print!("{}", model), +/// None => print!("unsupported"), +/// } +/// print!("\t{}\t", device.path); +/// match device.serial_number { +/// Some(serial_number) => println!("{}", serial_number), +/// None => println!("unknown"), +/// } +/// } +/// } +/// # Ok::<(), nitrokey::Error>(()) +/// ``` +/// +/// [`DeviceInfo`]: struct.DeviceInfo.html +/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected +/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error +pub fn list_devices() -> Result, Error> { + let ptr = NonNull::new(unsafe { nitrokey_sys::NK_list_devices() }); + match ptr { + Some(mut ptr) => { + let mut vec: Vec = Vec::new(); + push_device_info(&mut vec, unsafe { ptr.as_ref() })?; + unsafe { + nitrokey_sys::NK_free_device_info(ptr.as_mut()); + } + Ok(vec) + } + None => get_last_result().map(|_| Vec::new()), + } +} + +fn push_device_info( + vec: &mut Vec, + info: &nitrokey_sys::NK_device_info, +) -> Result<(), Error> { + vec.push(info.try_into()?); + if let Some(ptr) = NonNull::new(info.next) { + push_device_info(vec, unsafe { ptr.as_ref() })?; + } + Ok(()) +} + /// Enables or disables debug output. Calling this method with `true` is equivalent to setting the /// log level to `Debug`; calling it with `false` is equivalent to the log level `Error` (see /// [`set_log_level`][]). diff --git a/tests/device.rs b/tests/device.rs index e367558..5e60002 100644 --- a/tests/device.rs +++ b/tests/device.rs @@ -8,8 +8,8 @@ use std::process::Command; use std::{thread, time}; use nitrokey::{ - Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, Error, - GenerateOtp, GetPasswordSafe, LibraryError, OtpMode, OtpSlotData, Storage, VolumeMode, + Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, DeviceInfo, + Error, GenerateOtp, GetPasswordSafe, LibraryError, OtpMode, OtpSlotData, Storage, VolumeMode, DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN, }; use nitrokey_test::test as test_device; @@ -31,6 +31,33 @@ fn count_nitrokey_block_devices() -> usize { .count() } +#[test_device] +fn list_no_devices() { + let devices = nitrokey::list_devices(); + assert_ok!(Vec::::new(), devices); +} + +#[test_device] +fn list_devices(_device: DeviceWrapper) { + let devices = unwrap_ok!(nitrokey::list_devices()); + for device in devices { + assert!(!device.path.is_empty()); + if let Some(model) = device.model { + match model { + nitrokey::Model::Pro => { + assert!(device.serial_number.is_some()); + let serial_number = device.serial_number.unwrap(); + assert!(!serial_number.is_empty()); + assert_valid_serial_number(&serial_number); + }, + nitrokey::Model::Storage => { + assert_eq!(None, device.serial_number); + }, + } + } + } +} + #[test_device] fn connect_no_device() { let mut manager = unwrap_ok!(nitrokey::take()); @@ -83,12 +110,17 @@ fn disconnect(device: DeviceWrapper) { assert_empty_serial_number(); } +fn assert_valid_serial_number(serial_number: &str) { + assert!(serial_number.is_ascii()); + assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit())); +} + #[test_device] fn get_serial_number(device: DeviceWrapper) { let serial_number = unwrap_ok!(device.get_serial_number()); - assert!(serial_number.is_ascii()); - assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit())); + assert_valid_serial_number(&serial_number); } + #[test_device] fn get_firmware_version(device: Pro) { let version = unwrap_ok!(device.get_firmware_version()); -- cgit v1.2.1