~ireas/nitrokey-rs-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
13 2

[PATCH 0/5] Extract backend and support multiple devices

Details
Message ID
<cover.1645695842.git.robin.krahl@ireas.org>
DKIM signature
missing
Download raw message
This patch series is a first step to implement the Device Management
RFC:
	https://lists.sr.ht/~ireas/nitrokey-rs-dev/%3C20210611175806.GA1098%40ireas.org%3E

It contains two major changes:

Firstly, all nitrokey-sys interactions are extracted to a new backend
module.  This makes it easier to synchronize them and allows us to add
support for multiple backends in the future.  (Please don’t pay too much
attention to the structure of the backend.  This will be improved in the
future.)

Secondly, we support connecting to multiple devices at the same time.
This is not supported by libnitrokey, but we work around that by keeping
track of the path of the currently connected device and re-connecting if
necessary.

The changes are also available on the backend branch of the main
repository:
	https://git.sr.ht/~ireas/nitrokey-rs/log/backend

Robin Krahl (5):
  Move nitrokey-sys-specific code to backend
  Always connect by path
  Use mutex for libnitrokey operations
  Allow multiple devices per Manager instance
  Add OTP example using multiple devices

 CHANGELOG.md               |   3 +-
 examples/list-devices.rs   |   2 +-
 examples/multi-otp.rs      |  65 +++++
 examples/otp.rs            |   2 +-
 src/auth.rs                | 154 ++++------
 src/backend/conversions.rs | 258 +++++++++++++++++
 src/backend/mod.rs         | 560 +++++++++++++++++++++++++++++++++++++
 src/backend/util.rs        |  76 +++++
 src/config.rs              |  71 +----
 src/device/librem.rs       |  32 ++-
 src/device/mod.rs          | 259 +++--------------
 src/device/pro.rs          |  32 ++-
 src/device/storage.rs      | 229 ++++-----------
 src/device/wrapper.rs      |  23 +-
 src/error.rs               |   9 +-
 src/lib.rs                 | 147 ++++------
 src/otp.rs                 |  45 ++-
 src/pws.rs                 | 107 +++----
 src/util.rs                |  79 +-----
 tests/device.rs            |  46 ++-
 20 files changed, 1334 insertions(+), 865 deletions(-)
 create mode 100644 examples/multi-otp.rs
 create mode 100644 src/backend/conversions.rs
 create mode 100644 src/backend/mod.rs
 create mode 100644 src/backend/util.rs

-- 
2.20.1

[PATCH 1/5] Move nitrokey-sys-specific code to backend

Details
Message ID
<20ba5a6c3773fbf09cb45e57aa01997b9f2d3596.1645695842.git.robin.krahl@ireas.org>
In-Reply-To
<cover.1645695842.git.robin.krahl@ireas.org> (view parent)
DKIM signature
missing
Download raw message
Patch: +1041 -724
With this patch, we move all code that interacts with the nitrokey-sys
code to a new backend module.  This is now the only module that may use
unsafe code.  This is a first step for supporting multiple backends in
the future.

Note that the structure of the backend will be improved in the future.
---
 src/auth.rs                | 142 +++--------
 src/backend/conversions.rs | 258 +++++++++++++++++++
 src/backend/mod.rs         | 507 +++++++++++++++++++++++++++++++++++++
 src/backend/util.rs        |  76 ++++++
 src/config.rs              |  71 +-----
 src/device/librem.rs       |  12 +-
 src/device/mod.rs          | 199 ++-------------
 src/device/pro.rs          |  12 +-
 src/device/storage.rs      | 187 +++-----------
 src/device/wrapper.rs      |   5 +-
 src/error.rs               |   9 +-
 src/lib.rs                 |  96 ++-----
 src/otp.rs                 |  27 +-
 src/pws.rs                 |  85 +++----
 src/util.rs                |  79 +-----
 15 files changed, 1041 insertions(+), 724 deletions(-)
 create mode 100644 src/backend/conversions.rs
 create mode 100644 src/backend/mod.rs
 create mode 100644 src/backend/util.rs

diff --git a/src/auth.rs b/src/auth.rs
index f2b47c1..4dddecf 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -1,25 +1,23 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::convert::TryInto as _;
use std::ffi::CString;
use std::ffi::{CStr, CString};
use std::marker;
use std::ops;
use std::os::raw::c_char;
use std::os::raw::c_int;

use crate::backend::{self, DeviceExt};
use crate::config::Config;
use crate::device::{Device, DeviceWrapper, Librem, Pro, Storage};
use crate::error::Error;
use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData, RawOtpSlotData};
use crate::util::{generate_password, get_command_result, get_cstring, result_from_string};
use crate::otp::{ConfigureOtp, GenerateOtp, OtpSlotData};
use crate::util::generate_password;

static TEMPORARY_PASSWORD_LENGTH: usize = 25;

/// Provides methods to authenticate as a user or as an admin using a PIN.  The authenticated
/// methods will consume the current device instance.  On success, they return the authenticated
/// device.  Otherwise, they return the current unauthenticated device and the error code.
pub trait Authenticate<'a> {
pub trait Authenticate<'a>: DeviceExt {
    /// Performs user authentication.  This method consumes the device.  If successful, an
    /// authenticated device is returned.  Otherwise, the current unauthenticated device and the
    /// error are returned.
@@ -65,7 +63,10 @@ pub trait Authenticate<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    fn authenticate_user(self, password: &str) -> Result<User<'a, Self>, (Self, Error)>
    where
        Self: Device<'a> + Sized;
        Self: Device<'a> + Sized,
    {
        authenticate(self, password, backend::Device::authenticate_user)
    }

    /// Performs admin authentication.  This method consumes the device.  If successful, an
    /// authenticated device is returned.  Otherwise, the current unauthenticated device and the
@@ -112,7 +113,10 @@ pub trait Authenticate<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    fn authenticate_admin(self, password: &str) -> Result<Admin<'a, Self>, (Self, Error)>
    where
        Self: Device<'a> + Sized;
        Self: Device<'a> + Sized,
    {
        authenticate(self, password, backend::Device::authenticate_admin)
    }
}

trait AuthenticatedDevice<T> {
@@ -155,21 +159,19 @@ fn authenticate<'a, D, A, T>(device: D, password: &str, callback: T) -> Result<A
where
    D: Device<'a>,
    A: AuthenticatedDevice<D>,
    T: Fn(*const c_char, *const c_char) -> c_int,
    T: Fn(&backend::Device, &CStr, &CStr) -> Result<(), Error>,
{
    let temp_password = match generate_password(TEMPORARY_PASSWORD_LENGTH) {
        Ok(temp_password) => temp_password,
        Err(err) => return Err((device, err)),
    };
    let password = match get_cstring(password) {
    let password = match CString::new(password) {
        Ok(password) => password,
        Err(err) => return Err((device, err)),
        Err(err) => return Err((device, err.into())),
    };
    let password_ptr = password.as_ptr();
    let temp_password_ptr = temp_password.as_ptr();
    match callback(password_ptr, temp_password_ptr) {
        0 => Ok(A::new(device, temp_password)),
        rv => Err((device, Error::from(rv))),
    match callback(&device.device(), &password, &temp_password) {
        Ok(()) => Ok(A::new(device, temp_password)),
        Err(err) => Err((device, err)),
    }
}

@@ -228,17 +230,15 @@ impl<'a, T: Device<'a>> ops::DerefMut for User<'a, T> {
    }
}

impl<'a, T: Device<'a>> DeviceExt for User<'a, T> {}

impl<'a, T: Device<'a>> GenerateOtp for User<'a, T> {
    fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe {
            nitrokey_sys::NK_get_hotp_code_PIN(slot, self.temp_password.as_ptr())
        })
        DeviceExt::device(self).get_hotp_code_pin(slot, &self.temp_password)
    }

    fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe {
            nitrokey_sys::NK_get_totp_code_PIN(slot, 0, 0, 0, self.temp_password.as_ptr())
        })
        DeviceExt::device(self).get_totp_code_pin(slot, &self.temp_password)
    }
}

@@ -304,57 +304,35 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    pub fn write_config(&mut self, config: Config) -> Result<(), Error> {
        get_command_result(unsafe {
            nitrokey_sys::NK_write_config_struct(config.try_into()?, self.temp_password.as_ptr())
        })
        self.device
            .device()
            .write_config(config, &self.temp_password)
    }
}

impl<'a, T: Device<'a>> ConfigureOtp for Admin<'a, T> {
    fn write_hotp_slot(&mut self, data: OtpSlotData, counter: u64) -> Result<(), Error> {
        let raw_data = RawOtpSlotData::new(data)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_write_hotp_slot(
                raw_data.number,
                raw_data.name.as_ptr(),
                raw_data.secret.as_ptr(),
                counter,
                raw_data.mode == OtpMode::EightDigits,
                raw_data.use_enter,
                raw_data.use_token_id,
                raw_data.token_id.as_ptr(),
                self.temp_password.as_ptr(),
            )
        })
        self.device
            .device()
            .write_hotp_slot(data, counter, &self.temp_password)
    }

    fn write_totp_slot(&mut self, data: OtpSlotData, time_window: u16) -> Result<(), Error> {
        let raw_data = RawOtpSlotData::new(data)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_write_totp_slot(
                raw_data.number,
                raw_data.name.as_ptr(),
                raw_data.secret.as_ptr(),
                time_window,
                raw_data.mode == OtpMode::EightDigits,
                raw_data.use_enter,
                raw_data.use_token_id,
                raw_data.token_id.as_ptr(),
                self.temp_password.as_ptr(),
            )
        })
        self.device
            .device()
            .write_totp_slot(data, time_window, &self.temp_password)
    }

    fn erase_hotp_slot(&mut self, slot: u8) -> Result<(), Error> {
        get_command_result(unsafe {
            nitrokey_sys::NK_erase_hotp_slot(slot, self.temp_password.as_ptr())
        })
        self.device
            .device()
            .erase_hotp_slot(slot, &self.temp_password)
    }

    fn erase_totp_slot(&mut self, slot: u8) -> Result<(), Error> {
        get_command_result(unsafe {
            nitrokey_sys::NK_erase_totp_slot(slot, self.temp_password.as_ptr())
        })
        self.device
            .device()
            .erase_totp_slot(slot, &self.temp_password)
    }
}

@@ -396,44 +374,8 @@ impl<'a> Authenticate<'a> for DeviceWrapper<'a> {
    }
}

impl<'a> Authenticate<'a> for Librem<'a> {
    fn authenticate_user(self, password: &str) -> Result<User<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr)
        })
    }
impl<'a> Authenticate<'a> for Librem<'a> {}

    fn authenticate_admin(self, password: &str) -> Result<Admin<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr)
        })
    }
}
impl<'a> Authenticate<'a> for Pro<'a> {}

impl<'a> Authenticate<'a> for Pro<'a> {
    fn authenticate_user(self, password: &str) -> Result<User<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr)
        })
    }

    fn authenticate_admin(self, password: &str) -> Result<Admin<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr)
        })
    }
}

impl<'a> Authenticate<'a> for Storage<'a> {
    fn authenticate_user(self, password: &str) -> Result<User<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr)
        })
    }

    fn authenticate_admin(self, password: &str) -> Result<Admin<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr)
        })
    }
}
impl<'a> Authenticate<'a> for Storage<'a> {}
diff --git a/src/backend/conversions.rs b/src/backend/conversions.rs
new file mode 100644
index 0000000..b12fac8
--- /dev/null
+++ b/src/backend/conversions.rs
@@ -0,0 +1,258 @@
// Copyright (C) 2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::convert::TryFrom;
use std::convert::TryInto as _;
use std::ffi;

use crate::config;
use crate::device;
use crate::error::{self, Error};

use super::util;

impl TryFrom<config::Config> for nitrokey_sys::NK_config {
    type Error = Error;

    fn try_from(config: config::Config) -> Result<nitrokey_sys::NK_config, Error> {
        Ok(nitrokey_sys::NK_config {
            numlock: option_to_config_otp_slot(config.num_lock)?,
            capslock: option_to_config_otp_slot(config.caps_lock)?,
            scrolllock: option_to_config_otp_slot(config.scroll_lock)?,
            enable_user_password: config.user_password,
            disable_user_password: false,
        })
    }
}

impl From<nitrokey_sys::NK_config> for config::Config {
    fn from(config: nitrokey_sys::NK_config) -> Self {
        config_from_raw(
            config.numlock,
            config.capslock,
            config.scrolllock,
            config.enable_user_password,
        )
    }
}

impl From<&nitrokey_sys::NK_status> for config::Config {
    fn from(status: &nitrokey_sys::NK_status) -> Self {
        config_from_raw(
            status.config_numlock,
            status.config_capslock,
            status.config_scrolllock,
            status.otp_user_password,
        )
    }
}

fn config_from_raw(
    numlock: u8,
    capslock: u8,
    scrollock: u8,
    user_password: bool,
) -> config::Config {
    config::Config {
        num_lock: config_otp_slot_to_option(numlock),
        caps_lock: config_otp_slot_to_option(capslock),
        scroll_lock: config_otp_slot_to_option(scrollock),
        user_password,
    }
}

fn config_otp_slot_to_option(value: u8) -> Option<u8> {
    if value < 3 {
        Some(value)
    } else {
        None
    }
}

fn option_to_config_otp_slot(value: Option<u8>) -> Result<u8, Error> {
    if let Some(value) = value {
        if value < 3 {
            Ok(value)
        } else {
            Err(error::LibraryError::InvalidSlot.into())
        }
    } else {
        Ok(255)
    }
}

impl TryFrom<&nitrokey_sys::NK_device_info> for device::DeviceInfo {
    type Error = Error;

    fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result<device::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(device::DeviceInfo {
            model: model_option,
            path: util::owned_str_from_ptr(device_info.path)?,
            serial_number: get_hidapi_serial_number(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<device::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
    }
}

impl From<device::Model> for nitrokey_sys::NK_device_model {
    fn from(model: device::Model) -> Self {
        match model {
            device::Model::Librem => nitrokey_sys::NK_device_model_NK_LIBREM,
            device::Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE,
            device::Model::Pro => nitrokey_sys::NK_device_model_NK_PRO,
        }
    }
}

impl TryFrom<nitrokey_sys::NK_device_model> for device::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(error::CommunicationError::NotConnected.into())
            }
            nitrokey_sys::NK_device_model_NK_LIBREM => Ok(device::Model::Librem),
            nitrokey_sys::NK_device_model_NK_PRO => Ok(device::Model::Pro),
            nitrokey_sys::NK_device_model_NK_STORAGE => Ok(device::Model::Storage),
            _ => Err(Error::UnsupportedModelError),
        }
    }
}

impl From<nitrokey_sys::NK_status> for device::Status {
    fn from(status: nitrokey_sys::NK_status) -> Self {
        Self {
            firmware_version: device::FirmwareVersion {
                major: status.firmware_version_major,
                minor: status.firmware_version_minor,
            },
            serial_number: device::SerialNumber::new(status.serial_number_smart_card),
            config: config::Config::from(&status),
        }
    }
}

impl From<nitrokey_sys::NK_storage_ProductionTest> for device::StorageProductionInfo {
    fn from(data: nitrokey_sys::NK_storage_ProductionTest) -> Self {
        Self {
            firmware_version: device::FirmwareVersion {
                major: data.FirmwareVersion_au8[0],
                minor: data.FirmwareVersion_au8[1],
            },
            firmware_version_internal: data.FirmwareVersionInternal_u8,
            serial_number_cpu: data.CPU_CardID_u32,
            sd_card: device::SdCardData {
                serial_number: data.SD_CardID_u32,
                size: data.SD_Card_Size_u8,
                manufacturing_year: data.SD_Card_ManufacturingYear_u8,
                manufacturing_month: data.SD_Card_ManufacturingMonth_u8,
                oem: data.SD_Card_OEM_u16,
                manufacturer: data.SD_Card_Manufacturer_u8,
            },
        }
    }
}

impl From<nitrokey_sys::NK_storage_status> for device::StorageStatus {
    fn from(status: nitrokey_sys::NK_storage_status) -> Self {
        Self {
            unencrypted_volume: device::VolumeStatus {
                read_only: status.unencrypted_volume_read_only,
                active: status.unencrypted_volume_active,
            },
            encrypted_volume: device::VolumeStatus {
                read_only: status.encrypted_volume_read_only,
                active: status.encrypted_volume_active,
            },
            hidden_volume: device::VolumeStatus {
                read_only: status.hidden_volume_read_only,
                active: status.hidden_volume_active,
            },
            firmware_version: device::FirmwareVersion {
                major: status.firmware_version_major,
                minor: status.firmware_version_minor,
            },
            firmware_locked: status.firmware_locked,
            serial_number_sd_card: status.serial_number_sd_card,
            serial_number_smart_card: status.serial_number_smart_card,
            user_retry_count: status.user_retry_count,
            admin_retry_count: status.admin_retry_count,
            new_sd_card_found: status.new_sd_card_found,
            filled_with_random: status.filled_with_random,
            stick_initialized: status.stick_initialized,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::get_hidapi_serial_number;
    use crate::device::SerialNumber;

    #[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");
    }
}
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
new file mode 100644
index 0000000..6a2c4e2
--- /dev/null
+++ b/src/backend/mod.rs
@@ -0,0 +1,507 @@
// Copyright (C) 2021-2022 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

// We need unsafe code to interact with nitrokey-sys.
#![allow(unsafe_code)]

mod conversions;
mod util;

use std::convert::TryFrom as _;
use std::convert::TryInto as _;
use std::ffi;
use std::ops;
use std::ptr;

use crate::config;
use crate::device;
use crate::error::{self, Error};
use crate::otp;
use crate::Version;

pub fn set_debug(state: bool) {
    unsafe {
        nitrokey_sys::NK_set_debug(state);
    }
}

pub fn set_log_level(level: crate::util::LogLevel) {
    unsafe {
        nitrokey_sys::NK_set_debug_level(level.into());
    }
}

pub fn get_library_version() -> Result<Version, Error> {
    // NK_get_library_version returns a static string, so we don’t have to free the pointer.
    let git = unsafe { nitrokey_sys::NK_get_library_version() };
    let git = if git.is_null() {
        String::new()
    } else {
        util::owned_str_from_ptr(git)?
    };
    let major = unsafe { nitrokey_sys::NK_get_major_library_version() };
    let minor = unsafe { nitrokey_sys::NK_get_minor_library_version() };
    Ok(Version { git, major, minor })
}

pub fn list_devices() -> Result<Vec<device::DeviceInfo>, Error> {
    let ptr = ptr::NonNull::new(unsafe { nitrokey_sys::NK_list_devices() });
    match ptr {
        Some(mut ptr) => {
            let mut vec: Vec<device::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 => util::get_last_result().map(|_| Vec::new()),
    }
}

fn push_device_info(
    vec: &mut Vec<device::DeviceInfo>,
    info: &nitrokey_sys::NK_device_info,
) -> Result<(), Error> {
    vec.push(info.try_into()?);
    if let Some(ptr) = ptr::NonNull::new(info.next) {
        push_device_info(vec, unsafe { ptr.as_ref() })?;
    }
    Ok(())
}

pub fn connect() -> Result<device::Model, Error> {
    if unsafe { nitrokey_sys::NK_login_auto() } == 1 {
        device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
}

pub fn connect_model(model: device::Model) -> Result<(), Error> {
    if unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } {
        Ok(())
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
}

pub fn connect_path(path: &ffi::CString) -> Result<device::Model, Error> {
    if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
        device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
}

#[derive(Debug)]
pub struct Device {
    _private: (),
}

impl Device {
    fn new() -> Self {
        Self { _private: () }
    }

    pub fn get_serial_number(&self) -> Result<device::SerialNumber, Error> {
        let serial_number = unsafe { nitrokey_sys::NK_device_serial_number_as_u32() };
        util::result_or_error(device::SerialNumber::new(serial_number))
    }

    pub fn get_user_retry_count(&self) -> Result<u8, Error> {
        util::result_or_error(unsafe { nitrokey_sys::NK_get_user_retry_count() })
    }

    pub fn get_admin_retry_count(&self) -> Result<u8, Error> {
        util::result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() })
    }

    pub fn get_firmware_version(&self) -> Result<device::FirmwareVersion, Error> {
        unsafe {
            let major = util::result_or_error(nitrokey_sys::NK_get_major_firmware_version())?;
            let minor = util::result_or_error(nitrokey_sys::NK_get_minor_firmware_version())?;
            Ok(device::FirmwareVersion { major, minor })
        }
    }

    pub fn get_config(&self) -> Result<config::Config, Error> {
        util::get_struct(|out| unsafe { nitrokey_sys::NK_read_config_struct(out) })
    }

    pub fn change_admin_pin(&self, current: &ffi::CStr, new: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_change_admin_PIN(current.as_ptr(), new.as_ptr())
        })
    }

    pub fn change_user_pin(&self, current: &ffi::CStr, new: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_change_user_PIN(current.as_ptr(), new.as_ptr())
        })
    }

    pub fn unlock_user_pin(
        &self,
        admin_pin: &ffi::CStr,
        user_pin: &ffi::CStr,
    ) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_unlock_user_password(admin_pin.as_ptr(), user_pin.as_ptr())
        })
    }

    pub fn lock(&self) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_lock_device() })
    }

    pub fn factory_reset(&self, admin_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_factory_reset(admin_pin.as_ptr()) })
    }

    pub fn build_aes_key(&self, admin_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin.as_ptr()) })
    }

    pub fn logout(&self) {
        unsafe {
            nitrokey_sys::NK_logout();
        }
    }

    pub fn get_status(&self) -> Result<device::Status, Error> {
        util::get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })
    }

    pub fn change_update_pin(&self, current: &ffi::CStr, new: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_change_update_password(current.as_ptr(), new.as_ptr())
        })
    }

    pub fn enable_firmware_update(&self, update_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_enable_firmware_update(update_pin.as_ptr())
        })
    }

    pub fn enable_encrypted_volume(&self, user_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_unlock_encrypted_volume(user_pin.as_ptr())
        })
    }

    pub fn disable_encrypted_volume(&self) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_lock_encrypted_volume() })
    }

    pub fn enable_hidden_volume(&self, volume_password: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_unlock_hidden_volume(volume_password.as_ptr())
        })
    }

    pub fn disable_hidden_volume(&self) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_lock_hidden_volume() })
    }

    pub fn create_hidden_volume(
        &self,
        slot: u8,
        start: u8,
        end: u8,
        password: &ffi::CStr,
    ) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_create_hidden_volume(slot, start, end, password.as_ptr())
        })
    }

    pub fn set_unencrypted_volume_mode(
        &self,
        admin_pin: &ffi::CStr,
        mode: device::VolumeMode,
    ) -> Result<(), Error> {
        let result = match mode {
            device::VolumeMode::ReadOnly => unsafe {
                nitrokey_sys::NK_set_unencrypted_read_only_admin(admin_pin.as_ptr())
            },
            device::VolumeMode::ReadWrite => unsafe {
                nitrokey_sys::NK_set_unencrypted_read_write_admin(admin_pin.as_ptr())
            },
        };
        util::get_command_result(result)
    }

    pub fn set_encrypted_volume_mode(
        &self,
        admin_pin: &ffi::CStr,
        mode: device::VolumeMode,
    ) -> Result<(), Error> {
        let result = match mode {
            device::VolumeMode::ReadOnly => unsafe {
                nitrokey_sys::NK_set_encrypted_read_only(admin_pin.as_ptr())
            },
            device::VolumeMode::ReadWrite => unsafe {
                nitrokey_sys::NK_set_encrypted_read_write(admin_pin.as_ptr())
            },
        };
        util::get_command_result(result)
    }

    pub fn get_storage_status(&self) -> Result<device::StorageStatus, Error> {
        util::get_struct(|out| unsafe { nitrokey_sys::NK_get_status_storage(out) })
    }

    pub fn get_production_info(&self) -> Result<device::StorageProductionInfo, Error> {
        util::get_struct(|out| unsafe { nitrokey_sys::NK_get_storage_production_info(out) })
    }

    pub fn clear_new_sd_card_warning(&self, admin_pin: &ffi::CString) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_clear_new_sd_card_warning(admin_pin.as_ptr())
        })
    }

    pub fn get_sd_card_usage(&self) -> Result<ops::Range<u8>, Error> {
        let mut usage_data = nitrokey_sys::NK_SD_usage_data::default();
        let result = unsafe { nitrokey_sys::NK_get_SD_usage_data(&mut usage_data) };
        match util::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("Invalid write levels".to_owned()))
                } else {
                    Ok(ops::Range {
                        start: usage_data.write_level_min,
                        end: usage_data.write_level_max,
                    })
                }
            }
            Err(err) => Err(err),
        }
    }

    pub fn wink(&self) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_wink() })
    }

    pub fn get_operation_status(&self) -> Result<device::OperationStatus, Error> {
        let status = unsafe { nitrokey_sys::NK_get_progress_bar_value() };
        match status {
            0..=100 => u8::try_from(status)
                .map(device::OperationStatus::Ongoing)
                .map_err(|_| {
                    Error::UnexpectedError("Cannot create u8 from operation status".to_owned())
                }),
            -1 => Ok(device::OperationStatus::Idle),
            -2 => Err(util::get_last_error()),
            _ => Err(Error::UnexpectedError(
                "Invalid operation status".to_owned(),
            )),
        }
    }

    pub fn fill_sd_card(&self, admin_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_fill_SD_card_with_random_data(admin_pin.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(error::CommandError::WrongCrc) => Ok(()),
            err => Err(err),
        })
    }

    pub fn export_firmware(&self, admin_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_export_firmware(admin_pin.as_ptr()) })
    }

    pub fn set_time(&self, time: u64, force: bool) -> Result<(), Error> {
        let result = if force {
            unsafe { nitrokey_sys::NK_totp_set_time(time) }
        } else {
            unsafe { nitrokey_sys::NK_totp_set_time_soft(time) }
        };
        util::get_command_result(result)
    }

    pub fn get_hotp_slot_name(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_hotp_slot_name(slot) })
    }

    pub fn get_totp_slot_name(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_totp_slot_name(slot) })
    }

    pub fn get_hotp_code(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_hotp_code(slot) })
    }

    pub fn get_hotp_code_pin(&self, slot: u8, temp_password: &ffi::CStr) -> Result<String, Error> {
        util::result_from_string(unsafe {
            nitrokey_sys::NK_get_hotp_code_PIN(slot, temp_password.as_ptr())
        })
    }

    pub fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_totp_code(slot, 0, 0, 0) })
    }

    pub fn get_totp_code_pin(&self, slot: u8, temp_password: &ffi::CStr) -> Result<String, Error> {
        util::result_from_string(unsafe {
            nitrokey_sys::NK_get_totp_code_PIN(slot, 0, 0, 0, temp_password.as_ptr())
        })
    }

    pub fn write_config(
        &self,
        config: config::Config,
        temp_password: &ffi::CStr,
    ) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_write_config_struct(config.try_into()?, temp_password.as_ptr())
        })
    }

    pub fn write_hotp_slot(
        &self,
        data: otp::OtpSlotData,
        counter: u64,
        temp_password: &ffi::CStr,
    ) -> Result<(), Error> {
        let raw_data = otp::RawOtpSlotData::new(data)?;
        util::get_command_result(unsafe {
            nitrokey_sys::NK_write_hotp_slot(
                raw_data.number,
                raw_data.name.as_ptr(),
                raw_data.secret.as_ptr(),
                counter,
                raw_data.mode == otp::OtpMode::EightDigits,
                raw_data.use_enter,
                raw_data.use_token_id,
                raw_data.token_id.as_ptr(),
                temp_password.as_ptr(),
            )
        })
    }

    pub fn write_totp_slot(
        &self,
        data: otp::OtpSlotData,
        time_window: u16,
        temp_password: &ffi::CStr,
    ) -> Result<(), Error> {
        let raw_data = otp::RawOtpSlotData::new(data)?;
        util::get_command_result(unsafe {
            nitrokey_sys::NK_write_totp_slot(
                raw_data.number,
                raw_data.name.as_ptr(),
                raw_data.secret.as_ptr(),
                time_window,
                raw_data.mode == otp::OtpMode::EightDigits,
                raw_data.use_enter,
                raw_data.use_token_id,
                raw_data.token_id.as_ptr(),
                temp_password.as_ptr(),
            )
        })
    }

    pub fn erase_hotp_slot(&self, slot: u8, temp_password: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_erase_hotp_slot(slot, temp_password.as_ptr())
        })
    }

    pub fn erase_totp_slot(&self, slot: u8, temp_password: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_erase_totp_slot(slot, temp_password.as_ptr())
        })
    }

    pub fn authenticate_user(
        &self,
        password: &ffi::CStr,
        temp_password: &ffi::CStr,
    ) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_user_authenticate(password.as_ptr(), temp_password.as_ptr())
        })
    }

    pub fn authenticate_admin(
        &self,
        password: &ffi::CStr,
        temp_password: &ffi::CStr,
    ) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_first_authenticate(password.as_ptr(), temp_password.as_ptr())
        })
    }

    pub fn enable_password_safe(&self, user_pin: &ffi::CStr) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_enable_password_safe(user_pin.as_ptr())
        })
    }

    #[allow(deprecated)]
    pub fn get_pws_slot_status(&self) -> Result<[bool; 16], Error> {
        let status_ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_status() };
        if status_ptr.is_null() {
            return Err(util::get_last_error());
        }
        let status_array_ptr = status_ptr as *const [u8; crate::SLOT_COUNT as usize];
        let status_array = unsafe { *status_array_ptr };
        let mut result = [false; crate::SLOT_COUNT as usize];
        for i in 0..crate::SLOT_COUNT {
            result[i as usize] = status_array[i as usize] == 1;
        }
        unsafe {
            nitrokey_sys::NK_free_password_safe_slot_status(status_ptr);
        }
        Ok(result)
    }

    pub fn get_pws_slot_name(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(slot) })
    }

    pub fn get_pws_slot_login(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_login(slot) })
    }

    pub fn get_pws_slot_password(&self, slot: u8) -> Result<String, Error> {
        util::result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_password(slot) })
    }

    pub fn write_pws_slot(
        &self,
        slot: u8,
        name: &ffi::CStr,
        login: &ffi::CStr,
        password: &ffi::CStr,
    ) -> Result<(), Error> {
        util::get_command_result(unsafe {
            nitrokey_sys::NK_write_password_safe_slot(
                slot,
                name.as_ptr(),
                login.as_ptr(),
                password.as_ptr(),
            )
        })
    }

    pub fn erase_pws_slot(&mut self, slot: u8) -> Result<(), Error> {
        util::get_command_result(unsafe { nitrokey_sys::NK_erase_password_safe_slot(slot) })
    }
}

pub trait DeviceExt {
    fn device(&self) -> Device {
        Device::new()
    }
}
diff --git a/src/backend/util.rs b/src/backend/util.rs
new file mode 100644
index 0000000..6835b55
--- /dev/null
+++ b/src/backend/util.rs
@@ -0,0 +1,76 @@
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi::CStr;
use std::os::raw::{c_char, c_int};

use libc::{c_void, free};

use crate::error::Error;

fn str_from_ptr<'a>(ptr: *const c_char) -> Result<&'a str, Error> {
    unsafe { CStr::from_ptr(ptr) }.to_str().map_err(Error::from)
}

pub fn owned_str_from_ptr(ptr: *const c_char) -> Result<String, Error> {
    str_from_ptr(ptr).map(ToOwned::to_owned)
}

fn run_with_string<R, F>(ptr: *const c_char, op: F) -> Result<R, Error>
where
    F: FnOnce(&str) -> Result<R, Error>,
{
    if ptr.is_null() {
        return Err(Error::UnexpectedError(
            "libnitrokey returned a null pointer".to_owned(),
        ));
    }
    let result = str_from_ptr(ptr).and_then(op);
    unsafe { free(ptr as *mut c_void) };
    result
}

pub fn result_from_string(ptr: *const c_char) -> Result<String, Error> {
    run_with_string(ptr, |s| {
        // An empty string can both indicate an error or be a valid return value.  In this case, we
        // have to check the last command status to decide what to return.
        if s.is_empty() {
            get_last_result().map(|_| s.to_owned())
        } else {
            Ok(s.to_owned())
        }
    })
}

pub fn result_or_error<T>(value: T) -> Result<T, Error> {
    get_last_result().and(Ok(value))
}

pub fn get_struct<R, T, F>(f: F) -> Result<R, Error>
where
    R: From<T>,
    T: Default,
    F: Fn(&mut T) -> c_int,
{
    let mut out = T::default();
    get_command_result(f(&mut out))?;
    Ok(out.into())
}

pub fn get_command_result(value: c_int) -> Result<(), Error> {
    if value == 0 {
        Ok(())
    } else {
        Err(Error::from(value))
    }
}

pub fn get_last_result() -> Result<(), Error> {
    get_command_result(unsafe { nitrokey_sys::NK_get_last_command_status() }.into())
}

pub fn get_last_error() -> Error {
    get_last_result().err().unwrap_or_else(|| {
        Error::UnexpectedError("Expected an error, but command status is zero".to_owned())
    })
}
diff --git a/src/config.rs b/src/config.rs
index be31fee..1e86196 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,10 +1,6 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::convert;

use crate::error::{Error, LibraryError};

/// The configuration for a Nitrokey.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Config {
@@ -25,26 +21,6 @@ pub struct Config {
    pub user_password: bool,
}

fn config_otp_slot_to_option(value: u8) -> Option<u8> {
    if value < 3 {
        Some(value)
    } else {
        None
    }
}

fn option_to_config_otp_slot(value: Option<u8>) -> Result<u8, Error> {
    if let Some(value) = value {
        if value < 3 {
            Ok(value)
        } else {
            Err(LibraryError::InvalidSlot.into())
        }
    } else {
        Ok(255)
    }
}

impl Config {
    /// Constructs a new instance of this struct.
    pub fn new(
@@ -60,49 +36,4 @@ impl Config {
            user_password,
        }
    }

    fn from_raw(numlock: u8, capslock: u8, scrollock: u8, user_password: bool) -> Config {
        Config {
            num_lock: config_otp_slot_to_option(numlock),
            caps_lock: config_otp_slot_to_option(capslock),
            scroll_lock: config_otp_slot_to_option(scrollock),
            user_password,
        }
    }
}

impl convert::TryFrom<Config> for nitrokey_sys::NK_config {
    type Error = Error;

    fn try_from(config: Config) -> Result<nitrokey_sys::NK_config, Error> {
        Ok(nitrokey_sys::NK_config {
            numlock: option_to_config_otp_slot(config.num_lock)?,
            capslock: option_to_config_otp_slot(config.caps_lock)?,
            scrolllock: option_to_config_otp_slot(config.scroll_lock)?,
            enable_user_password: config.user_password,
            disable_user_password: false,
        })
    }
}

impl From<nitrokey_sys::NK_config> for Config {
    fn from(config: nitrokey_sys::NK_config) -> Config {
        Config::from_raw(
            config.numlock,
            config.capslock,
            config.scrolllock,
            config.enable_user_password,
        )
    }
}

impl From<&nitrokey_sys::NK_status> for Config {
    fn from(status: &nitrokey_sys::NK_status) -> Config {
        Config::from_raw(
            status.config_numlock,
            status.config_capslock,
            status.config_scrolllock,
            status.otp_user_password,
        )
    }
}
diff --git a/src/device/librem.rs b/src/device/librem.rs
index ea482c1..371f366 100644
--- a/src/device/librem.rs
+++ b/src/device/librem.rs
@@ -1,10 +1,10 @@
// Copyright (C) 2018-2020 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use crate::backend::DeviceExt;
use crate::device::{Device, Model, Status};
use crate::error::Error;
use crate::otp::GenerateOtp;
use crate::util::get_struct;

/// A Librem Key device without user or admin authentication.
///
@@ -60,9 +60,7 @@ impl<'a> Librem<'a> {

impl<'a> Drop for Librem<'a> {
    fn drop(&mut self) {
        unsafe {
            nitrokey_sys::NK_logout();
        }
        self.device().logout()
    }
}

@@ -76,8 +74,10 @@ impl<'a> Device<'a> for Librem<'a> {
    }

    fn get_status(&self) -> Result<Status, Error> {
        get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })
        self.device().get_status()
    }
}

impl<'a> DeviceExt for Librem<'a> {}

impl<'a> GenerateOtp for Librem<'a> {}
diff --git a/src/device/mod.rs b/src/device/mod.rs
index 5f55927..eea8d29 100644
--- a/src/device/mod.rs
+++ b/src/device/mod.rs
@@ -1,4 +1,4 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

mod librem;
@@ -6,19 +6,16 @@ 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::backend;
use crate::config::Config;
use crate::error::{CommunicationError, Error, LibraryError};
use crate::error::{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;
@@ -50,32 +47,6 @@ impl fmt::Display for Model {
    }
}

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
@@ -103,7 +74,7 @@ impl SerialNumber {
        SerialNumber::new(0)
    }

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

@@ -179,26 +150,6 @@ pub struct DeviceInfo {
    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 {
@@ -213,40 +164,6 @@ impl fmt::Display for DeviceInfo {
    }
}

/// 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 {
@@ -273,24 +190,13 @@ pub struct Status {
    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 {
pub trait Device<'a>:
    Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + backend::DeviceExt + fmt::Debug
{
    /// Returns the [`Manager`][] instance that has been used to connect to this device.
    ///
    /// # Example
@@ -382,8 +288,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    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))
        self.device().get_serial_number()
    }

    /// Returns the number of remaining authentication attempts for the user.  The total number of
@@ -407,7 +312,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    fn get_user_retry_count(&self) -> Result<u8, Error> {
        result_or_error(unsafe { nitrokey_sys::NK_get_user_retry_count() })
        self.device().get_user_retry_count()
    }

    /// Returns the number of remaining authentication attempts for the admin.  The total number of
@@ -431,7 +336,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    fn get_admin_retry_count(&self) -> Result<u8, Error> {
        result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() })
        self.device().get_admin_retry_count()
    }

    /// Returns the firmware version.
@@ -453,9 +358,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    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 })
        self.device().get_firmware_version()
    }

    /// Returns the current configuration of the Nitrokey device.
@@ -478,7 +381,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    fn get_config(&self) -> Result<Config, Error> {
        get_struct(|out| unsafe { nitrokey_sys::NK_read_config_struct(out) })
        self.device().get_config()
    }

    /// Changes the administrator PIN.
@@ -508,11 +411,9 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// [`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())
        })
        let current = ffi::CString::new(current)?;
        let new = ffi::CString::new(new)?;
        self.device().change_admin_pin(&current, &new)
    }

    /// Changes the user PIN.
@@ -542,11 +443,9 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// [`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())
        })
        let current = ffi::CString::new(current)?;
        let new = ffi::CString::new(new)?;
        self.device().change_user_pin(&current, &new)
    }

    /// Unlocks the user PIN after three failed login attempts and sets it to the given value.
@@ -576,14 +475,9 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// [`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(),
            )
        })
        let admin_pin = ffi::CString::new(admin_pin)?;
        let user_pin = ffi::CString::new(user_pin)?;
        self.device().unlock_user_pin(&admin_pin, &user_pin)
    }

    /// Locks the Nitrokey device.
@@ -608,7 +502,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    fn lock(&mut self) -> Result<(), Error> {
        get_command_result(unsafe { nitrokey_sys::NK_lock_device() })
        self.device().lock()
    }

    /// Performs a factory reset on the Nitrokey device.
@@ -644,8 +538,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// [`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()) })
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().factory_reset(&admin_pin)
    }

    /// Builds a new AES key on the Nitrokey.
@@ -681,15 +575,11 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// [`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()) })
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().build_aes_key(&admin_pin)
    }
}

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,
@@ -701,21 +591,11 @@ pub(crate) fn create_device_wrapper(
    }
}

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};
    use super::{LibraryError, SerialNumber};

    #[test]
    fn test_serial_number_display() {
@@ -757,29 +637,4 @@ mod tests {
        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");
    }
}
diff --git a/src/device/pro.rs b/src/device/pro.rs
index 2e921e9..03c81e7 100644
--- a/src/device/pro.rs
+++ b/src/device/pro.rs
@@ -1,10 +1,10 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use crate::backend::DeviceExt;
use crate::device::{Device, Model, Status};
use crate::error::Error;
use crate::otp::GenerateOtp;
use crate::util::get_struct;

/// A Nitrokey Pro device without user or admin authentication.
///
@@ -60,9 +60,7 @@ impl<'a> Pro<'a> {

impl<'a> Drop for Pro<'a> {
    fn drop(&mut self) {
        unsafe {
            nitrokey_sys::NK_logout();
        }
        self.device().logout()
    }
}

@@ -76,8 +74,10 @@ impl<'a> Device<'a> for Pro<'a> {
    }

    fn get_status(&self) -> Result<Status, Error> {
        get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })
        self.device().get_status()
    }
}

impl<'a> DeviceExt for Pro<'a> {}

impl<'a> GenerateOtp for Pro<'a> {}
diff --git a/src/device/storage.rs b/src/device/storage.rs
index ad5ca73..c1e03f3 100644
--- a/src/device/storage.rs
+++ b/src/device/storage.rs
@@ -1,14 +1,14 @@
// Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2019-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::convert::TryFrom as _;
use std::ffi;
use std::fmt;
use std::ops;

use crate::backend::DeviceExt;
use crate::device::{Device, FirmwareVersion, Model, SerialNumber, Status};
use crate::error::{CommandError, Error};
use crate::error::Error;
use crate::otp::GenerateOtp;
use crate::util::{get_command_result, get_cstring, get_last_error, get_struct};

/// A Nitrokey Storage device without user or admin authentication.
///
@@ -192,11 +192,9 @@ impl<'a> Storage<'a> {
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn change_update_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_update_password(current_string.as_ptr(), new_string.as_ptr())
        })
        let current = ffi::CString::new(current)?;
        let new = ffi::CString::new(new)?;
        self.device().change_update_pin(&current, &new)
    }

    /// Enables the firmware update mode.
@@ -230,10 +228,8 @@ impl<'a> Storage<'a> {
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn enable_firmware_update(&mut self, update_pin: &str) -> Result<(), Error> {
        let update_pin_string = get_cstring(update_pin)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_enable_firmware_update(update_pin_string.as_ptr())
        })
        let update_pin = ffi::CString::new(update_pin)?;
        self.device().enable_firmware_update(&update_pin)
    }

    /// Enables the encrypted storage volume.
@@ -265,8 +261,8 @@ impl<'a> Storage<'a> {
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn enable_encrypted_volume(&mut self, user_pin: &str) -> Result<(), Error> {
        let user_pin = get_cstring(user_pin)?;
        get_command_result(unsafe { nitrokey_sys::NK_unlock_encrypted_volume(user_pin.as_ptr()) })
        let user_pin = ffi::CString::new(user_pin)?;
        self.device().enable_encrypted_volume(&user_pin)
    }

    /// Disables the encrypted storage volume.
@@ -301,7 +297,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn disable_encrypted_volume(&mut self) -> Result<(), Error> {
        get_command_result(unsafe { nitrokey_sys::NK_lock_encrypted_volume() })
        self.device().disable_encrypted_volume()
    }

    /// Enables a hidden storage volume.
@@ -344,10 +340,8 @@ impl<'a> Storage<'a> {
    /// [`AesDecryptionFailed`]: enum.CommandError.html#variant.AesDecryptionFailed
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    pub fn enable_hidden_volume(&mut self, volume_password: &str) -> Result<(), Error> {
        let volume_password = get_cstring(volume_password)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_unlock_hidden_volume(volume_password.as_ptr())
        })
        let volume_password = ffi::CString::new(volume_password)?;
        self.device().enable_hidden_volume(&volume_password)
    }

    /// Disables a hidden storage volume.
@@ -383,7 +377,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn disable_hidden_volume(&mut self) -> Result<(), Error> {
        get_command_result(unsafe { nitrokey_sys::NK_lock_hidden_volume() })
        self.device().disable_hidden_volume()
    }

    /// Creates a hidden volume.
@@ -428,10 +422,9 @@ impl<'a> Storage<'a> {
        end: u8,
        password: &str,
    ) -> Result<(), Error> {
        let password = get_cstring(password)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_create_hidden_volume(slot, start, end, password.as_ptr())
        })
        let password = ffi::CString::new(password)?;
        self.device()
            .create_hidden_volume(slot, start, end, &password)
    }

    /// Sets the access mode of the unencrypted volume.
@@ -469,16 +462,8 @@ impl<'a> Storage<'a> {
        admin_pin: &str,
        mode: VolumeMode,
    ) -> Result<(), Error> {
        let admin_pin = get_cstring(admin_pin)?;
        let result = match mode {
            VolumeMode::ReadOnly => unsafe {
                nitrokey_sys::NK_set_unencrypted_read_only_admin(admin_pin.as_ptr())
            },
            VolumeMode::ReadWrite => unsafe {
                nitrokey_sys::NK_set_unencrypted_read_write_admin(admin_pin.as_ptr())
            },
        };
        get_command_result(result)
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().set_unencrypted_volume_mode(&admin_pin, mode)
    }

    /// Sets the access mode of the encrypted volume.
@@ -515,16 +500,8 @@ impl<'a> Storage<'a> {
        admin_pin: &str,
        mode: VolumeMode,
    ) -> Result<(), Error> {
        let admin_pin = get_cstring(admin_pin)?;
        let result = match mode {
            VolumeMode::ReadOnly => unsafe {
                nitrokey_sys::NK_set_encrypted_read_only(admin_pin.as_ptr())
            },
            VolumeMode::ReadWrite => unsafe {
                nitrokey_sys::NK_set_encrypted_read_write(admin_pin.as_ptr())
            },
        };
        get_command_result(result)
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().set_encrypted_volume_mode(&admin_pin, mode)
    }

    /// Returns the status of the connected storage device.
@@ -549,7 +526,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn get_storage_status(&self) -> Result<StorageStatus, Error> {
        get_struct(|out| unsafe { nitrokey_sys::NK_get_status_storage(out) })
        self.device().get_storage_status()
    }

    /// Returns the production information for the connected storage device.
@@ -575,7 +552,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn get_production_info(&self) -> Result<StorageProductionInfo, Error> {
        get_struct(|out| unsafe { nitrokey_sys::NK_get_storage_production_info(out) })
        self.device().get_production_info()
    }

    /// Clears the warning for a new SD card.
@@ -608,10 +585,8 @@ impl<'a> Storage<'a> {
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn clear_new_sd_card_warning(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin = get_cstring(admin_pin)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_clear_new_sd_card_warning(admin_pin.as_ptr())
        })
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().clear_new_sd_card_warning(&admin_pin)
    }

    /// Returns a range of the SD card that has not been used to during this power cycle.
@@ -631,28 +606,12 @@ impl<'a> Storage<'a> {
    /// # 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::default();
        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("Invalid write levels".to_owned()))
                } else {
                    Ok(ops::Range {
                        start: usage_data.write_level_min,
                        end: usage_data.write_level_max,
                    })
                }
            }
            Err(err) => Err(err),
        }
        self.device().get_sd_card_usage()
    }

    /// 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() })
        self.device().wink()
    }

    /// Returns the status of an ongoing background operation on the Nitrokey Storage.
@@ -664,19 +623,7 @@ impl<'a> Storage<'a> {
    ///
    /// [`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("Cannot create u8 from operation status".to_owned())
                }),
            -1 => Ok(OperationStatus::Idle),
            -2 => Err(get_last_error()),
            _ => Err(Error::UnexpectedError(
                "Invalid operation status".to_owned(),
            )),
        }
        self.device().get_operation_status()
    }

    /// Overwrites the SD card with random data.
@@ -714,16 +661,8 @@ impl<'a> Storage<'a> {
    /// [`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),
        })
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().fill_sd_card(&admin_pin)
    }

    /// Exports the firmware to the unencrypted volume.
@@ -743,16 +682,14 @@ impl<'a> Storage<'a> {
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn export_firmware(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin_string = get_cstring(admin_pin)?;
        get_command_result(unsafe { nitrokey_sys::NK_export_firmware(admin_pin_string.as_ptr()) })
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().export_firmware(&admin_pin)
    }
}

impl<'a> Drop for Storage<'a> {
    fn drop(&mut self) {
        unsafe {
            nitrokey_sys::NK_logout();
        }
        self.device().logout()
    }
}

@@ -773,7 +710,7 @@ impl<'a> Device<'a> for Storage<'a> {
        // [0] https://github.com/Nitrokey/nitrokey-storage-firmware/issues/96
        // [1] https://github.com/Nitrokey/libnitrokey/issues/166

        let mut status: Status = get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })?;
        let mut status = self.device().get_status()?;

        let storage_status = self.get_storage_status()?;
        status.firmware_version = storage_status.firmware_version;
@@ -783,56 +720,6 @@ impl<'a> Device<'a> for Storage<'a> {
    }
}

impl<'a> GenerateOtp for Storage<'a> {}

impl From<nitrokey_sys::NK_storage_ProductionTest> for StorageProductionInfo {
    fn from(data: nitrokey_sys::NK_storage_ProductionTest) -> Self {
        Self {
            firmware_version: FirmwareVersion {
                major: data.FirmwareVersion_au8[0],
                minor: data.FirmwareVersion_au8[1],
            },
            firmware_version_internal: data.FirmwareVersionInternal_u8,
            serial_number_cpu: data.CPU_CardID_u32,
            sd_card: SdCardData {
                serial_number: data.SD_CardID_u32,
                size: data.SD_Card_Size_u8,
                manufacturing_year: data.SD_Card_ManufacturingYear_u8,
                manufacturing_month: data.SD_Card_ManufacturingMonth_u8,
                oem: data.SD_Card_OEM_u16,
                manufacturer: data.SD_Card_Manufacturer_u8,
            },
        }
    }
}
impl<'a> DeviceExt for Storage<'a> {}

impl From<nitrokey_sys::NK_storage_status> for StorageStatus {
    fn from(status: nitrokey_sys::NK_storage_status) -> Self {
        StorageStatus {
            unencrypted_volume: VolumeStatus {
                read_only: status.unencrypted_volume_read_only,
                active: status.unencrypted_volume_active,
            },
            encrypted_volume: VolumeStatus {
                read_only: status.encrypted_volume_read_only,
                active: status.encrypted_volume_active,
            },
            hidden_volume: VolumeStatus {
                read_only: status.hidden_volume_read_only,
                active: status.hidden_volume_active,
            },
            firmware_version: FirmwareVersion {
                major: status.firmware_version_major,
                minor: status.firmware_version_minor,
            },
            firmware_locked: status.firmware_locked,
            serial_number_sd_card: status.serial_number_sd_card,
            serial_number_smart_card: status.serial_number_smart_card,
            user_retry_count: status.user_retry_count,
            admin_retry_count: status.admin_retry_count,
            new_sd_card_found: status.new_sd_card_found,
            filled_with_random: status.filled_with_random,
            stick_initialized: status.stick_initialized,
        }
    }
}
impl<'a> GenerateOtp for Storage<'a> {}
diff --git a/src/device/wrapper.rs b/src/device/wrapper.rs
index 9de0418..f9701a5 100644
--- a/src/device/wrapper.rs
+++ b/src/device/wrapper.rs
@@ -1,6 +1,7 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use crate::backend::DeviceExt;
use crate::device::{Device, Librem, Model, Pro, Status, Storage};
use crate::error::Error;
use crate::otp::GenerateOtp;
@@ -153,3 +154,5 @@ impl<'a> Device<'a> for DeviceWrapper<'a> {
        }
    }
}

impl<'a> DeviceExt for DeviceWrapper<'a> {}
diff --git a/src/error.rs b/src/error.rs
index f1e91c3..3908b8a 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,7 +1,8 @@
// Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2019-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::error;
use std::ffi;
use std::fmt;
use std::os::raw;
use std::str;
@@ -65,6 +66,12 @@ impl From<LibraryError> for Error {
    }
}

impl From<ffi::NulError> for Error {
    fn from(_error: ffi::NulError) -> Self {
        LibraryError::InvalidString.into()
    }
}

impl From<str::Utf8Error> for Error {
    fn from(error: str::Utf8Error) -> Self {
        Error::Utf8Error(error)
diff --git a/src/lib.rs b/src/lib.rs
index 6e9d252..d53db83 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2022 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

//! Provides access to a Nitrokey device using the native libnitrokey API.
@@ -116,11 +116,13 @@
//! [`WrongCrc`]: enum.CommandError.html#variant.WrongCrc

#![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)]
#![deny(unsafe_code)]

#[macro_use(lazy_static)]
extern crate lazy_static;

mod auth;
mod backend;
mod config;
mod device;
mod error;
@@ -128,10 +130,9 @@ mod otp;
mod pws;
mod util;

use std::convert::TryInto as _;
use std::ffi;
use std::fmt;
use std::marker;
use std::ptr::NonNull;
use std::sync;

pub use crate::auth::{Admin, Authenticate, User};
@@ -146,8 +147,6 @@ pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData};
pub use crate::pws::{GetPasswordSafe, PasswordSafe, PasswordSlot};
pub use crate::util::LogLevel;

use crate::util::{get_cstring, get_last_result};

/// The number of slots in a [`PasswordSafe`][].
///
/// This constant is deprecated.  Use [`PasswordSafe::get_slot_count`][] instead.
@@ -286,11 +285,8 @@ 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)
        } else {
            Err(CommunicationError::NotConnected.into())
        }
        let model = backend::connect()?;
        Ok(device::create_device_wrapper(self, model))
    }

    /// Connects to a Nitrokey device of the given model.
@@ -316,11 +312,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_model(&mut self, model: Model) -> Result<DeviceWrapper<'_>, Error> {
        if device::connect_enum(model) {
            Ok(device::create_device_wrapper(self, model))
        } else {
            Err(CommunicationError::NotConnected.into())
        }
        backend::connect_model(model)?;
        Ok(device::create_device_wrapper(self, model))
    }

    /// Connects to a Nitrokey device at the given USB path.
@@ -358,12 +351,9 @@ impl Manager {
    /// [`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())
        }
        let path = ffi::CString::new(path)?;
        let model = backend::connect_path(&path)?;
        Ok(device::create_device_wrapper(self, model))
    }

    /// Connects to a Librem Key.
@@ -388,11 +378,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_librem(&mut self) -> Result<Librem<'_>, Error> {
        if device::connect_enum(device::Model::Librem) {
            Ok(device::Librem::new(self))
        } else {
            Err(CommunicationError::NotConnected.into())
        }
        backend::connect_model(Model::Librem)?;
        Ok(device::Librem::new(self))
    }

    /// Connects to a Nitrokey Pro.
@@ -417,11 +404,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_pro(&mut self) -> Result<Pro<'_>, Error> {
        if device::connect_enum(device::Model::Pro) {
            Ok(device::Pro::new(self))
        } else {
            Err(CommunicationError::NotConnected.into())
        }
        backend::connect_model(Model::Pro)?;
        Ok(device::Pro::new(self))
    }

    /// Connects to a Nitrokey Storage.
@@ -446,11 +430,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_storage(&mut self) -> Result<Storage<'_>, Error> {
        if device::connect_enum(Model::Storage) {
            Ok(Storage::new(self))
        } else {
            Err(CommunicationError::NotConnected.into())
        }
        backend::connect_model(Model::Storage)?;
        Ok(Storage::new(self))
    }
}

@@ -556,29 +537,7 @@ pub fn force_take() -> Result<sync::MutexGuard<'static, Manager>, Error> {
/// [`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(())
    backend::list_devices()
}

/// Enables or disables debug output.  Calling this method with `true` is equivalent to setting the
@@ -590,17 +549,13 @@ fn push_device_info(
///
/// [`set_log_level`]: fn.set_log_level.html
pub fn set_debug(state: bool) {
    unsafe {
        nitrokey_sys::NK_set_debug(state);
    }
    backend::set_debug(state)
}

/// Sets the log level for libnitrokey.  All log messages are written to the standard error stream.
/// Setting the log level enables all log messages on the same or on a higher log level.
pub fn set_log_level(level: LogLevel) {
    unsafe {
        nitrokey_sys::NK_set_debug_level(level.into());
    }
    backend::set_log_level(level)
}

/// Returns the libnitrokey library version.
@@ -619,14 +574,5 @@ pub fn set_log_level(level: LogLevel) {
///
/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error
pub fn get_library_version() -> Result<Version, Error> {
    // NK_get_library_version returns a static string, so we don’t have to free the pointer.
    let git = unsafe { nitrokey_sys::NK_get_library_version() };
    let git = if git.is_null() {
        String::new()
    } else {
        util::owned_str_from_ptr(git)?
    };
    let major = unsafe { nitrokey_sys::NK_get_major_library_version() };
    let minor = unsafe { nitrokey_sys::NK_get_minor_library_version() };
    Ok(Version { git, major, minor })
    backend::get_library_version()
}
diff --git a/src/otp.rs b/src/otp.rs
index 7b47a3b..37e5c0a 100644
--- a/src/otp.rs
+++ b/src/otp.rs
@@ -1,10 +1,10 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi::CString;

use crate::backend;
use crate::error::Error;
use crate::util::{get_command_result, get_cstring, result_from_string};

/// Modes for one-time password generation.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -156,7 +156,7 @@ pub trait ConfigureOtp {

/// Provides methods to generate OTP codes and to query OTP slots on a Nitrokey
/// device.
pub trait GenerateOtp {
pub trait GenerateOtp: backend::DeviceExt {
    /// Sets the time on the Nitrokey.
    ///
    /// `time` is the number of seconds since January 1st, 1970 (Unix timestamp).  Unless `force`
@@ -191,12 +191,7 @@ pub trait GenerateOtp {
    /// [`get_totp_code`]: #method.get_totp_code
    /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp
    fn set_time(&mut self, time: u64, force: bool) -> Result<(), Error> {
        let result = if force {
            unsafe { nitrokey_sys::NK_totp_set_time(time) }
        } else {
            unsafe { nitrokey_sys::NK_totp_set_time_soft(time) }
        };
        get_command_result(result)
        self.device().set_time(time, force)
    }

    /// Returns the name of the given HOTP slot.
@@ -226,7 +221,7 @@ pub trait GenerateOtp {
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_hotp_slot_name(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_hotp_slot_name(slot) })
        self.device().get_hotp_slot_name(slot)
    }

    /// Returns the name of the given TOTP slot.
@@ -256,7 +251,7 @@ pub trait GenerateOtp {
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_totp_slot_name(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_totp_slot_name(slot) })
        self.device().get_totp_slot_name(slot)
    }

    /// Generates an HOTP code on the given slot.  This operation may require user authorization,
@@ -288,7 +283,7 @@ pub trait GenerateOtp {
    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_hotp_code(slot) })
        self.device().get_hotp_code(slot)
    }

    /// Generates a TOTP code on the given slot.  This operation may require user authorization,
@@ -332,7 +327,7 @@ pub trait GenerateOtp {
    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_totp_code(slot, 0, 0, 0) })
        self.device().get_totp_code(slot)
    }
}

@@ -403,10 +398,10 @@ impl OtpSlotData {

impl RawOtpSlotData {
    pub fn new(data: OtpSlotData) -> Result<RawOtpSlotData, Error> {
        let name = get_cstring(data.name)?;
        let secret = get_cstring(data.secret)?;
        let name = CString::new(data.name)?;
        let secret = CString::new(data.secret)?;
        let use_token_id = data.token_id.is_some();
        let token_id = get_cstring(data.token_id.unwrap_or_else(String::new))?;
        let token_id = CString::new(data.token_id.unwrap_or_else(String::new))?;

        Ok(RawOtpSlotData {
            number: data.number,
diff --git a/src/pws.rs b/src/pws.rs
index 83cb3e4..aee1c4c 100644
--- a/src/pws.rs
+++ b/src/pws.rs
@@ -1,9 +1,10 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi;

use crate::device::{Device, DeviceWrapper, Librem, Pro, Storage};
use crate::error::{CommandError, Error, LibraryError};
use crate::util::{get_command_result, get_cstring, get_last_error, result_from_string};

const SLOT_COUNT: u8 = 16;

@@ -53,7 +54,7 @@ const SLOT_COUNT: u8 = 16;
/// [`GetPasswordSafe`]: trait.GetPasswordSafe.html
#[derive(Debug)]
pub struct PasswordSafe<'a, 'b> {
    _device: &'a dyn Device<'b>,
    device: &'a dyn Device<'b>,
}

/// A slot of a [`PasswordSafe`][].
@@ -62,7 +63,7 @@ pub struct PasswordSafe<'a, 'b> {
#[derive(Clone, Copy, Debug)]
pub struct PasswordSlot<'p, 'a, 'b> {
    slot: u8,
    _pws: &'p PasswordSafe<'a, 'b>,
    pws: &'p PasswordSafe<'a, 'b>,
}

/// Provides access to a [`PasswordSafe`][].
@@ -129,9 +130,11 @@ fn get_password_safe<'a, 'b>(
    device: &'a dyn Device<'b>,
    user_pin: &str,
) -> Result<PasswordSafe<'a, 'b>, Error> {
    let user_pin_string = get_cstring(user_pin)?;
    get_command_result(unsafe { nitrokey_sys::NK_enable_password_safe(user_pin_string.as_ptr()) })
        .map(|_| PasswordSafe { _device: device })
    let user_pin = ffi::CString::new(user_pin)?;
    device
        .device()
        .enable_password_safe(&user_pin)
        .map(|_| PasswordSafe { device })
}

fn get_pws_result(s: String) -> Result<String, Error> {
@@ -178,20 +181,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// [`get_slots`]: #method.get_slots
    #[deprecated(since = "0.9.0", note = "Use get_slots() instead")]
    pub fn get_slot_status(&self) -> Result<[bool; 16], Error> {
        let status_ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_status() };
        if status_ptr.is_null() {
            return Err(get_last_error());
        }
        let status_array_ptr = status_ptr as *const [u8; SLOT_COUNT as usize];
        let status_array = unsafe { *status_array_ptr };
        let mut result = [false; SLOT_COUNT as usize];
        for i in 0..SLOT_COUNT {
            result[i as usize] = status_array[i as usize] == 1;
        }
        unsafe {
            nitrokey_sys::NK_free_password_safe_slot_status(status_ptr);
        }
        Ok(result)
        self.device.device().get_pws_slot_status()
    }

    /// Returns all password slots.
@@ -222,23 +212,15 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    ///
    /// [`PasswordSlot`]: struct.PasswordSlot.html
    pub fn get_slots(&self) -> Result<Vec<Option<PasswordSlot<'_, 'a, 'b>>>, Error> {
        let slot_status = self.device.device().get_pws_slot_status()?;
        let mut slots = Vec::new();
        let status_ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_status() };
        if status_ptr.is_null() {
            return Err(get_last_error());
        }
        let status_array_ptr = status_ptr as *const [u8; SLOT_COUNT as usize];
        let status_array = unsafe { *status_array_ptr };
        for slot in 0..SLOT_COUNT {
            if status_array[usize::from(slot)] == 1 {
                slots.push(Some(PasswordSlot { slot, _pws: self }));
            if slot_status[usize::from(slot)] {
                slots.push(Some(PasswordSlot { slot, pws: self }));
            } else {
                slots.push(None);
            }
        }
        unsafe {
            nitrokey_sys::NK_free_password_safe_slot_status(status_ptr);
        }
        Ok(slots)
    }

@@ -317,7 +299,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// [`get_slots`]: #method.get_slots
    pub fn get_slot_unchecked(&self, slot: u8) -> Result<PasswordSlot<'_, 'a, 'b>, Error> {
        if slot < self.get_slot_count() {
            Ok(PasswordSlot { slot, _pws: self })
            Ok(PasswordSlot { slot, pws: self })
        } else {
            Err(LibraryError::InvalidSlot.into())
        }
@@ -367,7 +349,9 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// [`get_slots`]: #method.get_slots
    #[deprecated(since = "0.9.0", note = "Use get_slot(slot)?.get_name() instead")]
    pub fn get_slot_name(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(slot) })
        self.device
            .device()
            .get_pws_slot_name(slot)
            .and_then(get_pws_result)
    }

@@ -411,7 +395,9 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// [`get_slots`]: #method.get_slots
    #[deprecated(since = "0.9.0", note = "Use get_slot(slot)?.get_login() instead")]
    pub fn get_slot_login(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_login(slot) })
        self.device
            .device()
            .get_pws_slot_login(slot)
            .and_then(get_pws_result)
    }

@@ -455,7 +441,9 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// [`get_slots`]: #method.get_slots
    #[deprecated(since = "0.9.0", note = "Use get_slot(slot)?.get_password() instead")]
    pub fn get_slot_password(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_password(slot) })
        self.device
            .device()
            .get_pws_slot_password(slot)
            .and_then(get_pws_result)
    }

@@ -490,17 +478,12 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
        login: &str,
        password: &str,
    ) -> Result<(), Error> {
        let name_string = get_cstring(name)?;
        let login_string = get_cstring(login)?;
        let password_string = get_cstring(password)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_write_password_safe_slot(
                slot,
                name_string.as_ptr(),
                login_string.as_ptr(),
                password_string.as_ptr(),
            )
        })
        let name = ffi::CString::new(name)?;
        let login = ffi::CString::new(login)?;
        let password = ffi::CString::new(password)?;
        self.device
            .device()
            .write_pws_slot(slot, &name, &login, &password)
    }

    /// Erases the given slot.  Erasing clears the stored name, login and password (if the slot was
@@ -530,7 +513,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    pub fn erase_slot(&mut self, slot: u8) -> Result<(), Error> {
        get_command_result(unsafe { nitrokey_sys::NK_erase_password_safe_slot(slot) })
        self.device.device().erase_pws_slot(slot)
    }
}

@@ -549,17 +532,17 @@ impl<'p, 'a, 'b> PasswordSlot<'p, 'a, 'b> {

    /// Returns the name stored in this PWS slot.
    pub fn get_name(&self) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(self.slot) })
        self.pws.device.device().get_pws_slot_name(self.slot)
    }

    /// Returns the login stored in this PWS slot.
    pub fn get_login(&self) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_login(self.slot) })
        self.pws.device.device().get_pws_slot_login(self.slot)
    }

    /// Returns the password stored in this PWS slot.
    pub fn get_password(&self) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_password(self.slot) })
        self.pws.device.device().get_pws_slot_password(self.slot)
    }
}

diff --git a/src/util.rs b/src/util.rs
index 89a8b46..2a76ac7 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,13 +1,11 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::ffi::CString;

use libc::{c_void, free};
use rand_core::{OsRng, RngCore};

use crate::error::{Error, LibraryError};
use crate::error::Error;

/// Log level for libnitrokey.
///
@@ -30,73 +28,6 @@ pub enum LogLevel {
    DebugL2,
}

pub fn str_from_ptr<'a>(ptr: *const c_char) -> Result<&'a str, Error> {
    unsafe { CStr::from_ptr(ptr) }.to_str().map_err(Error::from)
}

pub fn owned_str_from_ptr(ptr: *const c_char) -> Result<String, Error> {
    str_from_ptr(ptr).map(ToOwned::to_owned)
}

pub fn run_with_string<R, F>(ptr: *const c_char, op: F) -> Result<R, Error>
where
    F: FnOnce(&str) -> Result<R, Error>,
{
    if ptr.is_null() {
        return Err(Error::UnexpectedError(
            "libnitrokey returned a null pointer".to_owned(),
        ));
    }
    let result = str_from_ptr(ptr).and_then(op);
    unsafe { free(ptr as *mut c_void) };
    result
}

pub fn result_from_string(ptr: *const c_char) -> Result<String, Error> {
    run_with_string(ptr, |s| {
        // An empty string can both indicate an error or be a valid return value.  In this case, we
        // have to check the last command status to decide what to return.
        if s.is_empty() {
            get_last_result().map(|_| s.to_owned())
        } else {
            Ok(s.to_owned())
        }
    })
}

pub fn result_or_error<T>(value: T) -> Result<T, Error> {
    get_last_result().and(Ok(value))
}

pub fn get_struct<R, T, F>(f: F) -> Result<R, Error>
where
    R: From<T>,
    T: Default,
    F: Fn(&mut T) -> c_int,
{
    let mut out = T::default();
    get_command_result(f(&mut out))?;
    Ok(out.into())
}

pub fn get_command_result(value: c_int) -> Result<(), Error> {
    if value == 0 {
        Ok(())
    } else {
        Err(Error::from(value))
    }
}

pub fn get_last_result() -> Result<(), Error> {
    get_command_result(unsafe { nitrokey_sys::NK_get_last_command_status() }.into())
}

pub fn get_last_error() -> Error {
    get_last_result().err().unwrap_or_else(|| {
        Error::UnexpectedError("Expected an error, but command status is zero".to_owned())
    })
}

pub fn generate_password(length: usize) -> Result<CString, Error> {
    loop {
        // Randomly generate a password until we get a string *without* null bytes.  Otherwise
@@ -109,10 +40,6 @@ pub fn generate_password(length: usize) -> Result<CString, Error> {
    }
}

pub fn get_cstring<T: Into<Vec<u8>>>(s: T) -> Result<CString, Error> {
    CString::new(s).map_err(|_| LibraryError::InvalidString.into())
}

impl From<LogLevel> for i32 {
    fn from(log_level: LogLevel) -> i32 {
        match log_level {
-- 
2.20.1

[PATCH 2/5] Always connect by path

Details
Message ID
<11079fb7b2c45c71e81e2da8801a9b5cd40393db.1645695842.git.robin.krahl@ireas.org>
In-Reply-To
<cover.1645695842.git.robin.krahl@ireas.org> (view parent)
DKIM signature
missing
Download raw message
Patch: +18 -9
Currently, we have three ways to connect to devices: connecting to any
device, connecting by model and connecting by hidapi path.  This patch
changes the backend so that these connection methods are not delegated
to libnitrokey/nitrokey-sys directly.  Instead, we always use connection
by path.  This allows us to keep track of the connected device in future
patches.
---
 src/backend/mod.rs | 27 ++++++++++++++++++---------
 1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 6a2c4e2..3a402dd 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -70,23 +70,32 @@ fn push_device_info(
    Ok(())
}

pub fn connect() -> Result<device::Model, Error> {
    if unsafe { nitrokey_sys::NK_login_auto() } == 1 {
        device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
fn connect_device_info(device_info: Option<device::DeviceInfo>) -> Result<(), Error> {
    if let Some(device_info) = device_info {
        let path = ffi::CString::new(device_info.path)?;
        if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
            Ok(())
        } else {
            Err(error::CommunicationError::NotConnected.into())
        }
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
}

pub fn connect() -> Result<device::Model, Error> {
    connect_device_info(list_devices()?.pop())?;
    device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
}

pub fn connect_model(model: device::Model) -> Result<(), Error> {
    if unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } {
        Ok(())
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
    let device = list_devices()?
        .into_iter()
        .find(|info| info.model == Some(model));
    connect_device_info(device)
}

pub fn connect_path(path: &ffi::CString) -> Result<device::Model, Error> {
pub fn connect_path(path: &ffi::CStr) -> Result<device::Model, Error> {
    if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
        device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
    } else {
-- 
2.20.1

[PATCH 3/5] Use mutex for libnitrokey operations

Details
Message ID
<5db2598d5a4d82a5547224af9c1e7569b6401ae7.1645695842.git.robin.krahl@ireas.org>
In-Reply-To
<cover.1645695842.git.robin.krahl@ireas.org> (view parent)
DKIM signature
missing
Download raw message
Patch: +189 -108
This patch introduces a new Backend struct that manages and keeps track
of the current device connection and a mutex storing the global Backend
instance.  This means that all libnitrokey operations are now protected
by that mutex.  This will allow us to implement multiple device support
on top of libnitrokey by changing the connected libnitrokey device if
necessary.
---
 src/auth.rs           |  26 +++++++----
 src/backend/mod.rs    | 100 ++++++++++++++++++++++++++++++------------
 src/device/librem.rs  |  16 +++++--
 src/device/mod.rs     |  29 ++++++------
 src/device/pro.rs     |  16 +++++--
 src/device/storage.rs |  48 +++++++++++---------
 src/device/wrapper.rs |   8 +++-
 src/lib.rs            |  22 +++++-----
 src/otp.rs            |  10 ++---
 src/pws.rs            |  22 +++++-----
 10 files changed, 189 insertions(+), 108 deletions(-)

diff --git a/src/auth.rs b/src/auth.rs
index 4dddecf..b3fed6c 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -169,7 +169,11 @@ where
        Ok(password) => password,
        Err(err) => return Err((device, err.into())),
    };
    match callback(&device.device(), &password, &temp_password) {
    let backend_device = match device.device() {
        Ok(device) => device,
        Err(err) => return Err((device, err)),
    };
    match callback(&backend_device, &password, &temp_password) {
        Ok(()) => Ok(A::new(device, temp_password)),
        Err(err) => Err((device, err)),
    }
@@ -230,15 +234,19 @@ impl<'a, T: Device<'a>> ops::DerefMut for User<'a, T> {
    }
}

impl<'a, T: Device<'a>> DeviceExt for User<'a, T> {}
impl<'a, T: Device<'a>> DeviceExt for User<'a, T> {
    fn path(&self) -> &CStr {
        self.device.path()
    }
}

impl<'a, T: Device<'a>> GenerateOtp for User<'a, T> {
    fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
        DeviceExt::device(self).get_hotp_code_pin(slot, &self.temp_password)
        DeviceExt::device(self)?.get_hotp_code_pin(slot, &self.temp_password)
    }

    fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
        DeviceExt::device(self).get_totp_code_pin(slot, &self.temp_password)
        DeviceExt::device(self)?.get_totp_code_pin(slot, &self.temp_password)
    }
}

@@ -305,7 +313,7 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    pub fn write_config(&mut self, config: Config) -> Result<(), Error> {
        self.device
            .device()
            .device()?
            .write_config(config, &self.temp_password)
    }
}
@@ -313,25 +321,25 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
impl<'a, T: Device<'a>> ConfigureOtp for Admin<'a, T> {
    fn write_hotp_slot(&mut self, data: OtpSlotData, counter: u64) -> Result<(), Error> {
        self.device
            .device()
            .device()?
            .write_hotp_slot(data, counter, &self.temp_password)
    }

    fn write_totp_slot(&mut self, data: OtpSlotData, time_window: u16) -> Result<(), Error> {
        self.device
            .device()
            .device()?
            .write_totp_slot(data, time_window, &self.temp_password)
    }

    fn erase_hotp_slot(&mut self, slot: u8) -> Result<(), Error> {
        self.device
            .device()
            .device()?
            .erase_hotp_slot(slot, &self.temp_password)
    }

    fn erase_totp_slot(&mut self, slot: u8) -> Result<(), Error> {
        self.device
            .device()
            .device()?
            .erase_totp_slot(slot, &self.temp_password)
    }
}
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 3a402dd..874eb08 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -12,6 +12,7 @@ use std::convert::TryInto as _;
use std::ffi;
use std::ops;
use std::ptr;
use std::sync;

use crate::config;
use crate::device;
@@ -19,6 +20,46 @@ use crate::error::{self, Error};
use crate::otp;
use crate::Version;

lazy_static! {
    static ref BACKEND: sync::Mutex<Backend> = sync::Mutex::new(Backend::new());
}

struct Backend {
    connected_path: Option<ffi::CString>,
}

impl Backend {
    fn new() -> Self {
        Self {
            connected_path: None,
        }
    }

    pub fn is_connected(&self, path: &ffi::CStr) -> bool {
        self.connected_path.as_deref() == Some(path)
    }

    pub fn connect(&mut self, path: &ffi::CStr) -> Result<(), Error> {
        if self.is_connected(path) {
            Ok(())
        } else if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
            self.connected_path = Some(path.to_owned());
            Ok(())
        } else {
            Err(error::CommunicationError::NotConnected.into())
        }
    }

    pub fn logout(&mut self, path: &ffi::CStr) {
        if self.is_connected(path) {
            self.connected_path = None;
            unsafe {
                nitrokey_sys::NK_logout();
            }
        }
    }
}

pub fn set_debug(state: bool) {
    unsafe {
        nitrokey_sys::NK_set_debug(state);
@@ -70,47 +111,47 @@ fn push_device_info(
    Ok(())
}

fn connect_device_info(device_info: Option<device::DeviceInfo>) -> Result<(), Error> {
fn connect_device_info(
    backend: &mut Backend,
    device_info: Option<device::DeviceInfo>,
) -> Result<ffi::CString, Error> {
    if let Some(device_info) = device_info {
        let path = ffi::CString::new(device_info.path)?;
        if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
            Ok(())
        } else {
            Err(error::CommunicationError::NotConnected.into())
        }
        backend.connect(&path)?;
        Ok(path)
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
}

pub fn connect() -> Result<device::Model, Error> {
    connect_device_info(list_devices()?.pop())?;
    device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
pub fn connect() -> Result<(ffi::CString, device::Model), Error> {
    let mut backend = BACKEND.lock()?;
    let path = connect_device_info(&mut backend, list_devices()?.pop())?;
    let model = device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })?;
    Ok((path, model))
}

pub fn connect_model(model: device::Model) -> Result<(), Error> {
pub fn connect_model(model: device::Model) -> Result<ffi::CString, Error> {
    let mut backend = BACKEND.lock()?;
    let device = list_devices()?
        .into_iter()
        .find(|info| info.model == Some(model));
    connect_device_info(device)
    connect_device_info(&mut backend, device)
}

pub fn connect_path(path: &ffi::CStr) -> Result<device::Model, Error> {
    if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 {
        device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
    } else {
        Err(error::CommunicationError::NotConnected.into())
    }
    let mut backend = BACKEND.lock()?;
    backend.connect(path)?;
    device::Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() })
}

#[derive(Debug)]
pub struct Device {
    _private: (),
    _lock: sync::MutexGuard<'static, Backend>,
}

impl Device {
    fn new() -> Self {
        Self { _private: () }
    fn new(lock: sync::MutexGuard<'static, Backend>) -> Result<Self, Error> {
        Ok(Self { _lock: lock })
    }

    pub fn get_serial_number(&self) -> Result<device::SerialNumber, Error> {
@@ -172,12 +213,6 @@ impl Device {
        util::get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin.as_ptr()) })
    }

    pub fn logout(&self) {
        unsafe {
            nitrokey_sys::NK_logout();
        }
    }

    pub fn get_status(&self) -> Result<device::Status, Error> {
        util::get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })
    }
@@ -510,7 +545,16 @@ impl Device {
}

pub trait DeviceExt {
    fn device(&self) -> Device {
        Device::new()
    fn path(&self) -> &ffi::CStr;

    fn device(&self) -> Result<Device, Error> {
        let mut backend = BACKEND.lock()?;
        backend.connect(self.path())?;
        Device::new(backend)
    }

    fn logout(&self) -> Result<(), Error> {
        BACKEND.lock()?.logout(self.path());
        Ok(())
    }
}
diff --git a/src/device/librem.rs b/src/device/librem.rs
index 371f366..3731f8b 100644
--- a/src/device/librem.rs
+++ b/src/device/librem.rs
@@ -1,6 +1,8 @@
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi;

use crate::backend::DeviceExt;
use crate::device::{Device, Model, Status};
use crate::error::Error;
@@ -48,19 +50,21 @@ use crate::otp::GenerateOtp;
#[derive(Debug)]
pub struct Librem<'a> {
    manager: Option<&'a mut crate::Manager>,
    path: ffi::CString,
}

impl<'a> Librem<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager) -> Librem<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager, path: ffi::CString) -> Librem<'a> {
        Librem {
            manager: Some(manager),
            path,
        }
    }
}

impl<'a> Drop for Librem<'a> {
    fn drop(&mut self) {
        self.device().logout()
        self.logout().ok();
    }
}

@@ -74,10 +78,14 @@ impl<'a> Device<'a> for Librem<'a> {
    }

    fn get_status(&self) -> Result<Status, Error> {
        self.device().get_status()
        self.device()?.get_status()
    }
}

impl<'a> DeviceExt for Librem<'a> {}
impl<'a> DeviceExt for Librem<'a> {
    fn path(&self) -> &ffi::CStr {
        &self.path
    }
}

impl<'a> GenerateOtp for Librem<'a> {}
diff --git a/src/device/mod.rs b/src/device/mod.rs
index eea8d29..c84f881 100644
--- a/src/device/mod.rs
+++ b/src/device/mod.rs
@@ -288,7 +288,7 @@ pub trait Device<'a>:
    /// # }
    /// ```
    fn get_serial_number(&self) -> Result<SerialNumber, Error> {
        self.device().get_serial_number()
        self.device()?.get_serial_number()
    }

    /// Returns the number of remaining authentication attempts for the user.  The total number of
@@ -312,7 +312,7 @@ pub trait Device<'a>:
    /// # }
    /// ```
    fn get_user_retry_count(&self) -> Result<u8, Error> {
        self.device().get_user_retry_count()
        self.device()?.get_user_retry_count()
    }

    /// Returns the number of remaining authentication attempts for the admin.  The total number of
@@ -336,7 +336,7 @@ pub trait Device<'a>:
    /// # }
    /// ```
    fn get_admin_retry_count(&self) -> Result<u8, Error> {
        self.device().get_admin_retry_count()
        self.device()?.get_admin_retry_count()
    }

    /// Returns the firmware version.
@@ -358,7 +358,7 @@ pub trait Device<'a>:
    /// # }
    /// ```
    fn get_firmware_version(&self) -> Result<FirmwareVersion, Error> {
        self.device().get_firmware_version()
        self.device()?.get_firmware_version()
    }

    /// Returns the current configuration of the Nitrokey device.
@@ -381,7 +381,7 @@ pub trait Device<'a>:
    /// # }
    /// ```
    fn get_config(&self) -> Result<Config, Error> {
        self.device().get_config()
        self.device()?.get_config()
    }

    /// Changes the administrator PIN.
@@ -413,7 +413,7 @@ pub trait Device<'a>:
    fn change_admin_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
        let current = ffi::CString::new(current)?;
        let new = ffi::CString::new(new)?;
        self.device().change_admin_pin(&current, &new)
        self.device()?.change_admin_pin(&current, &new)
    }

    /// Changes the user PIN.
@@ -445,7 +445,7 @@ pub trait Device<'a>:
    fn change_user_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
        let current = ffi::CString::new(current)?;
        let new = ffi::CString::new(new)?;
        self.device().change_user_pin(&current, &new)
        self.device()?.change_user_pin(&current, &new)
    }

    /// Unlocks the user PIN after three failed login attempts and sets it to the given value.
@@ -477,7 +477,7 @@ pub trait Device<'a>:
    fn unlock_user_pin(&mut self, admin_pin: &str, user_pin: &str) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        let user_pin = ffi::CString::new(user_pin)?;
        self.device().unlock_user_pin(&admin_pin, &user_pin)
        self.device()?.unlock_user_pin(&admin_pin, &user_pin)
    }

    /// Locks the Nitrokey device.
@@ -502,7 +502,7 @@ pub trait Device<'a>:
    /// # }
    /// ```
    fn lock(&mut self) -> Result<(), Error> {
        self.device().lock()
        self.device()?.lock()
    }

    /// Performs a factory reset on the Nitrokey device.
@@ -539,7 +539,7 @@ pub trait Device<'a>:
    /// [`build_aes_key`]: #method.build_aes_key
    fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().factory_reset(&admin_pin)
        self.device()?.factory_reset(&admin_pin)
    }

    /// Builds a new AES key on the Nitrokey.
@@ -576,18 +576,19 @@ pub trait Device<'a>:
    /// [`factory_reset`]: #method.factory_reset
    fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().build_aes_key(&admin_pin)
        self.device()?.build_aes_key(&admin_pin)
    }
}

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

diff --git a/src/device/pro.rs b/src/device/pro.rs
index 03c81e7..8b69686 100644
--- a/src/device/pro.rs
+++ b/src/device/pro.rs
@@ -1,6 +1,8 @@
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi;

use crate::backend::DeviceExt;
use crate::device::{Device, Model, Status};
use crate::error::Error;
@@ -48,19 +50,21 @@ use crate::otp::GenerateOtp;
#[derive(Debug)]
pub struct Pro<'a> {
    manager: Option<&'a mut crate::Manager>,
    path: ffi::CString,
}

impl<'a> Pro<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager) -> Pro<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager, path: ffi::CString) -> Pro<'a> {
        Pro {
            manager: Some(manager),
            path,
        }
    }
}

impl<'a> Drop for Pro<'a> {
    fn drop(&mut self) {
        self.device().logout()
        self.logout().ok();
    }
}

@@ -74,10 +78,14 @@ impl<'a> Device<'a> for Pro<'a> {
    }

    fn get_status(&self) -> Result<Status, Error> {
        self.device().get_status()
        self.device()?.get_status()
    }
}

impl<'a> DeviceExt for Pro<'a> {}
impl<'a> DeviceExt for Pro<'a> {
    fn path(&self) -> &ffi::CStr {
        &self.path
    }
}

impl<'a> GenerateOtp for Pro<'a> {}
diff --git a/src/device/storage.rs b/src/device/storage.rs
index c1e03f3..e8558ad 100644
--- a/src/device/storage.rs
+++ b/src/device/storage.rs
@@ -52,6 +52,7 @@ use crate::otp::GenerateOtp;
#[derive(Debug)]
pub struct Storage<'a> {
    manager: Option<&'a mut crate::Manager>,
    path: ffi::CString,
}

/// The access mode of a volume on the Nitrokey Storage.
@@ -156,9 +157,10 @@ pub enum OperationStatus {
}

impl<'a> Storage<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager) -> Storage<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager, path: ffi::CString) -> Storage<'a> {
        Storage {
            manager: Some(manager),
            path,
        }
    }

@@ -194,7 +196,7 @@ impl<'a> Storage<'a> {
    pub fn change_update_pin(&mut self, current: &str, new: &str) -> Result<(), Error> {
        let current = ffi::CString::new(current)?;
        let new = ffi::CString::new(new)?;
        self.device().change_update_pin(&current, &new)
        self.device()?.change_update_pin(&current, &new)
    }

    /// Enables the firmware update mode.
@@ -229,7 +231,7 @@ impl<'a> Storage<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn enable_firmware_update(&mut self, update_pin: &str) -> Result<(), Error> {
        let update_pin = ffi::CString::new(update_pin)?;
        self.device().enable_firmware_update(&update_pin)
        self.device()?.enable_firmware_update(&update_pin)
    }

    /// Enables the encrypted storage volume.
@@ -262,7 +264,7 @@ impl<'a> Storage<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn enable_encrypted_volume(&mut self, user_pin: &str) -> Result<(), Error> {
        let user_pin = ffi::CString::new(user_pin)?;
        self.device().enable_encrypted_volume(&user_pin)
        self.device()?.enable_encrypted_volume(&user_pin)
    }

    /// Disables the encrypted storage volume.
@@ -297,7 +299,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn disable_encrypted_volume(&mut self) -> Result<(), Error> {
        self.device().disable_encrypted_volume()
        self.device()?.disable_encrypted_volume()
    }

    /// Enables a hidden storage volume.
@@ -341,7 +343,7 @@ impl<'a> Storage<'a> {
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    pub fn enable_hidden_volume(&mut self, volume_password: &str) -> Result<(), Error> {
        let volume_password = ffi::CString::new(volume_password)?;
        self.device().enable_hidden_volume(&volume_password)
        self.device()?.enable_hidden_volume(&volume_password)
    }

    /// Disables a hidden storage volume.
@@ -377,7 +379,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn disable_hidden_volume(&mut self) -> Result<(), Error> {
        self.device().disable_hidden_volume()
        self.device()?.disable_hidden_volume()
    }

    /// Creates a hidden volume.
@@ -423,7 +425,7 @@ impl<'a> Storage<'a> {
        password: &str,
    ) -> Result<(), Error> {
        let password = ffi::CString::new(password)?;
        self.device()
        self.device()?
            .create_hidden_volume(slot, start, end, &password)
    }

@@ -463,7 +465,7 @@ impl<'a> Storage<'a> {
        mode: VolumeMode,
    ) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().set_unencrypted_volume_mode(&admin_pin, mode)
        self.device()?.set_unencrypted_volume_mode(&admin_pin, mode)
    }

    /// Sets the access mode of the encrypted volume.
@@ -501,7 +503,7 @@ impl<'a> Storage<'a> {
        mode: VolumeMode,
    ) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().set_encrypted_volume_mode(&admin_pin, mode)
        self.device()?.set_encrypted_volume_mode(&admin_pin, mode)
    }

    /// Returns the status of the connected storage device.
@@ -526,7 +528,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn get_storage_status(&self) -> Result<StorageStatus, Error> {
        self.device().get_storage_status()
        self.device()?.get_storage_status()
    }

    /// Returns the production information for the connected storage device.
@@ -552,7 +554,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn get_production_info(&self) -> Result<StorageProductionInfo, Error> {
        self.device().get_production_info()
        self.device()?.get_production_info()
    }

    /// Clears the warning for a new SD card.
@@ -586,7 +588,7 @@ impl<'a> Storage<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn clear_new_sd_card_warning(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().clear_new_sd_card_warning(&admin_pin)
        self.device()?.clear_new_sd_card_warning(&admin_pin)
    }

    /// Returns a range of the SD card that has not been used to during this power cycle.
@@ -606,12 +608,12 @@ impl<'a> Storage<'a> {
    /// # Ok::<(), nitrokey::Error>(())
    /// ```
    pub fn get_sd_card_usage(&self) -> Result<ops::Range<u8>, Error> {
        self.device().get_sd_card_usage()
        self.device()?.get_sd_card_usage()
    }

    /// Blinks the red and green LED alternatively and infinitely until the device is reconnected.
    pub fn wink(&mut self) -> Result<(), Error> {
        self.device().wink()
        self.device()?.wink()
    }

    /// Returns the status of an ongoing background operation on the Nitrokey Storage.
@@ -623,7 +625,7 @@ impl<'a> Storage<'a> {
    ///
    /// [`fill_sd_card`]: #method.fill_sd_card
    pub fn get_operation_status(&self) -> Result<OperationStatus, Error> {
        self.device().get_operation_status()
        self.device()?.get_operation_status()
    }

    /// Overwrites the SD card with random data.
@@ -662,7 +664,7 @@ impl<'a> Storage<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn fill_sd_card(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().fill_sd_card(&admin_pin)
        self.device()?.fill_sd_card(&admin_pin)
    }

    /// Exports the firmware to the unencrypted volume.
@@ -683,13 +685,13 @@ impl<'a> Storage<'a> {
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn export_firmware(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin = ffi::CString::new(admin_pin)?;
        self.device().export_firmware(&admin_pin)
        self.device()?.export_firmware(&admin_pin)
    }
}

impl<'a> Drop for Storage<'a> {
    fn drop(&mut self) {
        self.device().logout()
        self.logout().ok();
    }
}

@@ -710,7 +712,7 @@ impl<'a> Device<'a> for Storage<'a> {
        // [0] https://github.com/Nitrokey/nitrokey-storage-firmware/issues/96
        // [1] https://github.com/Nitrokey/libnitrokey/issues/166

        let mut status = self.device().get_status()?;
        let mut status = self.device()?.get_status()?;

        let storage_status = self.get_storage_status()?;
        status.firmware_version = storage_status.firmware_version;
@@ -720,6 +722,10 @@ impl<'a> Device<'a> for Storage<'a> {
    }
}

impl<'a> DeviceExt for Storage<'a> {}
impl<'a> DeviceExt for Storage<'a> {
    fn path(&self) -> &ffi::CStr {
        &self.path
    }
}

impl<'a> GenerateOtp for Storage<'a> {}
diff --git a/src/device/wrapper.rs b/src/device/wrapper.rs
index f9701a5..ab4810b 100644
--- a/src/device/wrapper.rs
+++ b/src/device/wrapper.rs
@@ -1,6 +1,8 @@
// Copyright (C) 2018-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::ffi;

use crate::backend::DeviceExt;
use crate::device::{Device, Librem, Model, Pro, Status, Storage};
use crate::error::Error;
@@ -155,4 +157,8 @@ impl<'a> Device<'a> for DeviceWrapper<'a> {
    }
}

impl<'a> DeviceExt for DeviceWrapper<'a> {}
impl<'a> DeviceExt for DeviceWrapper<'a> {
    fn path(&self) -> &ffi::CStr {
        self.device().path()
    }
}
diff --git a/src/lib.rs b/src/lib.rs
index d53db83..2e64b7c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -285,8 +285,8 @@ impl Manager {
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError
    pub fn connect(&mut self) -> Result<DeviceWrapper<'_>, Error> {
        let model = backend::connect()?;
        Ok(device::create_device_wrapper(self, model))
        let (path, model) = backend::connect()?;
        Ok(device::create_device_wrapper(self, path, model))
    }

    /// Connects to a Nitrokey device of the given model.
@@ -312,8 +312,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_model(&mut self, model: Model) -> Result<DeviceWrapper<'_>, Error> {
        backend::connect_model(model)?;
        Ok(device::create_device_wrapper(self, model))
        let path = backend::connect_model(model)?;
        Ok(device::create_device_wrapper(self, path, model))
    }

    /// Connects to a Nitrokey device at the given USB path.
@@ -353,7 +353,7 @@ impl Manager {
    pub fn connect_path<S: Into<Vec<u8>>>(&mut self, path: S) -> Result<DeviceWrapper<'_>, Error> {
        let path = ffi::CString::new(path)?;
        let model = backend::connect_path(&path)?;
        Ok(device::create_device_wrapper(self, model))
        Ok(device::create_device_wrapper(self, path, model))
    }

    /// Connects to a Librem Key.
@@ -378,8 +378,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_librem(&mut self) -> Result<Librem<'_>, Error> {
        backend::connect_model(Model::Librem)?;
        Ok(device::Librem::new(self))
        let path = backend::connect_model(Model::Librem)?;
        Ok(device::Librem::new(self, path))
    }

    /// Connects to a Nitrokey Pro.
@@ -404,8 +404,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_pro(&mut self) -> Result<Pro<'_>, Error> {
        backend::connect_model(Model::Pro)?;
        Ok(device::Pro::new(self))
        let path = backend::connect_model(Model::Pro)?;
        Ok(device::Pro::new(self, path))
    }

    /// Connects to a Nitrokey Storage.
@@ -430,8 +430,8 @@ impl Manager {
    ///
    /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected
    pub fn connect_storage(&mut self) -> Result<Storage<'_>, Error> {
        backend::connect_model(Model::Storage)?;
        Ok(Storage::new(self))
        let path = backend::connect_model(Model::Storage)?;
        Ok(Storage::new(self, path))
    }
}

diff --git a/src/otp.rs b/src/otp.rs
index 37e5c0a..a3fee7b 100644
--- a/src/otp.rs
+++ b/src/otp.rs
@@ -191,7 +191,7 @@ pub trait GenerateOtp: backend::DeviceExt {
    /// [`get_totp_code`]: #method.get_totp_code
    /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp
    fn set_time(&mut self, time: u64, force: bool) -> Result<(), Error> {
        self.device().set_time(time, force)
        self.device()?.set_time(time, force)
    }

    /// Returns the name of the given HOTP slot.
@@ -221,7 +221,7 @@ pub trait GenerateOtp: backend::DeviceExt {
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_hotp_slot_name(&self, slot: u8) -> Result<String, Error> {
        self.device().get_hotp_slot_name(slot)
        self.device()?.get_hotp_slot_name(slot)
    }

    /// Returns the name of the given TOTP slot.
@@ -251,7 +251,7 @@ pub trait GenerateOtp: backend::DeviceExt {
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_totp_slot_name(&self, slot: u8) -> Result<String, Error> {
        self.device().get_totp_slot_name(slot)
        self.device()?.get_totp_slot_name(slot)
    }

    /// Generates an HOTP code on the given slot.  This operation may require user authorization,
@@ -283,7 +283,7 @@ pub trait GenerateOtp: backend::DeviceExt {
    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> {
        self.device().get_hotp_code(slot)
        self.device()?.get_hotp_code(slot)
    }

    /// Generates a TOTP code on the given slot.  This operation may require user authorization,
@@ -327,7 +327,7 @@ pub trait GenerateOtp: backend::DeviceExt {
    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    fn get_totp_code(&self, slot: u8) -> Result<String, Error> {
        self.device().get_totp_code(slot)
        self.device()?.get_totp_code(slot)
    }
}

diff --git a/src/pws.rs b/src/pws.rs
index aee1c4c..cefb882 100644
--- a/src/pws.rs
+++ b/src/pws.rs
@@ -132,7 +132,7 @@ fn get_password_safe<'a, 'b>(
) -> Result<PasswordSafe<'a, 'b>, Error> {
    let user_pin = ffi::CString::new(user_pin)?;
    device
        .device()
        .device()?
        .enable_password_safe(&user_pin)
        .map(|_| PasswordSafe { device })
}
@@ -181,7 +181,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// [`get_slots`]: #method.get_slots
    #[deprecated(since = "0.9.0", note = "Use get_slots() instead")]
    pub fn get_slot_status(&self) -> Result<[bool; 16], Error> {
        self.device.device().get_pws_slot_status()
        self.device.device()?.get_pws_slot_status()
    }

    /// Returns all password slots.
@@ -212,7 +212,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    ///
    /// [`PasswordSlot`]: struct.PasswordSlot.html
    pub fn get_slots(&self) -> Result<Vec<Option<PasswordSlot<'_, 'a, 'b>>>, Error> {
        let slot_status = self.device.device().get_pws_slot_status()?;
        let slot_status = self.device.device()?.get_pws_slot_status()?;
        let mut slots = Vec::new();
        for slot in 0..SLOT_COUNT {
            if slot_status[usize::from(slot)] {
@@ -350,7 +350,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    #[deprecated(since = "0.9.0", note = "Use get_slot(slot)?.get_name() instead")]
    pub fn get_slot_name(&self, slot: u8) -> Result<String, Error> {
        self.device
            .device()
            .device()?
            .get_pws_slot_name(slot)
            .and_then(get_pws_result)
    }
@@ -396,7 +396,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    #[deprecated(since = "0.9.0", note = "Use get_slot(slot)?.get_login() instead")]
    pub fn get_slot_login(&self, slot: u8) -> Result<String, Error> {
        self.device
            .device()
            .device()?
            .get_pws_slot_login(slot)
            .and_then(get_pws_result)
    }
@@ -442,7 +442,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    #[deprecated(since = "0.9.0", note = "Use get_slot(slot)?.get_password() instead")]
    pub fn get_slot_password(&self, slot: u8) -> Result<String, Error> {
        self.device
            .device()
            .device()?
            .get_pws_slot_password(slot)
            .and_then(get_pws_result)
    }
@@ -482,7 +482,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
        let login = ffi::CString::new(login)?;
        let password = ffi::CString::new(password)?;
        self.device
            .device()
            .device()?
            .write_pws_slot(slot, &name, &login, &password)
    }

@@ -513,7 +513,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    pub fn erase_slot(&mut self, slot: u8) -> Result<(), Error> {
        self.device.device().erase_pws_slot(slot)
        self.device.device()?.erase_pws_slot(slot)
    }
}

@@ -532,17 +532,17 @@ impl<'p, 'a, 'b> PasswordSlot<'p, 'a, 'b> {

    /// Returns the name stored in this PWS slot.
    pub fn get_name(&self) -> Result<String, Error> {
        self.pws.device.device().get_pws_slot_name(self.slot)
        self.pws.device.device()?.get_pws_slot_name(self.slot)
    }

    /// Returns the login stored in this PWS slot.
    pub fn get_login(&self) -> Result<String, Error> {
        self.pws.device.device().get_pws_slot_login(self.slot)
        self.pws.device.device()?.get_pws_slot_login(self.slot)
    }

    /// Returns the password stored in this PWS slot.
    pub fn get_password(&self) -> Result<String, Error> {
        self.pws.device.device().get_pws_slot_password(self.slot)
        self.pws.device.device()?.get_pws_slot_password(self.slot)
    }
}

-- 
2.20.1

[PATCH 4/5] Allow multiple devices per Manager instance

Details
Message ID
<e32f9a7143fb63c592f2f0b56f3cd99708652754.1645695842.git.robin.krahl@ireas.org>
In-Reply-To
<cover.1645695842.git.robin.krahl@ireas.org> (view parent)
DKIM signature
missing
Download raw message
Patch: +138 -141
Previously, connecting to a device required a mutable reference to the
Manager instance.  With this patch, we relax that requirement to a
shared reference, making it possible to connect to multiple devices at
the same time.  This is not supported by libnitrokey directly, but we
can work around that by keeping track of the USB path of the currently
connected device and re-connect if necessary.

See the Device Management RFC for more information:
	https://lists.sr.ht/~ireas/nitrokey-rs-dev/%3C20210611175806.GA1098%40ireas.org%3E
---
 CHANGELOG.md             |  3 ++-
 examples/list-devices.rs |  2 +-
 examples/otp.rs          |  2 +-
 src/auth.rs              |  6 ++---
 src/device/librem.rs     | 12 +++------
 src/device/mod.rs        | 53 +++++++++++-----------------------------
 src/device/pro.rs        | 12 +++------
 src/device/storage.rs    | 40 ++++++++++++++----------------
 src/device/wrapper.rs    | 12 ++-------
 src/lib.rs               | 51 +++++++++++++++++++++-----------------
 src/otp.rs               | 18 +++++++-------
 src/pws.rs               | 22 ++++++++---------
 tests/device.rs          | 46 +++++++++++++++++++++++++++++-----
 13 files changed, 138 insertions(+), 141 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 964d6ff..bf3fa07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,10 @@
<!---
Copyright (C) 2019-2021 Robin Krahl <robin.krahl@ireas.org>
Copyright (C) 2019-2022 Robin Krahl <robin.krahl@ireas.org>
SPDX-License-Identifier: CC0-1.0
-->

# Unreleased
- Allow connecting to multiple devices at the same time.
- Update the `nitrokey-test` dependency to 0.5.
- Bump the MSRV to 1.42.0.

diff --git a/examples/list-devices.rs b/examples/list-devices.rs
index 0066f8c..242b12e 100644
--- a/examples/list-devices.rs
+++ b/examples/list-devices.rs
@@ -6,7 +6,7 @@
use nitrokey::Device as _;

fn main() -> Result<(), nitrokey::Error> {
    let mut manager = nitrokey::take()?;
    let manager = nitrokey::take()?;
    let device_infos = nitrokey::list_devices()?;
    if device_infos.is_empty() {
        println!("No Nitrokey device found");
diff --git a/examples/otp.rs b/examples/otp.rs
index f2c6f3c..ab19973 100644
--- a/examples/otp.rs
+++ b/examples/otp.rs
@@ -9,7 +9,7 @@ use std::time;
use nitrokey::{Authenticate, ConfigureOtp, Device, GenerateOtp};

fn main() -> Result<(), nitrokey::Error> {
    let mut manager = nitrokey::take()?;
    let manager = nitrokey::take()?;
    let device = manager.connect()?;

    // Configure the OTP slot (requires admin PIN)
diff --git a/src/auth.rs b/src/auth.rs
index b3fed6c..1225079 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -41,7 +41,7 @@ pub trait Authenticate<'a>: DeviceExt {
    /// fn perform_other_task(device: &DeviceWrapper) {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let device = match device.authenticate_user("123456") {
    ///     Ok(user) => {
@@ -91,7 +91,7 @@ pub trait Authenticate<'a>: DeviceExt {
    /// fn perform_other_task(device: &DeviceWrapper) {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let device = match device.authenticate_admin("123456") {
    ///     Ok(admin) => {
@@ -296,7 +296,7 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let config = Config::new(None, None, None, false);
    /// match device.authenticate_admin("12345678") {
diff --git a/src/device/librem.rs b/src/device/librem.rs
index 3731f8b..71d72f7 100644
--- a/src/device/librem.rs
+++ b/src/device/librem.rs
@@ -26,7 +26,7 @@ use crate::otp::GenerateOtp;
/// fn perform_other_task(device: &Librem) {}
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let manager = nitrokey::take()?;
/// let device = manager.connect_librem()?;
/// let device = match device.authenticate_user("123456") {
///     Ok(user) => {
@@ -49,14 +49,14 @@ use crate::otp::GenerateOtp;
/// [`connect_librem`]: struct.Manager.html#method.connect_librem
#[derive(Debug)]
pub struct Librem<'a> {
    manager: Option<&'a mut crate::Manager>,
    _manager: &'a crate::Manager,
    path: ffi::CString,
}

impl<'a> Librem<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager, path: ffi::CString) -> Librem<'a> {
    pub(crate) fn new(manager: &'a crate::Manager, path: ffi::CString) -> Librem<'a> {
        Librem {
            manager: Some(manager),
            _manager: manager,
            path,
        }
    }
@@ -69,10 +69,6 @@ impl<'a> Drop for Librem<'a> {
}

impl<'a> Device<'a> for Librem<'a> {
    fn into_manager(mut self) -> &'a mut crate::Manager {
        self.manager.take().unwrap()
    }

    fn get_model(&self) -> Model {
        Model::Librem
    }
diff --git a/src/device/mod.rs b/src/device/mod.rs
index c84f881..1b9d92b 100644
--- a/src/device/mod.rs
+++ b/src/device/mod.rs
@@ -197,31 +197,6 @@ pub struct Status {
pub trait Device<'a>:
    Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + backend::DeviceExt + 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
@@ -231,7 +206,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// println!("Connected to a Nitrokey {}", device.get_model());
    /// #    Ok(())
@@ -253,7 +228,7 @@ pub trait Device<'a>:
    /// ```no_run
    /// use nitrokey::Device;
    ///
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let status = device.get_status()?;
    /// println!("Firmware version: {}", status.firmware_version);
@@ -278,7 +253,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// match device.get_serial_number() {
    ///     Ok(number) => println!("serial no: {}", number),
@@ -301,7 +276,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let count = device.get_user_retry_count();
    /// match device.get_user_retry_count() {
@@ -325,7 +300,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let count = device.get_admin_retry_count();
    /// match device.get_admin_retry_count() {
@@ -348,7 +323,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// match device.get_firmware_version() {
    ///     Ok(version) => println!("Firmware version: {}", version),
@@ -370,7 +345,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let config = device.get_config()?;
    /// println!("Num Lock binding:          {:?}", config.num_lock);
@@ -398,7 +373,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.change_admin_pin("12345678", "12345679") {
    ///     Ok(()) => println!("Updated admin PIN."),
@@ -430,7 +405,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.change_user_pin("123456", "123457") {
    ///     Ok(()) => println!("Updated admin PIN."),
@@ -462,7 +437,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.unlock_user_pin("12345678", "123456") {
    ///     Ok(()) => println!("Unlocked user PIN."),
@@ -492,7 +467,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.lock() {
    ///     Ok(()) => println!("Locked the Nitrokey device."),
@@ -524,7 +499,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.factory_reset("12345678") {
    ///     Ok(()) => println!("Performed a factory reset."),
@@ -561,7 +536,7 @@ pub trait Device<'a>:
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.build_aes_key("12345678") {
    ///     Ok(()) => println!("New AES keys have been built."),
@@ -581,7 +556,7 @@ pub trait Device<'a>:
}

pub(crate) fn create_device_wrapper(
    manager: &mut crate::Manager,
    manager: &crate::Manager,
    path: ffi::CString,
    model: Model,
) -> DeviceWrapper<'_> {
diff --git a/src/device/pro.rs b/src/device/pro.rs
index 8b69686..a42bda6 100644
--- a/src/device/pro.rs
+++ b/src/device/pro.rs
@@ -26,7 +26,7 @@ use crate::otp::GenerateOtp;
/// fn perform_other_task(device: &Pro) {}
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let manager = nitrokey::take()?;
/// let device = manager.connect_pro()?;
/// let device = match device.authenticate_user("123456") {
///     Ok(user) => {
@@ -49,14 +49,14 @@ use crate::otp::GenerateOtp;
/// [`connect_pro`]: struct.Manager.html#method.connect_pro
#[derive(Debug)]
pub struct Pro<'a> {
    manager: Option<&'a mut crate::Manager>,
    _manager: &'a crate::Manager,
    path: ffi::CString,
}

impl<'a> Pro<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager, path: ffi::CString) -> Pro<'a> {
    pub(crate) fn new(manager: &'a crate::Manager, path: ffi::CString) -> Pro<'a> {
        Pro {
            manager: Some(manager),
            _manager: manager,
            path,
        }
    }
@@ -69,10 +69,6 @@ impl<'a> Drop for Pro<'a> {
}

impl<'a> Device<'a> for Pro<'a> {
    fn into_manager(mut self) -> &'a mut crate::Manager {
        self.manager.take().unwrap()
    }

    fn get_model(&self) -> Model {
        Model::Pro
    }
diff --git a/src/device/storage.rs b/src/device/storage.rs
index e8558ad..8ebfa80 100644
--- a/src/device/storage.rs
+++ b/src/device/storage.rs
@@ -28,7 +28,7 @@ use crate::otp::GenerateOtp;
/// fn perform_other_task(device: &Storage) {}
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let manager = nitrokey::take()?;
/// let device = manager.connect_storage()?;
/// let device = match device.authenticate_user("123456") {
///     Ok(user) => {
@@ -51,7 +51,7 @@ use crate::otp::GenerateOtp;
/// [`connect_storage`]: struct.Manager.html#method.connect_storage
#[derive(Debug)]
pub struct Storage<'a> {
    manager: Option<&'a mut crate::Manager>,
    _manager: &'a crate::Manager,
    path: ffi::CString,
}

@@ -157,9 +157,9 @@ pub enum OperationStatus {
}

impl<'a> Storage<'a> {
    pub(crate) fn new(manager: &'a mut crate::Manager, path: ffi::CString) -> Storage<'a> {
    pub(crate) fn new(manager: &'a crate::Manager, path: ffi::CString) -> Storage<'a> {
        Storage {
            manager: Some(manager),
            _manager: manager,
            path,
        }
    }
@@ -181,7 +181,7 @@ impl<'a> Storage<'a> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.change_update_pin("12345678", "87654321") {
    ///     Ok(()) => println!("Updated update PIN."),
@@ -217,7 +217,7 @@ impl<'a> Storage<'a> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.enable_firmware_update("12345678") {
    ///     Ok(()) => println!("Nitrokey entered update mode."),
@@ -250,7 +250,7 @@ impl<'a> Storage<'a> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.enable_encrypted_volume("123456") {
    ///     Ok(()) => println!("Enabled the encrypted volume."),
@@ -280,7 +280,7 @@ impl<'a> Storage<'a> {
    /// fn use_volume() {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.enable_encrypted_volume("123456") {
    ///     Ok(()) => {
@@ -327,7 +327,7 @@ impl<'a> Storage<'a> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// device.enable_encrypted_volume("123445")?;
    /// match device.enable_hidden_volume("hidden-pw") {
@@ -359,7 +359,7 @@ impl<'a> Storage<'a> {
    /// fn use_volume() {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// device.enable_encrypted_volume("123445")?;
    /// match device.enable_hidden_volume("hidden-pw") {
@@ -407,7 +407,7 @@ impl<'a> Storage<'a> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// device.enable_encrypted_volume("123445")?;
    /// device.create_hidden_volume(0, 0, 100, "hidden-pw")?;
@@ -447,7 +447,7 @@ impl<'a> Storage<'a> {
    /// use nitrokey::VolumeMode;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.set_unencrypted_volume_mode("12345678", VolumeMode::ReadWrite) {
    ///     Ok(()) => println!("Set the unencrypted volume to read-write mode."),
@@ -485,7 +485,7 @@ impl<'a> Storage<'a> {
    /// use nitrokey::VolumeMode;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.set_encrypted_volume_mode("12345678", VolumeMode::ReadWrite) {
    ///     Ok(()) => println!("Set the encrypted volume to read-write mode."),
@@ -516,7 +516,7 @@ impl<'a> Storage<'a> {
    /// fn use_volume() {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect_storage()?;
    /// match device.get_storage_status() {
    ///     Ok(status) => {
@@ -541,7 +541,7 @@ impl<'a> Storage<'a> {
    /// fn use_volume() {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let device = manager.connect_storage()?;
    /// match device.get_production_info() {
    ///     Ok(data) => {
@@ -574,7 +574,7 @@ impl<'a> Storage<'a> {
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut device = manager.connect_storage()?;
    /// match device.clear_new_sd_card_warning("12345678") {
    ///     Ok(()) => println!("Cleared the new SD card warning."),
@@ -601,7 +601,7 @@ impl<'a> Storage<'a> {
    /// # Example
    ///
    /// ```no_run
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let storage = manager.connect_storage()?;
    /// let usage = storage.get_sd_card_usage()?;
    /// println!("SD card usage: {}..{}", usage.start, usage.end);
@@ -644,7 +644,7 @@ impl<'a> Storage<'a> {
    /// ```no_run
    /// use nitrokey::OperationStatus;
    ///
    /// let mut manager = nitrokey::take()?;
    /// let manager = nitrokey::take()?;
    /// let mut storage = manager.connect_storage()?;
    /// storage.fill_sd_card("12345678")?;
    /// loop {
@@ -696,10 +696,6 @@ impl<'a> Drop for Storage<'a> {
}

impl<'a> Device<'a> for Storage<'a> {
    fn into_manager(mut self) -> &'a mut crate::Manager {
        self.manager.take().unwrap()
    }

    fn get_model(&self) -> Model {
        Model::Storage
    }
diff --git a/src/device/wrapper.rs b/src/device/wrapper.rs
index ab4810b..3cf27a8 100644
--- a/src/device/wrapper.rs
+++ b/src/device/wrapper.rs
@@ -27,7 +27,7 @@ use crate::otp::GenerateOtp;
/// fn perform_other_task(device: &DeviceWrapper) {}
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let manager = nitrokey::take()?;
/// let device = manager.connect()?;
/// let device = match device.authenticate_user("123456") {
///     Ok(user) => {
@@ -54,7 +54,7 @@ use crate::otp::GenerateOtp;
/// fn perform_storage_task(device: &Storage) {}
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let manager = nitrokey::take()?;
/// let device = manager.connect()?;
/// perform_common_task(&device);
/// match device {
@@ -132,14 +132,6 @@ impl<'a> GenerateOtp for DeviceWrapper<'a> {
}

impl<'a> Device<'a> for DeviceWrapper<'a> {
    fn into_manager(self) -> &'a mut crate::Manager {
        match self {
            DeviceWrapper::Librem(dev) => dev.into_manager(),
            DeviceWrapper::Pro(dev) => dev.into_manager(),
            DeviceWrapper::Storage(dev) => dev.into_manager(),
        }
    }

    fn get_model(&self) -> Model {
        match *self {
            DeviceWrapper::Librem(_) => Model::Librem,
diff --git a/src/lib.rs b/src/lib.rs
index 2e64b7c..b558441 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,12 +9,12 @@
//! performed without authentication, some require user access, and some require admin access.
//! This is modelled using the types [`User`][] and [`Admin`][].