diff options
Diffstat (limited to 'nitrokey/src')
| -rw-r--r-- | nitrokey/src/device/mod.rs | 238 | ||||
| -rw-r--r-- | nitrokey/src/device/pro.rs | 18 | ||||
| -rw-r--r-- | nitrokey/src/device/storage.rs | 161 | ||||
| -rw-r--r-- | nitrokey/src/device/wrapper.rs | 9 | ||||
| -rw-r--r-- | nitrokey/src/error.rs | 4 | ||||
| -rw-r--r-- | nitrokey/src/lib.rs | 136 | 
6 files changed, 539 insertions, 27 deletions
| diff --git a/nitrokey/src/device/mod.rs b/nitrokey/src/device/mod.rs index 5e15f08..0234bf0 100644 --- a/nitrokey/src/device/mod.rs +++ b/nitrokey/src/device/mod.rs @@ -5,6 +5,8 @@ mod pro;  mod storage;  mod wrapper; +use std::convert::{TryFrom, TryInto}; +use std::ffi;  use std::fmt;  use libc; @@ -16,12 +18,14 @@ 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;  pub use storage::{ -    SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, +    OperationStatus, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, +    VolumeStatus,  };  pub use wrapper::DeviceWrapper; @@ -43,6 +47,114 @@ impl fmt::Display for Model {      }  } +impl From<Model> 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<nitrokey_sys::NK_device_model> for Model { +    type Error = Error; + +    fn try_from(model: nitrokey_sys::NK_device_model) -> Result<Self, Error> { +        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), +        } +    } +} + +/// 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<Model>, +    /// 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<String>, +} + +impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo { +    type Error = Error; + +    fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result<DeviceInfo, Error> { +        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, it uses the last +/// eight characters.  If these are all zero, the first eight characters are used instead.  This +/// function also makes sure that the returned string is lowercase, consistent with libnitrokey’s +/// hex string formatting. +/// +/// The reason for this behavior is that the Nitrokey Storage does not report its serial number at +/// all (all zero value), while the Nitrokey Pro with firmware 0.9 or later writes its serial +/// number to the last eight characters.  Nitrokey Pro devices with firmware 0.8 or earlier wrote +/// their serial number to the first eight characters. +fn get_hidapi_serial_number(serial_number: &str) -> Option<String> { +    let len = serial_number.len(); +    if len < 8 { +        // The serial number in the USB descriptor has 12 bytes, we need at least four of them +        return None; +    } + +    let iter = serial_number.char_indices().rev(); +    let first_non_null = iter.skip_while(|(_, c)| *c == '0').next(); +    if let Some((i, _)) = first_non_null { +        if len - i < 8 { +            // The last eight characters contain at least one non-zero character --> use them +            let mut serial_number = serial_number.split_at(len - 8).1.to_string(); +            serial_number.make_ascii_lowercase(); +            Some(serial_number) +        } else { +            // The last eight characters are all zero --> use the first eight +            let mut serial_number = serial_number.split_at(8).0.to_string(); +            serial_number.make_ascii_lowercase(); +            Some(serial_number) +        } +    } else { +        // The serial number is all zero +        None +    } +} +  /// A firmware version for a Nitrokey device.  #[derive(Clone, Copy, Debug, PartialEq)]  pub struct FirmwareVersion { @@ -58,6 +170,36 @@ impl fmt::Display for FirmwareVersion {      }  } +/// The status information common to all Nitrokey devices. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Status { +    /// The firmware version of the device. +    pub firmware_version: FirmwareVersion, +    /// The serial number of the device. +    pub serial_number: u32, +    /// The configuration of the device. +    pub config: Config, +} + +impl From<nitrokey_sys::NK_status> for Status { +    fn from(status: nitrokey_sys::NK_status) -> Self { +        Self { +            firmware_version: FirmwareVersion { +                major: status.firmware_version_major, +                minor: status.firmware_version_minor, +            }, +            serial_number: status.serial_number_smart_card, +            config: RawConfig { +                numlock: status.config_numlock, +                capslock: status.config_capslock, +                scrollock: status.config_scrolllock, +                user_password: status.otp_user_password, +            } +            .into(), +        } +    } +} +  /// A Nitrokey device.  ///  /// This trait provides the commands that can be executed without authentication and that are @@ -102,6 +244,35 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt      /// # }      fn get_model(&self) -> Model; +    /// Returns the status of the Nitrokey device. +    /// +    /// This methods returns the status information common to all Nitrokey devices as a +    /// [`Status`][] struct.  Some models may provide more information, for example +    /// [`get_storage_status`][] returns the [`StorageStatus`][] struct. +    /// +    /// # Errors +    /// +    /// - [`NotConnected`][] if the Nitrokey device has been disconnected +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::Device; +    /// +    /// let mut manager = nitrokey::take()?; +    /// let device = manager.connect()?; +    /// let status = device.get_status()?; +    /// println!("Firmware version: {}", status.firmware_version); +    /// println!("Serial number:    {:x}", status.serial_number); +    /// # Ok::<(), nitrokey::Error>(()) +    /// ``` +    /// +    /// [`get_storage_status`]: struct.Storage.html#method.get_storage_status +    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected +    /// [`Status`]: struct.Status.html +    /// [`StorageStatus`]: struct.StorageStatus.html +    fn get_status(&self) -> Result<Status, Error>; +      /// Returns the serial number of the Nitrokey device.  The serial number is the string      /// representation of a hex number.      /// @@ -428,12 +599,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt      }  } -fn get_connected_model() -> Option<Model> { -    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, Error> { +    Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })  }  pub(crate) fn create_device_wrapper( @@ -449,16 +616,53 @@ pub(crate) fn create_device_wrapper(  pub(crate) fn get_connected_device(      manager: &mut crate::Manager,  ) -> Result<DeviceWrapper<'_>, 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 } +} + +#[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!(None, 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("00001234".to_string()), +            get_hidapi_serial_number("100000001234") +        ); +        assert_eq!( +            Some("12340000".to_string()), +            get_hidapi_serial_number("123400000000") +        ); +        assert_eq!( +            Some("00005678".to_string()), +            get_hidapi_serial_number("000000000000000000005678") +        ); +        assert_eq!( +            Some("00001234".to_string()), +            get_hidapi_serial_number("000012340000000000000000") +        ); +        assert_eq!( +            Some("0000ffff".to_string()), +            get_hidapi_serial_number("00000000000000000000FFFF") +        ); +        assert_eq!( +            Some("0000ffff".to_string()), +            get_hidapi_serial_number("00000000000000000000ffff") +        ); +    }  } diff --git a/nitrokey/src/device/pro.rs b/nitrokey/src/device/pro.rs index a65345e..591b730 100644 --- a/nitrokey/src/device/pro.rs +++ b/nitrokey/src/device/pro.rs @@ -3,8 +3,10 @@  use nitrokey_sys; -use crate::device::{Device, Model}; +use crate::device::{Device, Model, Status}; +use crate::error::Error;  use crate::otp::GenerateOtp; +use crate::util::get_command_result;  /// A Nitrokey Pro device without user or admin authentication.  /// @@ -74,6 +76,20 @@ impl<'a> Device<'a> for Pro<'a> {      fn get_model(&self) -> Model {          Model::Pro      } + +    fn get_status(&self) -> Result<Status, Error> { +        let mut raw_status = nitrokey_sys::NK_status { +            firmware_version_major: 0, +            firmware_version_minor: 0, +            serial_number_smart_card: 0, +            config_numlock: 0, +            config_capslock: 0, +            config_scrolllock: 0, +            otp_user_password: false, +        }; +        get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?; +        Ok(raw_status.into()) +    }  }  impl<'a> GenerateOtp for Pro<'a> {} diff --git a/nitrokey/src/device/storage.rs b/nitrokey/src/device/storage.rs index 370ce36..deb2844 100644 --- a/nitrokey/src/device/storage.rs +++ b/nitrokey/src/device/storage.rs @@ -1,14 +1,16 @@ -// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org> +// Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>  // SPDX-License-Identifier: MIT +use std::convert::TryFrom as _;  use std::fmt; +use std::ops;  use nitrokey_sys; -use crate::device::{Device, FirmwareVersion, Model}; -use crate::error::Error; +use crate::device::{Device, FirmwareVersion, Model, Status}; +use crate::error::{CommandError, Error};  use crate::otp::GenerateOtp; -use crate::util::{get_command_result, get_cstring}; +use crate::util::{get_command_result, get_cstring, get_last_error};  /// A Nitrokey Storage device without user or admin authentication.  /// @@ -141,6 +143,20 @@ pub struct StorageStatus {      pub stick_initialized: bool,  } +/// The progress of a background operation on the Nitrokey. +/// +/// Some commands may start a background operation during which no other commands can be executed. +/// This enum stores the status of a background operation:  Ongoing with a relative progress (up to +/// 100), or idle, i. e. no background operation has been started or the last one has been +/// finished. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum OperationStatus { +    /// A background operation with its progress value (less than or equal to 100). +    Ongoing(u8), +    /// No backgrund operation. +    Idle, +} +  impl<'a> Storage<'a> {      pub(crate) fn new(manager: &'a mut crate::Manager) -> Storage<'a> {          Storage { @@ -525,7 +541,7 @@ impl<'a> Storage<'a> {      /// # fn try_main() -> Result<(), Error> {      /// let mut manager = nitrokey::take()?;      /// let device = manager.connect_storage()?; -    /// match device.get_status() { +    /// match device.get_storage_status() {      ///     Ok(status) => {      ///         println!("SD card ID: {:#x}", status.serial_number_sd_card);      ///     }, @@ -534,7 +550,7 @@ impl<'a> Storage<'a> {      /// #     Ok(())      /// # }      /// ``` -    pub fn get_status(&self) -> Result<StorageStatus, Error> { +    pub fn get_storage_status(&self) -> Result<StorageStatus, Error> {          let mut raw_status = nitrokey_sys::NK_storage_status {              unencrypted_volume_read_only: false,              unencrypted_volume_active: false, @@ -635,11 +651,117 @@ impl<'a> Storage<'a> {          })      } +    /// Returns a range of the SD card that has not been used to during this power cycle. +    /// +    /// The Nitrokey Storage tracks read and write access to the SD card during a power cycle. +    /// This method returns a range of the SD card that has not been accessed during this power +    /// cycle.  The range is relative to the total size of the SD card, so both values are less +    /// than or equal to 100.  This can be used as a guideline when creating a hidden volume. +    /// +    /// # Example +    /// +    /// ```no_run +    /// let mut manager = nitrokey::take()?; +    /// let storage = manager.connect_storage()?; +    /// let usage = storage.get_sd_card_usage()?; +    /// println!("SD card usage: {}..{}", usage.start, usage.end); +    /// # Ok::<(), nitrokey::Error>(()) +    /// ``` +    pub fn get_sd_card_usage(&self) -> Result<ops::Range<u8>, Error> { +        let mut usage_data = nitrokey_sys::NK_SD_usage_data { +            write_level_min: 0, +            write_level_max: 0, +        }; +        let result = unsafe { nitrokey_sys::NK_get_SD_usage_data(&mut usage_data) }; +        match get_command_result(result) { +            Ok(_) => { +                if usage_data.write_level_min > usage_data.write_level_max +                    || usage_data.write_level_max > 100 +                { +                    Err(Error::UnexpectedError) +                } else { +                    Ok(ops::Range { +                        start: usage_data.write_level_min, +                        end: usage_data.write_level_max, +                    }) +                } +            } +            Err(err) => Err(err), +        } +    } +      /// Blinks the red and green LED alternatively and infinitely until the device is reconnected.      pub fn wink(&mut self) -> Result<(), Error> {          get_command_result(unsafe { nitrokey_sys::NK_wink() })      } +    /// Returns the status of an ongoing background operation on the Nitrokey Storage. +    /// +    /// Some commands may start a background operation during which no other commands can be +    /// executed.  This method can be used to check whether such an operation is ongoing. +    /// +    /// Currently, this is only used by the [`fill_sd_card`][] method. +    /// +    /// [`fill_sd_card`]: #method.fill_sd_card +    pub fn get_operation_status(&self) -> Result<OperationStatus, Error> { +        let status = unsafe { nitrokey_sys::NK_get_progress_bar_value() }; +        match status { +            0..=100 => u8::try_from(status) +                .map(OperationStatus::Ongoing) +                .map_err(|_| Error::UnexpectedError), +            -1 => Ok(OperationStatus::Idle), +            -2 => Err(get_last_error()), +            _ => Err(Error::UnexpectedError), +        } +    } + +    /// Overwrites the SD card with random data. +    /// +    /// Ths method starts a background operation that overwrites the SD card with random data. +    /// While this operation is ongoing, no other commands can be executed.  Use the +    /// [`get_operation_status`][] function to check the progress of the operation. +    /// +    /// # Errors +    /// +    /// - [`InvalidString`][] if one of the provided passwords contains a null byte +    /// - [`WrongPassword`][] if the admin password is wrong +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::OperationStatus; +    /// +    /// let mut manager = nitrokey::take()?; +    /// let mut storage = manager.connect_storage()?; +    /// storage.fill_sd_card("12345678")?; +    /// loop { +    ///     match storage.get_operation_status()? { +    ///         OperationStatus::Ongoing(progress) => println!("{}/100", progress), +    ///         OperationStatus::Idle => { +    ///             println!("Done!"); +    ///             break; +    ///         } +    ///     } +    /// } +    /// # Ok::<(), nitrokey::Error>(()) +    /// ``` +    /// +    /// [`get_operation_status`]: #method.get_operation_status +    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString +    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword +    pub fn fill_sd_card(&mut self, admin_pin: &str) -> Result<(), Error> { +        let admin_pin_string = get_cstring(admin_pin)?; +        get_command_result(unsafe { +            nitrokey_sys::NK_fill_SD_card_with_random_data(admin_pin_string.as_ptr()) +        }) +        .or_else(|err| match err { +            // libnitrokey’s C API returns a LongOperationInProgressException with the same error +            // code as the WrongCrc command error, so we cannot distinguish them. +            Error::CommandError(CommandError::WrongCrc) => Ok(()), +            err => Err(err), +        }) +    } +      /// Exports the firmware to the unencrypted volume.      ///      /// This command requires the admin PIN.  The unencrypted volume must be in read-write mode @@ -678,6 +800,33 @@ impl<'a> Device<'a> for Storage<'a> {      fn get_model(&self) -> Model {          Model::Storage      } + +    fn get_status(&self) -> Result<Status, Error> { +        // Currently, the GET_STATUS command does not report the correct firmware version and +        // serial number on the Nitrokey Storage, see [0].  Until this is fixed in libnitrokey, we +        // have to manually execute the GET_DEVICE_STATUS command (get_storage_status) and complete +        // the missing data, see [1]. +        // [0] https://github.com/Nitrokey/nitrokey-storage-firmware/issues/96 +        // [1] https://github.com/Nitrokey/libnitrokey/issues/166 + +        let mut raw_status = nitrokey_sys::NK_status { +            firmware_version_major: 0, +            firmware_version_minor: 0, +            serial_number_smart_card: 0, +            config_numlock: 0, +            config_capslock: 0, +            config_scrolllock: 0, +            otp_user_password: false, +        }; +        get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?; +        let mut status = Status::from(raw_status); + +        let storage_status = self.get_storage_status()?; +        status.firmware_version = storage_status.firmware_version; +        status.serial_number = storage_status.serial_number_smart_card; + +        Ok(status) +    }  }  impl<'a> GenerateOtp for Storage<'a> {} diff --git a/nitrokey/src/device/wrapper.rs b/nitrokey/src/device/wrapper.rs index a3a18f9..69291ad 100644 --- a/nitrokey/src/device/wrapper.rs +++ b/nitrokey/src/device/wrapper.rs @@ -1,7 +1,7 @@  // Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>  // SPDX-License-Identifier: MIT -use crate::device::{Device, Model, Pro, Storage}; +use crate::device::{Device, Model, Pro, Status, Storage};  use crate::error::Error;  use crate::otp::GenerateOtp; @@ -131,4 +131,11 @@ impl<'a> Device<'a> for DeviceWrapper<'a> {              DeviceWrapper::Storage(_) => Model::Storage,          }      } + +    fn get_status(&self) -> Result<Status, Error> { +        match self { +            DeviceWrapper::Pro(dev) => dev.get_status(), +            DeviceWrapper::Storage(dev) => dev.get_status(), +        } +    }  } diff --git a/nitrokey/src/error.rs b/nitrokey/src/error.rs index 9e6adc0..f9af594 100644 --- a/nitrokey/src/error.rs +++ b/nitrokey/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),          }      } diff --git a/nitrokey/src/lib.rs b/nitrokey/src/lib.rs index 059792d..9efad91 100644 --- a/nitrokey/src/lib.rs +++ b/nitrokey/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. @@ -25,6 +29,15 @@  //! passwords – [`get_hotp_code`][] and [`get_totp_code`][].  Depending on the stick configuration,  //! these operations are available without authentication or with user authentication.  //! +//! # Background operations +//! +//! Some commands may start background operations.  During such an operation, every new command +//! will cause a [`WrongCrc`][] error.  To check whether a background operation is currently +//! running, use the [`get_operation_status`][] method. +//! +//! Background operations are only available on the Nitrokey Storage.  Currently, +//! [`fill_sd_card`][] is the only command that triggers a background operation. +//!  //! # Examples  //!  //! Connect to any Nitrokey and print its serial number: @@ -86,8 +99,12 @@  //! [`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 +//! [`fill_sd_card`]: struct.Storage.html#method.fill_sd_card +//! [`get_operation_status`]: struct.Storage.html#method.get_operation_status +//! [`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 @@ -95,6 +112,7 @@  //! [`Admin`]: struct.Admin.html  //! [`DeviceWrapper`]: enum.DeviceWrapper.html  //! [`User`]: struct.User.html +//! [`WrongCrc`]: enum.CommandError.html#variant.WrongCrc  #![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)] @@ -109,8 +127,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; @@ -118,14 +138,16 @@ 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, OperationStatus, Pro, SdCardData, Status, Storage, +    StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus,  };  pub use crate::error::{CommandError, CommunicationError, Error, LibraryError};  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_cstring, 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. @@ -235,6 +257,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 +275,7 @@ impl Manager {      /// ```      ///      /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected +    /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError      pub fn connect(&mut self) -> Result<DeviceWrapper<'_>, Error> {          if unsafe { nitrokey_sys::NK_login_auto() } == 1 {              device::get_connected_device(self) @@ -290,6 +314,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<S: Into<Vec<u8>>>(&mut self, path: S) -> Result<DeviceWrapper<'_>, 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 @@ -414,6 +481,71 @@ pub fn force_take() -> Result<sync::MutexGuard<'static, Manager>, 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.  To connect to a supported model, call the [`connect_path`][] function. +/// +/// # 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>(()) +/// ``` +/// +/// [`connect_path`]: struct.Manager.html#fn.connect_path +/// [`DeviceInfo`]: struct.DeviceInfo.html +/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected +/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error +pub fn list_devices() -> Result<Vec<DeviceInfo>, Error> { +    let ptr = NonNull::new(unsafe { nitrokey_sys::NK_list_devices() }); +    match ptr { +        Some(mut ptr) => { +            let mut vec: Vec<DeviceInfo> = 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<DeviceInfo>, +    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`][]). | 
