~ireas/nitrokey-rs

283d2105cb14517c39c9a96b441b5633e3ee7209 — Robin Krahl a month ago 21de2b9 + c070c26 master
Merge branch 'release-v0.8.0'
M .builds/archlinux-msrv.yml => .builds/archlinux-msrv.yml +7 -3
@@ 1,4 1,4 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
# Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:


@@ 7,11 7,11 @@ packages:
environment:
  USE_SYSTEM_LIBNITROKEY: "1"
sources:
  - https://git.ireas.org/nitrokey-rs
  - https://git.sr.ht/~ireas/nitrokey-rs
tasks:
  - setup: |
      rustup set profile minimal
      rustup default 1.34.2
      rustup default 1.40.0
  - version: |
      rustc -V
  - build: |


@@ 20,3 20,7 @@ tasks:
  - test: |
      cd nitrokey-rs
      cargo test
triggers:
  - action: email
    condition: failure
    to: robin.krahl@ireas.org

M .builds/archlinux-use-system-lib.yml => .builds/archlinux-use-system-lib.yml +6 -2
@@ 1,4 1,4 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
# Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:


@@ 7,7 7,7 @@ packages:
environment:
  USE_SYSTEM_LIBNITROKEY: "1"
sources:
  - https://git.ireas.org/nitrokey-rs
  - https://git.sr.ht/~ireas/nitrokey-rs
tasks:
  - version: |
      rustc -V


@@ 23,3 23,7 @@ tasks:
  - clippy: |
      cd nitrokey-rs
      cargo clippy -- -D warnings
triggers:
  - action: email
    condition: failure
    to: robin.krahl@ireas.org

M .builds/archlinux.yml => .builds/archlinux.yml +6 -2
@@ 1,4 1,4 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
# Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:


@@ 6,7 6,7 @@ packages:
  - hidapi
  - gcc
sources:
  - https://git.ireas.org/nitrokey-rs
  - https://git.sr.ht/~ireas/nitrokey-rs
tasks:
  - build: |
      cd nitrokey-rs


@@ 14,3 14,7 @@ tasks:
  - test: |
      cd nitrokey-rs
      cargo test
triggers:
  - action: email
    condition: failure
    to: robin.krahl@ireas.org

M .builds/lint.yml => .builds/lint.yml +7 -3
@@ 1,16 1,20 @@
# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
# Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
# SPDX-License-Identifier: CC0-1.0
image: archlinux
packages:
  - gnupg
  - reuse
sources:
  - https://git.ireas.org/nitrokey-rs
  - https://git.sr.ht/~ireas/nitrokey-rs
tasks:
  - verify: |
      cd nitrokey-rs
      curl -s "https://pgp.ireas.org/0x6D533958F070C57C.txt" | gpg --import
      git verify-commit HEAD
      git verify-commit HEAD || [ `git config user.email` == "builds@sr.ht" ]
  - reuse: |
      cd nitrokey-rs
      reuse lint
triggers:
  - action: email
    condition: failure
    to: robin.krahl@ireas.org

M CHANGELOG.md => CHANGELOG.md +23 -0
@@ 3,6 3,29 @@ Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
SPDX-License-Identifier: CC0-1.0
-->

# v0.8.0 (2020-09-29)
- Export the `FirmwareVersion` struct.
- Mark the `Error`, `CommandError`, `CommunicationError`, `LibraryError`,
  `Model` and `DeviceWrapper` enums as non-exhaustive.
  - Bump the MSRV to 1.40.0.
- Rename the `numlock`, `capslock`, `scrollock` fields of the `Config` struct
  to `num_lock`, `caps_lock`, `scroll_lock`.
- Update the `nitrokey-sys` dependency to v3.6.0.
  - Use `NK_device_serial_number_as_u32` instead of `NK_device_serial_number`
    in `Device::get_serial_number`.
  - Use `NK_free_password_safe_slot_status` to free the pointer returned by
    `NK_get_password_safe_slot_status` in `PasswordSafe::get_slot_status`.
  - Use the `NK_config` struct for configuration handling.
  - Use the derived `Default` implementation for the structs defined by
    `nitrokey-sys`.
- Add `get_struct` utility function for querying structs with libnitrokey
  functions that use an output parameter.
- Support the Librem Key model.
  - Add the `Librem` variant to the `Model` and `DeviceWrapper` enums.
  - Add the `Librem` struct.
  - Change the `Display` implementation for the `Model` enum to print the
    complete model name, i. e. Nitrokey Pro, Nitrokey Storage or Librem Key.

# v0.7.1 (2020-08-30)
- Remove the custom `std::error::Error::source` implementation for
  `error::Error` to avoid duplicate error messages.

M Cargo.toml => Cargo.toml +3 -3
@@ 3,11 3,11 @@

[package]
name = "nitrokey"
version = "0.7.1"
version = "0.8.0"
authors = ["Robin Krahl <robin.krahl@ireas.org>"]
edition = "2018"
homepage = "https://code.ireas.org/nitrokey-rs/"
repository = "https://git.ireas.org/nitrokey-rs/"
repository = "https://git.sr.ht/~ireas/nitrokey-rs"
documentation = "https://docs.rs/nitrokey"
description = "Bindings to libnitrokey for communication with Nitrokey devices"
keywords = ["nitrokey", "otp"]


@@ 19,7 19,7 @@ exclude = [".builds/*"]
[dependencies]
lazy_static = "1.2"
libc = "0.2"
nitrokey-sys = "3.5"
nitrokey-sys = "3.6"
rand_core = {version = "0.5.1", features = ["getrandom"] }

[dev-dependencies]

M README.md => README.md +23 -12
@@ 1,5 1,5 @@
<!---
Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org>
Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
SPDX-License-Identifier: CC0-1.0
-->



@@ 46,12 46,11 @@ supported by `nitrokey-rs`:
- `NK_is_AES_supported`.  This function is no longer needed for Nitrokey
  devices with a recent firmware version.
- `NK_send_startup`.  Currently, this function is redundant to `NK_get_time`.
- `NK_set_unencrypted_volume_rorw_pin_type_user`,
  `NK_set_unencrypted_read_only`, `NK_set_unencrypted_read_write`.  These
  functions are only relevant for older firmware versions (pre-v0.51).  As the
  Nitrokey Storage firmware can be updated easily, we do not support these
  outdated versions.
- `NK_totp_get_time`, `NK_status`.  These functions are deprecated.
- `NK_set_unencrypted_volume_rorw_pin_type_user`: This function is only
  relevant for older firmware versions (pre-v0.51).  As the Nitrokey Storage
  firmware can be updated easily, we do not support these outdated versions.
- `NK_totp_get_time`, `NK_status`, `NK_set_unencrypted_read_only`,
  `NK_set_unencrypted_read_write`.  These functions are deprecated.
- `NK_read_HOTP_slot`.  This function is only available for HOTP slots, not for
  TOTP.  We will support it once both types are supported by `libnitrokey`.
- All `*_as_string` functions that return string representations of data


@@ 77,20 76,30 @@ The test suite contains some test that take very long to execute, for example
filling the SD card of a Nitrokey Storage with random data.  These tests are
ignored per default.  Use `cargo test -- --ignored` to execute the tests.

Currently, there are no tests specifically for the Librem Key as we first have
to release a new `nitrokey-rs` version that `nitrokey-test` can use.

## Contributing

Contributions to this project are welcome!  Please submit patches to the
mailing list [~ireas/nitrokey-rs-dev@lists.sr.ht][] ([archive][]) using the
`[PATCH nitrokey-rs]` subject prefix.  For more information, see the
[Contributing Guide][].

## Acknowledgments

Thanks to Nitrokey UG for providing two Nitrokey devices to support the
Thanks to Nitrokey GmbH for providing two Nitrokey devices to support the
development of this crate.  Thanks to Daniel Mueller for contributions to
`nitrokey-rs` and for the `nitrokey-test` crate.

## Minimum Supported Rust Version

This crate supports Rust 1.34.2 or later.
This crate supports Rust 1.40.0 or later.

## Contact

For bug reports, patches, feature requests or other messages, please send a
mail to [nitrokey-rs-dev@ireas.org][].
mail to [~ireas/nitrokey-rs-dev@lists.sr.ht][] ([archive][]).

## License



@@ 103,11 112,13 @@ in the `LICENSES` directory.  `libnitrokey` is licensed under the [LGPL-3.0][].

[API reference]: https://docs.rs/nitrokey
[examples]: https://git.ireas.org/nitrokey-rs/tree/examples
[`nitrocli`]: https://github.com/d-e-s-o/nitrocli/tree/master/nitrocli
[`nitrocli`]: https://github.com/d-e-s-o/nitrocli
[Nitrokey udev rules]: https://www.nitrokey.com/documentation/frequently-asked-questions-faq#openpgp-card-not-available
[`libnitrokey`]: https://github.com/nitrokey/libnitrokey
[`nitrokey-test`]: https://github.com/d-e-s-o/nitrokey-test
[nitrokey-rs-dev@ireas.org]: mailto:nitrokey-rs-dev@ireas.org
[~ireas/nitrokey-rs-dev@lists.sr.ht]: mailto:~ireas/nitrokey-rs-dev@lists.sr.ht
[archive]: https://lists.sr.ht/~ireas/nitrokey-rs-dev
[Contributing Guide]: https://man.sr.ht/~ireas/guides/contributing.md
[MIT]: https://opensource.org/licenses/MIT
[CC0]: https://creativecommons.org/publicdomain/zero/1.0/
[LGPL-3.0]: https://opensource.org/licenses/lgpl-3.0.html

M src/auth.rs => src/auth.rs +27 -14
@@ 1,15 1,15 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::convert::TryFrom as _;
use std::convert::TryInto as _;
use std::ffi::CString;
use std::marker;
use std::ops;
use std::os::raw::c_char;
use std::os::raw::c_int;

use crate::config::{Config, RawConfig};
use crate::device::{Device, DeviceWrapper, Pro, Storage};
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};


@@ 126,7 126,7 @@ trait AuthenticatedDevice<T> {
/// method.
///
/// [`Authenticate`]: trait.Authenticate.html
/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin
/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
/// [`device`]: #method.device
#[derive(Debug)]
pub struct User<'a, T: Device<'a>> {


@@ 278,7 278,8 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
    ///
    /// # Errors
    ///
    /// - [`InvalidSlot`][] if the provided numlock, capslock or scrolllock slot is larger than two
    /// - [`InvalidSlot`][] if the provided Num Lock, Caps Lock or Scroll Lock slot is larger than
    ///   two
    ///
    /// # Example
    ///


@@ 303,16 304,8 @@ impl<'a, T: Device<'a>> Admin<'a, T> {
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    pub fn write_config(&mut self, config: Config) -> Result<(), Error> {
        let raw_config = RawConfig::try_from(config)?;
        get_command_result(unsafe {
            nitrokey_sys::NK_write_config(
                raw_config.numlock,
                raw_config.capslock,
                raw_config.scrollock,
                raw_config.user_password,
                false,
                self.temp_password.as_ptr(),
            )
            nitrokey_sys::NK_write_config_struct(config.try_into()?, self.temp_password.as_ptr())
        })
    }
}


@@ 378,6 371,9 @@ impl<'a, T: Device<'a>> AuthenticatedDevice<T> for Admin<'a, T> {
impl<'a> Authenticate<'a> for DeviceWrapper<'a> {
    fn authenticate_user(self, password: &str) -> Result<User<'a, Self>, (Self, Error)> {
        match self {
            DeviceWrapper::Librem(librem) => {
                authenticate_user_wrapper(librem, DeviceWrapper::Librem, password)
            }
            DeviceWrapper::Storage(storage) => {
                authenticate_user_wrapper(storage, DeviceWrapper::Storage, password)
            }


@@ 387,6 383,9 @@ impl<'a> Authenticate<'a> for DeviceWrapper<'a> {

    fn authenticate_admin(self, password: &str) -> Result<Admin<'a, Self>, (Self, Error)> {
        match self {
            DeviceWrapper::Librem(librem) => {
                authenticate_admin_wrapper(librem, DeviceWrapper::Librem, password)
            }
            DeviceWrapper::Storage(storage) => {
                authenticate_admin_wrapper(storage, DeviceWrapper::Storage, password)
            }


@@ 397,6 396,20 @@ 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)
        })
    }

    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> {
    fn authenticate_user(self, password: &str) -> Result<User<'a, Self>, (Self, Error)> {
        authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {

M src/config.rs => src/config.rs +47 -45
@@ 8,15 8,15 @@ use crate::error::{Error, LibraryError};
/// The configuration for a Nitrokey.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Config {
    /// If set, the stick will generate a code from the HOTP slot with the given number if numlock
    /// If set, the stick will generate a code from the HOTP slot with the given number if Num Lock
    /// is pressed.  The slot number must be 0, 1 or 2.
    pub numlock: Option<u8>,
    /// If set, the stick will generate a code from the HOTP slot with the given number if capslock
    /// is pressed.  The slot number must be 0, 1 or 2.
    pub capslock: Option<u8>,
    /// If set, the stick will generate a code from the HOTP slot with the given number if
    /// scrollock is pressed.  The slot number must be 0, 1 or 2.
    pub scrollock: Option<u8>,
    pub num_lock: Option<u8>,
    /// If set, the stick will generate a code from the HOTP slot with the given number if Caps
    /// Lock is pressed.  The slot number must be 0, 1 or 2.
    pub caps_lock: Option<u8>,
    /// If set, the stick will generate a code from the HOTP slot with the given number if Scroll
    /// Lock is pressed.  The slot number must be 0, 1 or 2.
    pub scroll_lock: Option<u8>,
    /// If set, OTP generation using [`get_hotp_code`][] or [`get_totp_code`][] requires user
    /// authentication.  Otherwise, OTPs can be generated without authentication.
    ///


@@ 25,14 25,6 @@ pub struct Config {
    pub user_password: bool,
}

#[derive(Debug)]
pub struct RawConfig {
    pub numlock: u8,
    pub capslock: u8,
    pub scrollock: u8,
    pub user_password: bool,
}

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


@@ 56,51 48,61 @@ fn option_to_config_otp_slot(value: Option<u8>) -> Result<u8, Error> {
impl Config {
    /// Constructs a new instance of this struct.
    pub fn new(
        numlock: Option<u8>,
        capslock: Option<u8>,
        scrollock: Option<u8>,
        num_lock: Option<u8>,
        caps_lock: Option<u8>,
        scroll_lock: Option<u8>,
        user_password: bool,
    ) -> Config {
        Config {
            numlock,
            capslock,
            scrollock,
            num_lock,
            caps_lock,
            scroll_lock,
            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 RawConfig {
impl convert::TryFrom<Config> for nitrokey_sys::NK_config {
    type Error = Error;

    fn try_from(config: Config) -> Result<RawConfig, Error> {
        Ok(RawConfig {
            numlock: option_to_config_otp_slot(config.numlock)?,
            capslock: option_to_config_otp_slot(config.capslock)?,
            scrollock: option_to_config_otp_slot(config.scrollock)?,
            user_password: config.user_password,
    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_status> for RawConfig {
    fn from(status: &nitrokey_sys::NK_status) -> Self {
        Self {
            numlock: status.config_numlock,
            capslock: status.config_capslock,
            scrollock: status.config_scrolllock,
            user_password: status.otp_user_password,
        }
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 Into<Config> for RawConfig {
    fn into(self) -> Config {
        Config {
            numlock: config_otp_slot_to_option(self.numlock),
            capslock: config_otp_slot_to_option(self.capslock),
            scrollock: config_otp_slot_to_option(self.scrollock),
            user_password: self.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,
        )
    }
}

A src/device/librem.rs => src/device/librem.rs +83 -0
@@ 0,0 1,83 @@
// Copyright (C) 2018-2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

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.
///
/// Use the [`connect`][] method to obtain an instance wrapper or the [`connect_librem`] method to
/// directly obtain an instance.  If you want to execute a command that requires user or admin
/// authentication, use [`authenticate_admin`][] or [`authenticate_user`][].
///
/// # Examples
///
/// Authentication with error handling:
///
/// ```no_run
/// use nitrokey::{Authenticate, User, Librem};
/// # use nitrokey::Error;
///
/// fn perform_user_task<'a>(device: &User<'a, Librem<'a>>) {}
/// fn perform_other_task(device: &Librem) {}
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let device = manager.connect_librem()?;
/// let device = match device.authenticate_user("123456") {
///     Ok(user) => {
///         perform_user_task(&user);
///         user.device()
///     },
///     Err((device, err)) => {
///         eprintln!("Could not authenticate as user: {}", err);
///         device
///     },
/// };
/// perform_other_task(&device);
/// #     Ok(())
/// # }
/// ```
///
/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin
/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
/// [`connect`]: struct.Manager.html#method.connect
/// [`connect_librem`]: struct.Manager.html#method.connect_librem
#[derive(Debug)]
pub struct Librem<'a> {
    manager: Option<&'a mut crate::Manager>,
}

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

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

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
    }

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

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

M src/device/mod.rs => src/device/mod.rs +28 -24
@@ 1,6 1,7 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

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


@@ 11,14 12,15 @@ use std::fmt;
use std::str;

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

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


@@ 28,7 30,10 @@ pub use wrapper::DeviceWrapper;

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


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


@@ 47,6 53,7 @@ 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,
        }


@@ 61,6 68,7 @@ impl TryFrom<nitrokey_sys::NK_device_model> for 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),


@@ 194,8 202,8 @@ impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo {
impl fmt::Display for DeviceInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.model {
            Some(model) => write!(f, "Nitrokey {}", model)?,
            None => write!(f, "Unsupported Nitrokey model")?,
            Some(model) => f.write_str(&model.to_string())?,
            None => f.write_str("Unsupported Nitrokey model")?,
        }
        write!(f, " at {} with ", self.path)?;
        match self.serial_number {


@@ 273,7 281,7 @@ impl From<nitrokey_sys::NK_status> for Status {
                minor: status.firmware_version_minor,
            },
            serial_number: SerialNumber::new(status.serial_number_smart_card),
            config: RawConfig::from(&status).into(),
            config: Config::from(&status),
        }
    }
}


@@ 304,6 312,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// }
    /// # Ok::<(), nitrokey::Error>(())
    /// ```
    ///
    /// [`Manager`]: struct.Manager.html
    fn into_manager(self) -> &'a mut crate::Manager;

    /// Returns the model of the connected Nitrokey device.


@@ 372,9 382,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    fn get_serial_number(&self) -> Result<SerialNumber, Error> {
        run_with_string(unsafe { nitrokey_sys::NK_device_serial_number() }, |s| {
            s.parse()
        })
        let serial_number = unsafe { nitrokey_sys::NK_device_serial_number_as_u32() };
        result_or_error(SerialNumber::new(serial_number))
    }

    /// Returns the number of remaining authentication attempts for the user.  The total number of


@@ 461,25 470,15 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// let mut manager = nitrokey::take()?;
    /// let device = manager.connect()?;
    /// let config = device.get_config()?;
    /// println!("numlock binding:          {:?}", config.numlock);
    /// println!("capslock binding:         {:?}", config.capslock);
    /// println!("scrollock binding:        {:?}", config.scrollock);
    /// println!("Num Lock binding:          {:?}", config.num_lock);
    /// println!("Caps Lock binding:         {:?}", config.caps_lock);
    /// println!("Scroll Lock binding:       {:?}", config.scroll_lock);
    /// println!("require password for OTP: {:?}", config.user_password);
    /// #     Ok(())
    /// # }
    /// ```
    fn get_config(&self) -> Result<Config, Error> {
        let mut raw_status = nitrokey_sys::NK_status {
            firmware_version_major: 0,
            firmware_version_minor: 0,
            serial_number_smart_card: 0,
            config_numlock: 0,
            config_capslock: 0,
            config_scrolllock: 0,
            otp_user_password: false,
        };
        get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?;
        Ok(RawConfig::from(&raw_status).into())
        get_struct(|out| unsafe { nitrokey_sys::NK_read_config_struct(out) })
    }

    /// Changes the administrator PIN.


@@ 641,6 640,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    /// [`build_aes_key`]: #method.build_aes_key
    fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin_string = get_cstring(admin_pin)?;


@@ 676,6 677,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    /// [`factory_reset`]: #method.factory_reset
    fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> {
        let admin_pin_string = get_cstring(admin_pin)?;


@@ 692,6 695,7 @@ pub(crate) fn create_device_wrapper(
    model: Model,
) -> DeviceWrapper<'_> {
    match model {
        Model::Librem => Librem::new(manager).into(),
        Model::Pro => Pro::new(manager).into(),
        Model::Storage => Storage::new(manager).into(),
    }

M src/device/pro.rs => src/device/pro.rs +2 -12
@@ 4,7 4,7 @@
use crate::device::{Device, Model, Status};
use crate::error::Error;
use crate::otp::GenerateOtp;
use crate::util::get_command_result;
use crate::util::get_struct;

/// A Nitrokey Pro device without user or admin authentication.
///


@@ 76,17 76,7 @@ impl<'a> Device<'a> for Pro<'a> {
    }

    fn get_status(&self) -> Result<Status, Error> {
        let mut raw_status = nitrokey_sys::NK_status {
            firmware_version_major: 0,
            firmware_version_minor: 0,
            serial_number_smart_card: 0,
            config_numlock: 0,
            config_capslock: 0,
            config_scrolllock: 0,
            otp_user_password: false,
        };
        get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?;
        Ok(raw_status.into())
        get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })
    }
}


M src/device/storage.rs => src/device/storage.rs +5 -53
@@ 8,7 8,7 @@ use std::ops;
use crate::device::{Device, FirmwareVersion, Model, SerialNumber, Status};
use crate::error::{CommandError, Error};
use crate::otp::GenerateOtp;
use crate::util::{get_command_result, get_cstring, get_last_error};
use crate::util::{get_command_result, get_cstring, get_last_error, get_struct};

/// A Nitrokey Storage device without user or admin authentication.
///


@@ 549,26 549,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn get_storage_status(&self) -> Result<StorageStatus, Error> {
        let mut raw_status = nitrokey_sys::NK_storage_status {
            unencrypted_volume_read_only: false,
            unencrypted_volume_active: false,
            encrypted_volume_read_only: false,
            encrypted_volume_active: false,
            hidden_volume_read_only: false,
            hidden_volume_active: false,
            firmware_version_major: 0,
            firmware_version_minor: 0,
            firmware_locked: false,
            serial_number_sd_card: 0,
            serial_number_smart_card: 0,
            user_retry_count: 0,
            admin_retry_count: 0,
            new_sd_card_found: false,
            filled_with_random: false,
            stick_initialized: false,
        };
        let raw_result = unsafe { nitrokey_sys::NK_get_status_storage(&mut raw_status) };
        get_command_result(raw_result).map(|_| StorageStatus::from(raw_status))
        get_struct(|out| unsafe { nitrokey_sys::NK_get_status_storage(out) })
    }

    /// Returns the production information for the connected storage device.


@@ 594,23 575,7 @@ impl<'a> Storage<'a> {
    /// # }
    /// ```
    pub fn get_production_info(&self) -> Result<StorageProductionInfo, Error> {
        let mut raw_data = nitrokey_sys::NK_storage_ProductionTest {
            FirmwareVersion_au8: [0, 2],
            FirmwareVersionInternal_u8: 0,
            SD_Card_Size_u8: 0,
            CPU_CardID_u32: 0,
            SmartCardID_u32: 0,
            SD_CardID_u32: 0,
            SC_UserPwRetryCount: 0,
            SC_AdminPwRetryCount: 0,
            SD_Card_ManufacturingYear_u8: 0,
            SD_Card_ManufacturingMonth_u8: 0,
            SD_Card_OEM_u16: 0,
            SD_WriteSpeed_u16: 0,
            SD_Card_Manufacturer_u8: 0,
        };
        let raw_result = unsafe { nitrokey_sys::NK_get_storage_production_info(&mut raw_data) };
        get_command_result(raw_result).map(|_| StorageProductionInfo::from(raw_data))
        get_struct(|out| unsafe { nitrokey_sys::NK_get_storage_production_info(out) })
    }

    /// Clears the warning for a new SD card.


@@ 666,10 631,7 @@ 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 {
            write_level_min: 0,
            write_level_max: 0,
        };
        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(_) => {


@@ 811,17 773,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 raw_status = nitrokey_sys::NK_status {
            firmware_version_major: 0,
            firmware_version_minor: 0,
            serial_number_smart_card: 0,
            config_numlock: 0,
            config_capslock: 0,
            config_scrolllock: 0,
            otp_user_password: false,
        };
        get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?;
        let mut status = Status::from(raw_status);
        let mut status: Status = get_struct(|out| unsafe { nitrokey_sys::NK_get_status(out) })?;

        let storage_status = self.get_storage_status()?;
        status.firmware_version = storage_status.firmware_version;

M src/device/wrapper.rs => src/device/wrapper.rs +15 -1
@@ 1,7 1,7 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use crate::device::{Device, Model, Pro, Status, Storage};
use crate::device::{Device, Librem, Model, Pro, Status, Storage};
use crate::error::Error;
use crate::otp::GenerateOtp;



@@ 64,7 64,10 @@ use crate::otp::GenerateOtp;
///
/// [`connect`]: struct.Manager.html#method.connect
#[derive(Debug)]
#[non_exhaustive]
pub enum DeviceWrapper<'a> {
    /// A Librem Key device.
    Librem(Librem<'a>),
    /// A Nitrokey Storage device.
    Storage(Storage<'a>),
    /// A Nitrokey Pro device.


@@ 74,6 77,7 @@ pub enum DeviceWrapper<'a> {
impl<'a> DeviceWrapper<'a> {
    fn device(&self) -> &dyn Device<'a> {
        match *self {
            DeviceWrapper::Librem(ref librem) => librem,
            DeviceWrapper::Storage(ref storage) => storage,
            DeviceWrapper::Pro(ref pro) => pro,
        }


@@ 81,12 85,19 @@ impl<'a> DeviceWrapper<'a> {

    fn device_mut(&mut self) -> &mut dyn Device<'a> {
        match *self {
            DeviceWrapper::Librem(ref mut librem) => librem,
            DeviceWrapper::Storage(ref mut storage) => storage,
            DeviceWrapper::Pro(ref mut pro) => pro,
        }
    }
}

impl<'a> From<Librem<'a>> for DeviceWrapper<'a> {
    fn from(device: Librem<'a>) -> Self {
        DeviceWrapper::Librem(device)
    }
}

impl<'a> From<Pro<'a>> for DeviceWrapper<'a> {
    fn from(device: Pro<'a>) -> Self {
        DeviceWrapper::Pro(device)


@@ 120,6 131,7 @@ 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(),
        }


@@ 127,6 139,7 @@ impl<'a> Device<'a> for DeviceWrapper<'a> {

    fn get_model(&self) -> Model {
        match *self {
            DeviceWrapper::Librem(_) => Model::Librem,
            DeviceWrapper::Pro(_) => Model::Pro,
            DeviceWrapper::Storage(_) => Model::Storage,
        }


@@ 134,6 147,7 @@ impl<'a> Device<'a> for DeviceWrapper<'a> {

    fn get_status(&self) -> Result<Status, Error> {
        match self {
            DeviceWrapper::Librem(dev) => dev.get_status(),
            DeviceWrapper::Pro(dev) => dev.get_status(),
            DeviceWrapper::Storage(dev) => dev.get_status(),
        }

M src/error.rs => src/error.rs +4 -0
@@ 11,6 11,7 @@ use crate::device;

/// An error returned by the nitrokey crate.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// An error reported by the Nitrokey device in the response packet.
    CommandError(CommandError),


@@ 111,6 112,7 @@ impl fmt::Display for Error {

/// An error reported by the Nitrokey device in the response packet.
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum CommandError {
    /// A packet with a wrong checksum has been sent or received.
    WrongCrc,


@@ 177,6 179,7 @@ impl fmt::Display for CommandError {

/// A device communication error.
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum CommunicationError {
    /// Could not connect to a Nitrokey device.
    NotConnected,


@@ 215,6 218,7 @@ impl fmt::Display for CommunicationError {

/// A library usage error.
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum LibraryError {
    /// A supplied string exceeded a length limit.
    StringTooLong,

M src/lib.rs => src/lib.rs +39 -8
@@ 13,8 13,8 @@
//! an reference to the [`Manager`][] singleton that keeps track of the connections.  Then use the
//! [`connect`][] method to connect to any Nitrokey device.  The method will return a
//! [`DeviceWrapper`][] that abstracts over the supported Nitrokey devices.  You can also use
//! [`connect_model`][], [`connect_pro`][] or [`connect_storage`][] to connect to a specific
//! device.
//! [`connect_model`][], [`connect_librem`][], [`connect_pro`][] or [`connect_storage`][] to
//! connect to a specific device.
//!
//! To get a list of all connected Nitrokey devices, use the [`list_devices`][] function.  You can
//! then connect to one of the connected devices using the [`connect_path`][] function of the


@@ 98,6 98,7 @@
//! [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
//! [`take`]: fn.take.html
//! [`connect`]: struct.Manager.html#method.connect
//! [`connect_librem`]: struct.Manager.html#method.connect_librem
//! [`connect_model`]: struct.Manager.html#method.connect_model
//! [`connect_path`]: struct.Manager.html#method.connect_path
//! [`connect_pro`]: struct.Manager.html#method.connect_pro


@@ 136,8 137,9 @@ use std::sync;
pub use crate::auth::{Admin, Authenticate, User};
pub use crate::config::Config;
pub use crate::device::{
    Device, DeviceInfo, DeviceWrapper, Model, OperationStatus, Pro, SdCardData, SerialNumber,
    Status, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus,
    Device, DeviceInfo, DeviceWrapper, FirmwareVersion, Librem, Model, OperationStatus, Pro,
    SdCardData, SerialNumber, Status, Storage, StorageProductionInfo, StorageStatus, VolumeMode,
    VolumeStatus,
};
pub use crate::error::{CommandError, CommunicationError, Error, LibraryError};
pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData};


@@ 192,8 194,8 @@ impl fmt::Display for Version {
/// time.
///
/// To obtain a reference to an instance of this manager, use the [`take`][] function.  Use one of
/// the connect methods – [`connect`][], [`connect_model`][], [`connect_pro`][] or
/// [`connect_storage`][] – to retrieve a [`Device`][] instance.
/// the connect methods – [`connect`][], [`connect_model`][], [`connect_librem`][],
/// [`connect_pro`][] or [`connect_storage`][] – to retrieve a [`Device`][] instance.
///
/// # Examples
///


@@ 229,6 231,7 @@ impl fmt::Display for Version {
/// ```
///
/// [`connect`]: #method.connect
/// [`connect_librem`]: #method.connect_librem
/// [`connect_model`]: #method.connect_model
/// [`connect_pro`]: #method.connect_pro
/// [`connect_storage`]: #method.connect_storage


@@ 249,8 252,7 @@ impl Manager {

    /// Connects to a Nitrokey device.
    ///
    /// This method can be used to connect to any connected device, both a Nitrokey Pro and a
    /// Nitrokey Storage.
    /// This method can be used to connect to any connected device.
    ///
    /// # Errors
    ///


@@ 355,6 357,35 @@ impl Manager {
        }
    }

    /// Connects to a Librem Key.
    ///
    /// # Errors
    ///
    /// - [`NotConnected`][] if no Nitrokey device of the given model is connected
    ///
    /// # Example
    ///
    /// ```
    /// use nitrokey::Librem;
    ///
    /// fn use_librem(device: Librem) {}
    ///
    /// match nitrokey::take()?.connect_librem() {
    ///     Ok(device) => use_librem(device),
    ///     Err(err) => println!("Could not connect to the Librem Key: {}", err),
    /// }
    /// # Ok::<(), nitrokey::Error>(())
    /// ```
    ///
    /// [`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())
        }
    }

    /// Connects to a Nitrokey Pro.
    ///
    /// # Errors

M src/otp.rs => src/otp.rs +4 -4
@@ 347,8 347,8 @@ pub struct OtpSlotData {
    pub secret: String,
    /// The OTP generation mode.
    pub mode: OtpMode,
    /// If true, press the enter key after sending an OTP code using double-pressed
    /// numlock, capslock or scrolllock.
    /// If true, press the enter key after sending an OTP code using double-pressed Num Lock, Caps
    /// Lock or Scroll Lock.
    pub use_enter: bool,
    /// Set the token ID, see [OATH Token Identifier Specification][tokspec], section “Class A”.
    ///


@@ 385,8 385,8 @@ impl OtpSlotData {
        }
    }

    /// Enables pressing the enter key after sending an OTP code using double-pressed numlock,
    /// capslock or scrollock.
    /// Enables pressing the enter key after sending an OTP code using double-pressed Num Lock,
    /// Caps Lock or Scroll Lock.
    pub fn use_enter(mut self) -> OtpSlotData {
        self.use_enter = true;
        self

M src/pws.rs => src/pws.rs +8 -2
@@ 1,7 1,7 @@
// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

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



@@ 173,7 173,7 @@ impl<'a, 'b> PasswordSafe<'a, 'b> {
            result[i as usize] = status_array[i as usize] == 1;
        }
        unsafe {
            libc::free(status_ptr as *mut libc::c_void);
            nitrokey_sys::NK_free_password_safe_slot_status(status_ptr);
        }
        Ok(result)
    }


@@ 369,6 369,12 @@ impl<'a, 'b> Drop for PasswordSafe<'a, 'b> {
    }
}

impl<'a> GetPasswordSafe<'a> for Librem<'a> {
    fn get_password_safe(&mut self, user_pin: &str) -> Result<PasswordSafe<'_, 'a>, Error> {
        get_password_safe(self, user_pin)
    }
}

impl<'a> GetPasswordSafe<'a> for Pro<'a> {
    fn get_password_safe(&mut self, user_pin: &str) -> Result<PasswordSafe<'_, 'a>, Error> {
        get_password_safe(self, user_pin)

M src/util.rs => src/util.rs +11 -0
@@ 68,6 68,17 @@ 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(())

M tests/device.rs => tests/device.rs +14 -22
@@ 42,17 42,12 @@ fn list_devices(_device: DeviceWrapper) {
    let devices = unwrap_ok!(nitrokey::list_devices());
    for device in devices {
        assert!(!device.path.is_empty());
        if let Some(model) = device.model {
            match model {
                nitrokey::Model::Pro => {
                    assert!(device.serial_number.is_some());
                    let serial_number = device.serial_number.unwrap();
                    assert!(serial_number != SerialNumber::empty());
                }
                nitrokey::Model::Storage => {
                    assert_eq!(None, device.serial_number);
                }
            }
        if Some(nitrokey::Model::Storage) == device.model {
            assert_eq!(None, device.serial_number);
        } else {
            assert!(device.serial_number.is_some());
            let serial_number = device.serial_number.unwrap();
            assert!(serial_number != SerialNumber::empty());
        }
    }
}


@@ 131,17 126,14 @@ fn connect_path(device: DeviceWrapper) {
    for device in devices {
        let connected_device = unwrap_ok!(manager.connect_path(device.path));
        assert_eq!(device.model, Some(connected_device.get_model()));
        match device.model.unwrap() {
            nitrokey::Model::Pro => {
                assert!(device.serial_number.is_some());
                assert_ok!(
                    device.serial_number.unwrap(),
                    connected_device.get_serial_number()
                );
            }
            nitrokey::Model::Storage => {
                assert_eq!(None, device.serial_number);
            }
        if nitrokey::Model::Storage == device.model.unwrap() {
            assert_eq!(None, device.serial_number);
        } else {
            assert!(device.serial_number.is_some());
            assert_ok!(
                device.serial_number.unwrap(),
                connected_device.get_serial_number()
            );
        }
    }
}