From ddb175bd963070e837aa9207c2dee32ffffc51f4 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 30 Dec 2019 10:53:54 +0100 Subject: Add the Error::UnsupportedModelError variant This patch adds the UnsupportedModelError variant to the Error enum: When parsing the model returned by libnitrokey, we should provide a meaningful error message for unknown values. --- src/error.rs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/error.rs b/src/error.rs index 9e6adc0..f9af594 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,8 @@ pub enum Error { UnexpectedError, /// An unknown error returned by libnitrokey. UnknownError(i64), + /// An error caused by a Nitrokey model that is not supported by this crate. + UnsupportedModelError, /// An error occurred when interpreting a UTF-8 string. Utf8Error(str::Utf8Error), } @@ -102,6 +104,7 @@ impl error::Error for Error { Error::RandError(ref err) => Some(err.as_ref()), Error::UnexpectedError => None, Error::UnknownError(_) => None, + Error::UnsupportedModelError => None, Error::Utf8Error(ref err) => Some(err), } } @@ -118,6 +121,7 @@ impl fmt::Display for Error { Error::RandError(ref err) => write!(f, "RNG error: {}", err), Error::UnexpectedError => write!(f, "An unexpected error occurred"), Error::UnknownError(ref err) => write!(f, "Unknown error: {}", err), + Error::UnsupportedModelError => write!(f, "Unsupported Nitrokey model"), Error::Utf8Error(ref err) => write!(f, "UTF-8 error: {}", err), } } -- cgit v1.2.1 From 6b439950c3c856f1b0eb6df2c9e89387b1697608 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 28 Dec 2019 23:34:58 +0100 Subject: Implement conversion traits for Model and NK_device_model A nitrokey_sys::NK_device_model (= u32) value may correspond to a nitrokey::Model, and vice versa. This patch adds the appropriate From and TryFrom implementations. --- src/device/mod.rs | 44 +++++++++++++++++++++++++++++--------------- src/lib.rs | 2 ++ 2 files changed, 31 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/device/mod.rs b/src/device/mod.rs index 5e15f08..a3a0255 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -5,6 +5,7 @@ mod pro; mod storage; mod wrapper; +use std::convert::TryFrom; use std::fmt; use libc; @@ -43,6 +44,30 @@ impl fmt::Display for Model { } } +impl From for nitrokey_sys::NK_device_model { + fn from(model: Model) -> Self { + match model { + Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, + Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, + } + } +} + +impl TryFrom for Model { + type Error = Error; + + fn try_from(model: nitrokey_sys::NK_device_model) -> Result { + match model { + nitrokey_sys::NK_device_model_NK_DISCONNECTED => { + Err(CommunicationError::NotConnected.into()) + } + nitrokey_sys::NK_device_model_NK_PRO => Ok(Model::Pro), + nitrokey_sys::NK_device_model_NK_STORAGE => Ok(Model::Storage), + _ => Err(Error::UnsupportedModelError), + } + } +} + /// A firmware version for a Nitrokey device. #[derive(Clone, Copy, Debug, PartialEq)] pub struct FirmwareVersion { @@ -428,12 +453,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt } } -fn get_connected_model() -> Option { - match unsafe { nitrokey_sys::NK_get_device_model() } { - nitrokey_sys::NK_device_model_NK_PRO => Some(Model::Pro), - nitrokey_sys::NK_device_model_NK_STORAGE => Some(Model::Storage), - _ => None, - } +fn get_connected_model() -> Result { + Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() }) } pub(crate) fn create_device_wrapper( @@ -449,16 +470,9 @@ pub(crate) fn create_device_wrapper( pub(crate) fn get_connected_device( manager: &mut crate::Manager, ) -> Result, Error> { - match get_connected_model() { - Some(model) => Ok(create_device_wrapper(manager, model)), - None => Err(CommunicationError::NotConnected.into()), - } + Ok(create_device_wrapper(manager, get_connected_model()?)) } pub(crate) fn connect_enum(model: Model) -> bool { - let model = match model { - Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, - Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, - }; - unsafe { nitrokey_sys::NK_login_enum(model) == 1 } + unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } } diff --git a/src/lib.rs b/src/lib.rs index 059792d..0eb4f40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,6 +235,7 @@ impl Manager { /// # Errors /// /// - [`NotConnected`][] if no Nitrokey device is connected + /// - [`UnsupportedModelError`][] if the Nitrokey device is not supported by this crate /// /// # Example /// @@ -252,6 +253,7 @@ impl Manager { /// ``` /// /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError pub fn connect(&mut self) -> Result, Error> { if unsafe { nitrokey_sys::NK_login_auto() } == 1 { device::get_connected_device(self) -- cgit v1.2.1 From bcad061ed3e7777547c1b6fc9223dd65f752d94e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 28 Dec 2019 23:53:24 +0100 Subject: Add the DeviceInfo struct In the next patch, we will add support for the NK_list_devices functions that returns a list of NK_device_info structs with information about the connected devices. This patch introduces the DeviceInfo struct that holds the information returned by NK_list_devices and that can be created from a NK_device_info struct. --- src/device/mod.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 4 +-- 2 files changed, 105 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/device/mod.rs b/src/device/mod.rs index a3a0255..e015886 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -5,7 +5,9 @@ mod pro; mod storage; mod wrapper; -use std::convert::TryFrom; +use std::cmp; +use std::convert::{TryFrom, TryInto}; +use std::ffi; use std::fmt; use libc; @@ -17,7 +19,8 @@ use crate::error::{CommunicationError, Error}; use crate::otp::GenerateOtp; use crate::pws::GetPasswordSafe; use crate::util::{ - get_command_result, get_cstring, get_last_error, result_from_string, result_or_error, + get_command_result, get_cstring, get_last_error, owned_str_from_ptr, result_from_string, + result_or_error, }; pub use pro::Pro; @@ -68,6 +71,73 @@ impl TryFrom for Model { } } +/// Connection information for a Nitrokey device. +#[derive(Clone, Debug, PartialEq)] +pub struct DeviceInfo { + /// The model of the Nitrokey device, or `None` if the model is not supported by this crate. + pub model: Option, + /// The USB device path. + pub path: String, + /// The serial number as a 8-character hex string, or `None` if the device does not expose its + /// serial number. + pub serial_number: Option, +} + +impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo { + type Error = Error; + + fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result { + let model_result = device_info.model.try_into(); + let model_option = model_result.map(Some).or_else(|err| match err { + Error::UnsupportedModelError => Ok(None), + _ => Err(err), + })?; + let serial_number = unsafe { ffi::CStr::from_ptr(device_info.serial_number) } + .to_str() + .map_err(Error::from)?; + Ok(DeviceInfo { + model: model_option, + path: owned_str_from_ptr(device_info.path)?, + serial_number: get_hidapi_serial_number(serial_number), + }) + } +} + +impl fmt::Display for DeviceInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.model { + Some(model) => write!(f, "Nitrokey {}", model)?, + None => write!(f, "Unsupported Nitrokey model")?, + } + write!(f, " at {} with ", self.path)?; + match &self.serial_number { + Some(ref serial_number) => write!(f, "serial no. {}", serial_number), + None => write!(f, "an unknown serial number"), + } + } +} + +/// Parses a serial number returned by hidapi and transforms it to the Nitrokey format. +/// +/// If the serial number is all zero, this function returns `None`. Otherwise, all leading zeros +/// (except the last eight characters) are stripped and `Some(_)` is returned. If the serial +/// number has less than eight characters, leading zeros are added. +fn get_hidapi_serial_number(serial_number: &str) -> Option { + let mut iter = serial_number.char_indices().skip_while(|(_, c)| *c == '0'); + let first_non_null = iter.next(); + if let Some((i, _)) = first_non_null { + // keep at least the last 8 characters + let len = serial_number.len(); + let cut = cmp::min(len.saturating_sub(8), i); + let (_, suffix) = serial_number.split_at(cut); + // if necessary, add leading zeros to reach 8 characters + let fill = 8usize.saturating_sub(len); + Some("0".repeat(fill) + suffix) + } else { + None + } +} + /// A firmware version for a Nitrokey device. #[derive(Clone, Copy, Debug, PartialEq)] pub struct FirmwareVersion { @@ -476,3 +546,34 @@ pub(crate) fn get_connected_device( pub(crate) fn connect_enum(model: Model) -> bool { unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } } + +#[cfg(test)] +mod tests { + use super::get_hidapi_serial_number; + + #[test] + fn hidapi_serial_number() { + assert_eq!(None, get_hidapi_serial_number("")); + assert_eq!(None, get_hidapi_serial_number("00000000000000000")); + assert_eq!( + Some("00001234".to_string()), + get_hidapi_serial_number("1234") + ); + assert_eq!( + Some("00001234".to_string()), + get_hidapi_serial_number("00001234") + ); + assert_eq!( + Some("00001234".to_string()), + get_hidapi_serial_number("000000001234") + ); + assert_eq!( + Some("100000001234".to_string()), + get_hidapi_serial_number("100000001234") + ); + assert_eq!( + Some("10000001234".to_string()), + get_hidapi_serial_number("010000001234") + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0eb4f40..dad4a20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,8 +118,8 @@ use nitrokey_sys; pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; pub use crate::device::{ - Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, StorageStatus, - VolumeMode, VolumeStatus, + Device, DeviceInfo, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, + StorageStatus, VolumeMode, VolumeStatus, }; pub use crate::error::{CommandError, CommunicationError, Error, LibraryError}; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; -- cgit v1.2.1 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. --- src/lib.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) (limited to 'src') 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`][]). -- cgit v1.2.1 From 97c772724dd1fe395e7154e0d71c3b2408436082 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 29 Dec 2019 13:15:04 +0100 Subject: Add the connect_path function to the Manager struct This patch adds the connect_path function to the Manager struct that uses NK_connect_with_path to connect to a Nitrokey device at a given USB path. --- src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index 5600d24..7e8b3ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,10 @@ //! [`connect_model`][], [`connect_pro`][] or [`connect_storage`][] to connect to a specific //! device. //! +//! To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. You can +//! then connect to one of the connected devices using the [`connect_path`][] function of the +//! `Manager` struct. +//! //! You can call [`authenticate_user`][] or [`authenticate_admin`][] to get an authenticated device //! that can perform operations that require authentication. You can use [`device`][] to go back //! to the unauthenticated device. @@ -86,8 +90,10 @@ //! [`take`]: fn.take.html //! [`connect`]: struct.Manager.html#method.connect //! [`connect_model`]: struct.Manager.html#method.connect_model +//! [`connect_path`]: struct.Manager.html#method.connect_path //! [`connect_pro`]: struct.Manager.html#method.connect_pro //! [`connect_storage`]: struct.Manager.html#method.connect_storage +//! [`list_devices`]: fn.list_devices.html //! [`manager`]: trait.Device.html#method.manager //! [`device`]: struct.User.html#method.device //! [`get_hotp_code`]: trait.GenerateOtp.html#method.get_hotp_code @@ -128,7 +134,7 @@ 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; +use crate::util::{get_cstring, get_last_result}; /// The default admin PIN for all Nitrokey devices. pub const DEFAULT_ADMIN_PIN: &str = "12345678"; @@ -296,6 +302,49 @@ impl Manager { } } + /// Connects to a Nitrokey device at the given USB path. + /// + /// To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. The + /// [`DeviceInfo`][] structs returned by that function contain the USB path in the `path` + /// field. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the USB path contains a null byte + /// - [`NotConnected`][] if no Nitrokey device can be found at the given USB path + /// - [`UnsupportedModelError`][] if the model of the Nitrokey device at the given USB path is + /// not supported by this crate + /// + /// # Example + /// + /// ``` + /// use nitrokey::DeviceWrapper; + /// + /// fn use_device(device: DeviceWrapper) {} + /// + /// let mut manager = nitrokey::take()?; + /// let devices = nitrokey::list_devices()?; + /// for device in devices { + /// let device = manager.connect_path(device.path)?; + /// use_device(device); + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`list_devices`]: fn.list_devices.html + /// [`DeviceInfo`]: struct.DeviceInfo.html + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError + pub fn connect_path>>(&mut self, path: S) -> Result, Error> { + let path = get_cstring(path)?; + if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 { + device::get_connected_device(self) + } else { + Err(CommunicationError::NotConnected.into()) + } + } + /// Connects to a Nitrokey Pro. /// /// # Errors @@ -424,7 +473,7 @@ pub fn force_take() -> Result, Error> { /// /// 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. +/// connect to them. To connect to a supported model, call the [`connect_path`][] function. /// /// # Errors /// @@ -455,6 +504,7 @@ pub fn force_take() -> Result, Error> { /// # Ok::<(), nitrokey::Error>(()) /// ``` /// +/// [`connect_path`]: struct.Manager.html#fn.connect_path /// [`DeviceInfo`]: struct.DeviceInfo.html /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected /// [`Utf8Error`]: enum.Error.html#variant.Utf8Error -- cgit v1.2.1