nitrokey 0.9.0

Bindings to libnitrokey for communication with Nitrokey devices
Documentation
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

mod librem;
mod pro;
mod storage;
mod wrapper;

use std::convert::{TryFrom, TryInto};
use std::ffi;
use std::fmt;
use std::str;

use crate::auth::Authenticate;
use crate::config::Config;
use crate::error::{CommunicationError, Error, LibraryError};
use crate::otp::GenerateOtp;
use crate::pws::GetPasswordSafe;
use crate::util::{
    get_command_result, get_cstring, get_struct, owned_str_from_ptr, result_or_error,
};

pub use librem::Librem;
pub use pro::Pro;
pub use storage::{
    OperationStatus, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode,
    VolumeStatus,
};
pub use wrapper::DeviceWrapper;

/// Available Nitrokey models.
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Model {
    /// The Librem Key.
    Librem,
    /// The Nitrokey Storage.
    Storage,
    /// The Nitrokey Pro.
    Pro,
}

impl fmt::Display for Model {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match *self {
            Model::Librem => "Librem Key",
            Model::Pro => "Nitrokey Pro",
            Model::Storage => "Nitrokey Storage",
        })
    }
}

impl From<Model> for nitrokey_sys::NK_device_model {
    fn from(model: Model) -> Self {
        match model {
            Model::Librem => nitrokey_sys::NK_device_model_NK_LIBREM,
            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_LIBREM => Ok(Model::Librem),
            nitrokey_sys::NK_device_model_NK_PRO => Ok(Model::Pro),
            nitrokey_sys::NK_device_model_NK_STORAGE => Ok(Model::Storage),
            _ => Err(Error::UnsupportedModelError),
        }
    }
}

/// Serial number of a Nitrokey device.
///
/// The serial number can be formatted as a string using the [`ToString`][] trait, and it can be
/// parsed from a string using the [`FromStr`][] trait.  It can also be represented as a 32-bit
/// unsigned integer using [`as_u32`][].  This integer is the ID of the smartcard of the Nitrokey
/// device.
///
/// Neither the format of the string representation nor the integer representation are guaranteed
/// to stay the same for new firmware versions.
///
/// [`as_u32`]: #method.as_u32
/// [`FromStr`]: #impl-FromStr
/// [`ToString`]: #impl-ToString
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SerialNumber {
    value: u32,
}

impl SerialNumber {
    /// Creates an emtpty serial number.
    ///
    /// This function can be used to create a placeholder value or to compare a `SerialNumber`
    /// instance with an empty serial number.
    pub fn empty() -> Self {
        SerialNumber::new(0)
    }

    fn new(value: u32) -> Self {
        SerialNumber { value }
    }

    /// Returns the integer reprensentation of this serial number.
    ///
    /// This integer currently is the ID of the smartcard of the Nitrokey device.  Upcoming
    /// firmware versions might change the meaning of this representation, or add additional
    /// components to the serial number.
    // To provide a stable API even if the internal representation of SerialNumber changes, we want
    // to borrow SerialNumber instead of copying it even if it might be less efficient.
    #[allow(clippy::trivially_copy_pass_by_ref)]
    pub fn as_u32(&self) -> u32 {
        self.value
    }
}

impl fmt::Display for SerialNumber {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:#010x}", self.value)
    }
}

impl str::FromStr for SerialNumber {
    type Err = Error;

    /// Try to parse a serial number from a hex string.
    ///
    /// The input string must be a valid hex string.  Optionally, it can include a `0x` prefix.
    ///
    /// # Errors
    ///
    /// - [`InvalidHexString`][] if the given string is not a valid hex string
    ///
    /// # Example
    ///
    /// ```no_run
    /// use std::convert::TryFrom;
    /// use nitrokey::{DeviceInfo, Error, SerialNumber};
    ///
    /// fn find_device(serial_number: &str) -> Result<Option<DeviceInfo>, Error> {
    ///     let serial_number: SerialNumber = serial_number.parse()?;
    ///     Ok(nitrokey::list_devices()?
    ///         .into_iter()
    ///         .filter(|device| device.serial_number == Some(serial_number))
    ///         .next())
    /// }
    ///
    /// ```
    ///
    /// [`InvalidHexString`]: enum.LibraryError.html#variant.InvalidHexString
    fn from_str(s: &str) -> Result<SerialNumber, Error> {
        // ignore leading 0x
        let hex_string = if s.starts_with("0x") {
            s.split_at(2).1
        } else {
            s
        };

        u32::from_str_radix(hex_string, 16)
            .map(SerialNumber::new)
            .map_err(|_| LibraryError::InvalidHexString.into())
    }
}

/// 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 of the device, or `None` if the device does not expose its serial number.
    pub serial_number: Option<SerialNumber>,
}

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) => f.write_str(&model.to_string())?,
            None => f.write_str("Unsupported Nitrokey model")?,
        }
        write!(f, " at {} with ", self.path)?;
        match self.serial_number {
            Some(serial_number) => write!(f, "serial no. {}", serial_number),
            None => write!(f, "an unknown serial number"),
        }
    }
}

/// Parses a serial number returned by hidapi.
///
/// 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.  The
/// selected substring is parse as a hex string and its integer value is returned from the
/// function.  If the string cannot be parsed, this function returns `None`.
///
/// 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<SerialNumber> {
    let len = serial_number.len();
    if len < 8 {
        // The serial number in the USB descriptor has 12 bytes, we need at least four
        return None;
    }

    let mut iter = serial_number.char_indices().rev();
    if let Some((i, _)) = iter.find(|(_, c)| *c != '0') {
        let substr = if len - i < 8 {
            // The last eight characters contain at least one non-zero character --> use them
            serial_number.split_at(len - 8).1
        } else {
            // The last eight characters are all zero --> use the first eight
            serial_number.split_at(8).0
        };
        substr.parse().ok()
    } else {
        // The serial number is all zero
        None
    }
}

/// A firmware version for a Nitrokey device.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FirmwareVersion {
    /// The major firmware version, e. g. 0 in v0.40.
    pub major: u8,
    /// The minor firmware version, e. g. 40 in v0.40.
    pub minor: u8,
}

impl fmt::Display for FirmwareVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "v{}.{}", self.major, self.minor)
    }
}

/// 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: SerialNumber,
    /// 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: SerialNumber::new(status.serial_number_smart_card),
            config: Config::from(&status),
        }
    }
}

/// A Nitrokey device.
///
/// This trait provides the commands that can be executed without authentication and that are
/// present on all supported Nitrokey devices.
pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt::Debug {
    /// Returns the [`Manager`][] instance that has been used to connect to this device.
    ///
    /// # Example
    ///
    /// ```
    /// use nitrokey::{Device, DeviceWrapper};
    ///
    /// fn do_something(device: DeviceWrapper) {
    ///     // reconnect to any device
    ///     let manager = device.into_manager();
    ///     let device = manager.connect();
    ///     // do something with the device
    ///     // ...
    /// }
    ///
    /// match nitrokey::take()?.connect() {
    ///     Ok(device) => do_something(device),
    ///     Err(err) => println!("Could not connect to a Nitrokey: {}", err),
    /// }
    /// # Ok::<(), nitrokey::Error>(())
    /// ```
    ///
    /// [`Manager`]: struct.Manager.html
    fn into_manager(self) -> &'a mut crate::Manager;

    /// Returns the model of the connected Nitrokey device.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// println!("Connected to a Nitrokey {}", device.get_model());
    /// #    Ok(())
    /// # }
    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:    {}", 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.
    ///
    /// For display purpuses, the serial number should be formatted as an 8-digit hex string.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// match device.get_serial_number() {
    ///     Ok(number) => println!("serial no: {}", number),
    ///     Err(err) => eprintln!("Could not get serial number: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    fn get_serial_number(&self) -> Result<SerialNumber, Error> {
        let serial_number = unsafe { nitrokey_sys::NK_device_serial_number_as_u32() };
        result_or_error(SerialNumber::new(serial_number))
    }

    /// Returns the number of remaining authentication attempts for the user.  The total number of
    /// available attempts is three.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let count = device.get_user_retry_count();
    /// match device.get_user_retry_count() {
    ///     Ok(count) => println!("{} remaining authentication attempts (user)", count),
    ///     Err(err) => eprintln!("Could not get user retry count: {}", err),
    /// }
    /// #     Ok(())
    /// # }
    /// ```
    fn get_user_retry_count(&self) -> Result<u8, Error> {
        result_or_error(unsafe { nitrokey_sys::NK_get_user_retry_count() })
    }

    /// Returns the number of remaining authentication attempts for the admin.  The total number of
    /// available attempts is three.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let count = device.get_admin_retry_count();
    /// match device.get_admin_retry_count() {
    ///     Ok(count) => println!("{} remaining authentication attempts (admin)", count),
    ///     Err(err) => eprintln!("Could not get admin retry count: {}", err),
    /// }
    /// #     Ok(())
    /// # }
    /// ```
    fn get_admin_retry_count(&self) -> Result<u8, Error> {
        result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() })
    }

    /// Returns the firmware version.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// match device.get_firmware_version() {
    ///     Ok(version) => println!("Firmware version: {}", version),
    ///     Err(err) => eprintln!("Could not access firmware version: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    fn get_firmware_version(&self) -> Result<FirmwareVersion, Error> {
        let major = result_or_error(unsafe { nitrokey_sys::NK_get_major_firmware_version() })?;
        let minor = result_or_error(unsafe { nitrokey_sys::NK_get_minor_firmware_version() })?;
        Ok(FirmwareVersion { major, minor })
    }

    /// Returns the current configuration of the Nitrokey device.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let config = device.get_config()?;
    /// println!("Num Lock binding:          {:?}", config.num_lock);
    /// println!("Caps Lock binding:         {:?}", config.caps_lock);
    /// println!("Scroll Lock binding:       {:?}", config.scroll_lock);
    /// println!("require password for OTP: {:?}", config.user_password);
    /// #     Ok(())
    /// # }
    /// ```
    fn get_config(&self) -> Result<Config, Error> {
        get_struct(|out| unsafe { nitrokey_sys::NK_read_config_struct(out) })
    }

    /// Changes the administrator PIN.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if one of the provided passwords contains a null byte
    /// - [`WrongPassword`][] if the current admin password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.change_admin_pin("12345678", "12345679") {
    ///     Ok(()) => println!("Updated admin PIN."),
    ///     Err(err) => eprintln!("Failed to update admin PIN: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    fn change_admin_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
        let current_string = get_cstring(current)?;
        let new_string = get_cstring(new)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_change_admin_PIN(current_string.as_ptr(), new_string.as_ptr())
        })
    }

    /// Changes the user PIN.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if one of the provided passwords contains a null byte
    /// - [`WrongPassword`][] if the current user password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.change_user_pin("123456", "123457") {
    ///     Ok(()) => println!("Updated admin PIN."),
    ///     Err(err) => eprintln!("Failed to update admin PIN: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    fn change_user_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
        let current_string = get_cstring(current)?;
        let new_string = get_cstring(new)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_change_user_PIN(current_string.as_ptr(), new_string.as_ptr())
        })
    }

    /// Unlocks the user PIN after three failed login attempts and sets it to the given value.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if one of the provided passwords contains a null byte
    /// - [`WrongPassword`][] if the admin password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.unlock_user_pin("12345678", "123456") {
    ///     Ok(()) => println!("Unlocked user PIN."),
    ///     Err(err) => eprintln!("Failed to unlock user PIN: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    fn unlock_user_pin(&mut self, admin_pin: &str, user_pin: &str) -> Result<(), Error> {
        let admin_pin_string = get_cstring(admin_pin)?;
        let user_pin_string = get_cstring(user_pin)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_unlock_user_password(
                admin_pin_string.as_ptr(),
                user_pin_string.as_ptr(),
            )
        })
    }

    /// Locks the Nitrokey device.
    ///
    /// This disables the password store if it has been unlocked.  On the Nitrokey Storage, this
    /// also disables the volumes if they have been enabled.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.lock() {
    ///     Ok(()) => println!("Locked the Nitrokey device."),
    ///     Err(err) => eprintln!("Could not lock the Nitrokey device: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    fn lock(&mut self) -> Result<(), Error> {
        get_command_result(unsafe { nitrokey_sys::NK_lock_device() })
    }

    /// Performs a factory reset on the Nitrokey device.
    ///
    /// This commands performs a factory reset on the smart card (like the factory reset via `gpg
    /// --card-edit`) and then clears the flash memory (password safe, one-time passwords etc.).
    /// After a factory reset, [`build_aes_key`][] has to be called before the password safe or the
    /// encrypted volume can be used.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if the provided password contains a null byte
    /// - [`WrongPassword`][] if the admin password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.factory_reset("12345678") {
    ///     Ok(()) => println!("Performed a factory reset."),
    ///     Err(err) => eprintln!("Could not perform a factory reset: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    /// [`build_aes_key`]: #method.build_aes_key
    fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin_string = get_cstring(admin_pin)?;
        get_command_result(unsafe { nitrokey_sys::NK_factory_reset(admin_pin_string.as_ptr()) })
    }

    /// Builds a new AES key on the Nitrokey.
    ///
    /// The AES key is used to encrypt the password safe and the encrypted volume.  You may need
    /// to call this method after a factory reset, either using [`factory_reset`][] or using `gpg
    /// --card-edit`.  You can also use it to destroy the data stored in the password safe or on
    /// the encrypted volume.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if the provided password contains a null byte
    /// - [`WrongPassword`][] if the admin password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.build_aes_key("12345678") {
    ///     Ok(()) => println!("New AES keys have been built."),
    ///     Err(err) => eprintln!("Could not build new AES keys: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    /// [`factory_reset`]: #method.factory_reset
    fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin_string = get_cstring(admin_pin)?;
        get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin_string.as_ptr()) })
    }
}

fn get_connected_model() -> Result<Model, Error> {
    Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
}

pub(crate) fn create_device_wrapper(
    manager: &mut crate::Manager,
    model: Model,
) -> DeviceWrapper<'_> {
    match model {
        Model::Librem => Librem::new(manager).into(),
        Model::Pro => Pro::new(manager).into(),
        Model::Storage => Storage::new(manager).into(),
    }
}

pub(crate) fn get_connected_device(
    manager: &mut crate::Manager,
) -> Result<DeviceWrapper<'_>, Error> {
    Ok(create_device_wrapper(manager, get_connected_model()?))
}

pub(crate) fn connect_enum(model: Model) -> bool {
    unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::{get_hidapi_serial_number, LibraryError, SerialNumber};

    #[test]
    fn test_serial_number_display() {
        fn assert_str(s: &str, n: u32) {
            assert_eq!(s.to_owned(), SerialNumber::new(n).to_string());
        }

        assert_str("0x00000000", 0);
        assert_str("0x00001000", 0x1000);
        assert_str("0x12345678", 0x12345678);
    }

    #[test]
    fn test_serial_number_try_from() {
        fn assert_ok(v: u32, s: &str) {
            assert_eq!(SerialNumber::new(v), SerialNumber::from_str(s).unwrap());
            assert_eq!(
                SerialNumber::new(v),
                SerialNumber::from_str(format!("0x{}", s).as_ref()).unwrap()
            );
        }

        fn assert_err(s: &str) {
            match SerialNumber::from_str(s).unwrap_err() {
                super::Error::LibraryError(LibraryError::InvalidHexString) => {}
                err => assert!(
                    false,
                    "expected InvalidHexString error, got {} (input {})",
                    err, s
                ),
            }
        }

        assert_ok(0x1234, "1234");
        assert_ok(0x1234, "01234");
        assert_ok(0x1234, "001234");
        assert_ok(0x1234, "0001234");

        assert_ok(0, "0");
        assert_ok(0xdeadbeef, "deadbeef");

        assert_err("deadpork");
        assert_err("blubb");
        assert_err("");
    }

    #[test]
    fn test_get_hidapi_serial_number() {
        fn assert_none(s: &str) {
            assert_eq!(None, get_hidapi_serial_number(s));
        }

        fn assert_some(n: u32, s: &str) {
            assert_eq!(Some(SerialNumber::new(n)), get_hidapi_serial_number(s));
        }

        assert_none("");
        assert_none("00000000000000000");
        assert_none("blubb");
        assert_none("1234");

        assert_some(0x1234, "00001234");
        assert_some(0x1234, "000000001234");
        assert_some(0x1234, "100000001234");
        assert_some(0x12340000, "123400000000");
        assert_some(0x5678, "000000000000000000005678");
        assert_some(0x1234, "000012340000000000000000");
        assert_some(0xffff, "00000000000000000000FFFF");
        assert_some(0xffff, "00000000000000000000ffff");
    }
}