~ireas/nitrokey-rs

a8517d9707e5ef313d6f4d69b51d21251c82ea91 — Robin Krahl 3 years ago
Initial commit
A  => .gitignore +5 -0
@@ 1,5 @@

/target
/nitrokey-sys/target
**/*.rs.bk
Cargo.lock

A  => Cargo.toml +20 -0
@@ 1,20 @@
[package]
name = "nitrokey"
version = "0.1.0"
authors = ["Robin Krahl <robin.krahl@ireas.org>"]
homepage = "https://code.ireas.org/nitrokey-rs/"
repository = "https://git.ireas.org/nitrokey-rs/"
description = "Bindings to libnitrokey for communication with Nitrokey devices"
keywords = ["nitrokey", "otp"]
categories = ["api-bindings"]
license = "MIT"

[features]
default = ["test-no-device"]
test-no-device = []
test-pro = []

[dependencies]
libc = "0.2"
nitrokey-sys = { path = "nitrokey-sys" }
rand = "0.4"

A  => LICENSE +21 -0
@@ 1,21 @@
The MIT License (MIT)

Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

A  => README.md +64 -0
@@ 1,64 @@
# nitrokey-rs

A libnitrokey wrapper for Rust providing access to Nitrokey devices.

[Documentation][]

```toml
[dependencies]
nitrokey = "0.1.0"
```

## Compatibility

In order to use this crate, a [`libnitrokey`][] installation is required
(both development headers and library).  The crate is developed using version
3.2, but any newer version should work too.

As I only have access to a Nitrokey Pro, this crate only provides support for
the Nitrokey Pro methods.  If you want to contribute for the Nitrokey Storage,
please send a mail to [nitrokey-rs-dev@ireas.org][].

### Unsupported Functions

The following functions provided by `libnitrokey` are deliberately not
supported by `nitrokey-rs`:

- `NK_get_time()`.  This method is useless as it will always cause a timestamp
  error on the device (see [pull request #114][] for `libnitrokey` for details).
- `NK_get_status()`.  This method only provides a string representation of
  data that can be accessed by other methods (firmware version, seriel number,
  configuration).

## Tests

The default test suite assumes that no Nitrokey device is connected and only
performs minor sanity checks.  There is another test suite that assumes that a
Nitrokey Pro is connected (admin password `12345678`, user password `123456`).
To execute this test suite, run `cargo test --no-default-features --features 
test-pro -- --test-threads 1`.  Note that this test suite might lock your stick
if you have different passwords!

The `totp` and `totp_pin` tests can occasionally fail due to bad timing.  Also
make sure to run the tests sequentially (`--test-threads 1`), otherwise they
might interfere.

The `get_major_firmware_version` test will fail for newer `libnitrokey`
versions as it relies on buggy behavior in version 3.2.

## Contact

For bug reports, patches, feature requests or other messages, please send a
mail to [nitrokey-rs-dev@ireas.org][].

## License

This project is licensed under the [MIT License][].  `libnitrokey` is licensed
under the [LGPL-3.0][].

[Documentation]: https://docs.rs/nitrokey
[`libnitrokey`]: https://github.com/nitrokey/libnitrokey
[nitrokey-rs-dev@ireas.org]: mailto:nitrokey-rs-dev@ireas.org
[pull request #114]: https://github.com/Nitrokey/libnitrokey/pull/114
[MIT license]: https://opensource.org/licenses/MIT
[LGPL-3.0]: https://opensource.org/licenses/lgpl-3.0.html

A  => TODO.md +42 -0
@@ 1,42 @@
- Fix segmentation faults when freeing string literals with old Nitrokey
  versions.
- Add support and tests for the Nitrokey Storage.
- Add support for the currently unsupported commands:
  - `NK_lock_device`
  - `NK_factory_reset`
  - `NK_build_aes_key`
  - `NK_unlock_user_password`
  - `NK_erase_hotp_slot`
  - `NK_erase_totp_slot`
  - `NK_change_admin_PIN`
  - `NK_change_user_PIN`
  - `NK_enable_password_safe`
  - `NK_get_password_safe_slot_status`
  - `NK_get_password_safe_slot_name`
  - `NK_get_password_safe_slot_login`
  - `NK_get_password_safe_slot_password`
  - `NK_write_password_safe_slot`
  - `NK_erase_password_safe_slot`
  - `NK_is_AES_supported`
  - `NK_send_startup`
  - `NK_unlock_encrypted_volume`
  - `NK_lock_encrypted_volume`
  - `NK_unlock_hidden_volume`
  - `NK_lock_hidden_volume`
  - `NK_create_hidden_volume`
  - `NK_set_unencrypted_read_only`
  - `NK_set_unencrypted_read_write`
  - `NK_export_firmware`
  - `NK_clear_new_sd_card_warning`
  - `NK_fill_SD_card_with_random_data`
  - `NK_change_update_password`
  - `NK_get_status_storage_as_string`
  - `NK_get_SD_usage_data_as_string`
  - `NK_get_progress_bar_value`
- Fix timing issues with the `totp` and `totp_pin` test cases.
- Fix the inconsistent method `get_major_firmware_version`.
- Consider implementing `Drop` instead of the method `disconnect`.
- Find an example for `set_time`, also adapt `get_totp_code`.
- Improve log level documentation.
- Clear passwords from memory.
- Find a nicer syntax for the `write_config` test.

A  => nitrokey-sys/Cargo.toml +16 -0
@@ 1,16 @@
[package]
name = "nitrokey-sys"
version = "0.1.0"
authors = ["Robin Krahl <robin.krahl@ireas.org>"]
homepage = "https://code.ireas.org/nitrokey-rs/"
repository = "https://git.ireas.org/nitrokey-rs/"
description = "Bindings to libnitrokey for communication with Nitrokey devices"
categories = ["external-ffi-bindings"]
license = "MIT"
links = "nitrokey"
build = "build.rs"

[dependencies]

[build-dependencies]
bindgen = "0.26.3"

A  => nitrokey-sys/build.rs +17 -0
@@ 1,17 @@
extern crate bindgen;

use std::env;
use std::path::PathBuf;

fn main() {
    println!("cargo:rustc-link-lib=nitrokey");

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .generate()
        .expect("Unable to generate bindings");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Could not write bindings");
}

A  => nitrokey-sys/src/lib.rs +29 -0
@@ 1,29 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

#[cfg(test)]
mod tests {
    use super::*;
    use std::ffi::CString;

    #[test]
    fn login_auto() {
        unsafe {
            assert_eq!(0, NK_login_auto());
        }
    }

    #[test]
    fn login() {
        unsafe {
            // Unconnected
            assert_eq!(0, NK_login(CString::new("S").unwrap().as_ptr()));
            assert_eq!(0, NK_login(CString::new("P").unwrap().as_ptr()));
            // Unsupported model
            assert_eq!(0, NK_login(CString::new("T").unwrap().as_ptr()));
        }
    }
}

A  => nitrokey-sys/wrapper.h +2 -0
@@ 1,2 @@
#include <stdbool.h>
#include <libnitrokey/NK_C_API.h>

A  => src/lib.rs +1303 -0
@@ 1,1303 @@
//! Provides access to a Nitrokey device using the native libnitrokey API.
//!
//! # Usage
//!
//! Operations on the Nitrokey require different authentication levels.  Some
//! operations can be performed without authentication, some require user
//! access, and some require admin access.  This is modelled using the types
//! [`UnauthenticatedDevice`][], [`UserAuthenticatedDevice`][] and
//! [`AdminAuthenticatedDevice`][].
//!
//! Use [`connect`][] or [`connect_model`][] to obtain an
//! [`UnauthenticatedDevice`][].  You can then use [`authenticate_user`][] or
//! [`authenticate_admin`][] to get an authenticated device.  You can then use
//! [`device`][] to go back to the unauthenticated device.
//!
//! This makes sure that you can only execute a command if you have the
//! required access rights.  Otherwise, your code will not compile.  The only
//! exception are the methods to generate one-time passwords –
//! [`get_hotp_code`][] and [`get_totp_code`][].  Depending on the stick
//! configuration, these operations are available without authentication or
//! with user authentication.
//!
//! Per default, libnitrokey writes log messages, for example the packets that
//! are sent to and received from the stick, to the standard output.  To
//! change this behaviour, use [`set_debug`][] or [`set_log_level`][].
//!
//! # Examples
//!
//! Connect to any Nitrokey and print its serial number:
//!
//! ```no_run
//! use nitrokey::Device;
//! # use nitrokey::CommandError;
//!
//! # fn try_main() -> Result<(), CommandError> {
//! let device = nitrokey::connect()?;
//! println!("{}", device.get_serial_number()?);
//! #     Ok(())
//! # }
//! ```
//!
//! Configure an HOTP slot:
//!
//! ```no_run
//! use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData};
//! # use nitrokey::CommandError;
//!
//! # fn try_main() -> Result<(), (CommandError)> {
//! let device = nitrokey::connect()?;
//! let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits);
//! match device.authenticate_admin("12345678") {
//!     Ok(admin) => {
//!         match admin.write_hotp_slot(slot_data, 0) {
//!             CommandStatus::Success => println!("Successfully wrote slot."),
//!             CommandStatus::Error(err) => println!("Could not write slot: {:?}", err),
//!         }
//!     },
//!     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err),
//! }
//! #     Ok(())
//! # }
//! ```
//!
//! Generate an HOTP one-time password:
//!
//! ```no_run
//! use nitrokey::Device;
//! # use nitrokey::CommandError;
//!
//! # fn try_main() -> Result<(), (CommandError)> {
//! let device = nitrokey::connect()?;
//! match device.get_hotp_code(1) {
//!     Ok(code) => println!("Generated HOTP code: {:?}", code),
//!     Err(err) => println!("Could not generate HOTP code: {:?}", err),
//! }
//! #     Ok(())
//! # }
//! ```
//!
//! [`authenticate_admin`]: struct.UnauthenticatedDevice.html#method.authenticate_admin
//! [`authenticate_user`]: struct.UnauthenticatedDevice.html#method.authenticate_user
//! [`connect`]: fn.connect.html
//! [`connect_model`]: fn.connect_model.html
//! [`device`]: struct.AuthenticatedDevice.html#method.device
//! [`get_hotp_code`]: trait.Device.html#method.get_hotp_code
//! [`get_totp_code`]: trait.Device.html#method.get_totp_code
//! [`set_debug`]: fn.set_debug.html
//! [`set_log_level`]: fn.set_log_level.html
//! [`AdminAuthenticatedDevice`]: struct.AdminAuthenticatedDevice.html
//! [`UserAuthenticatedDevice`]: struct.UserAuthenticatedDevice.html
//! [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html

extern crate libc;
extern crate nitrokey_sys;
extern crate rand;

use std::ffi::CString;
use std::ffi::CStr;
use libc::c_int;
use rand::Rng;

#[cfg(test)]
mod tests;

/// Modes for one-time password generation.
#[derive(Debug, PartialEq)]
pub enum OtpMode {
    /// Generate one-time passwords with six digits.
    SixDigits,
    /// Generate one-time passwords with eight digits.
    EightDigits,
}

/// Error types returned by Nitrokey device or by the library.
#[derive(Debug, PartialEq)]
pub enum CommandError {
    /// A packet with a wrong checksum has been sent or received.
    WrongCrc,
    /// A command tried to access an OTP slot that does not exist.
    WrongSlot,
    /// A command tried to generate an OTP on a slot that is not configured.
    SlotNotProgrammed,
    /// The provided password is wrong.
    WrongPassword,
    /// You are not authorized for this command or provided a wrong temporary
    /// password.
    NotAuthorized,
    /// An error occured when getting or setting the time.
    Timestamp,
    /// You did not provide a name for the OTP slot.
    NoName,
    /// This command is not supported by this device.
    NotSupported,
    /// This command is unknown.
    UnknownCommand,
    /// AES decryptionfailed.
    AesDecryptionFailed,
    /// An unknown error occured.
    Unknown,
    /// You passed a string containing a null byte.
    InvalidString,
    /// You passed an invalid slot.
    InvalidSlot,
    /// An error occured during random number generation.
    RngError,
}

/// Command execution status.
#[derive(Debug, PartialEq)]
pub enum CommandStatus {
    /// The command was successful.
    Success,
    /// An error occured during command execution.
    Error(CommandError),
}

/// Log level for libnitrokey.
#[derive(Debug, PartialEq)]
pub enum LogLevel {
    /// Only log error messages.
    Error,
    /// Log error messages and warnings.
    Warning,
    /// Log error messages, warnings and info messages.
    Info,
    /// Log error messages, warnings, info messages and debug messages.
    DebugL1,
    /// Log error messages, warnings, info messages and detailed debug
    /// messages.
    Debug,
    /// Log error messages, warnings, info messages and very detailed debug
    /// messages.
    DebugL2,
}

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

/// 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 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>,
    /// If set, OTP generation using [`get_hotp_code`][] or [`get_totp_code`][]
    /// requires user authentication.  Otherwise, OTPs can be generated without
    /// authentication.
    ///
    /// [`get_hotp_code`]: struct.Device.html#method.get_hotp_code
    /// [`get_totp_code`]: struct.Device.html#method.get_totp_code
    pub user_password: bool,
}

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

#[derive(Debug)]
/// A Nitrokey device without user or admin authentication.
///
/// Use [`connect`][] or [`connect_model`][] to 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::{UnauthenticatedDevice, UserAuthenticatedDevice};
/// # use nitrokey::CommandError;
///
/// fn perform_user_task(device: &UserAuthenticatedDevice) {}
/// fn perform_other_task(device: &UnauthenticatedDevice) {}
///
/// # fn try_main() -> Result<(), CommandError> {
/// let device = nitrokey::connect()?;
/// let device = match device.authenticate_user("123456") {
///     Ok(user) => {
///         perform_user_task(&user);
///         user.device()
///     },
///     Err((device, err)) => {
///         println!("Could not authenticate as user: {:?}", err);
///         device
///     },
/// };
/// perform_other_task(&device);
/// #     Ok(())
/// # }
/// ```
///
/// [`authenticate_admin`]: #method.authenticate_admin
/// [`authenticate_user`]: #method.authenticate_user
/// [`connect`]: fn.connect.html
/// [`connect_model`]: fn.connect_model.html
pub struct UnauthenticatedDevice {}

/// A Nitrokey device with user authentication.
///
/// To obtain an instance of this struct, use the [`authenticate_user`][]
/// method on an [`UnauthenticatedDevice`][].  To get back to an
/// unauthenticated device, use the [`device`][] method.
///
/// [`authenticate_admin`]: struct.UnauthenticatedDevice#method.authenticate_admin
/// [`device`]: #method.device
/// [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html
pub struct UserAuthenticatedDevice {
    device: UnauthenticatedDevice,
    temp_password: Vec<u8>,
}

/// A Nitrokey device with admin authentication.
///
/// To obtain an instance of this struct, use the [`authenticate_admin`][]
/// method on an [`UnauthenticatedDevice`][].  To get back to an
/// unauthenticated device, use the [`device`][] method.
///
/// [`authenticate_admin`]: struct.UnauthenticatedDevice#method.authenticate_admin
/// [`device`]: #method.device
/// [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html
pub struct AdminAuthenticatedDevice {
    device: UnauthenticatedDevice,
    temp_password: Vec<u8>,
}

/// The configuration for an OTP slot.
#[derive(Debug)]
pub struct OtpSlotData {
    /// The number of the slot – must be less than three for HOTP and less than
    /// 15 for TOTP.
    pub number: u8,
    /// The name of the slot – must not be empty.
    pub name: String,
    /// The secret for the slot.
    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.
    pub use_enter: bool,
    /// Set the token ID [OATH Token Identifier Specification][tokspec], section
    /// “Class A”.
    ///
    /// [tokspec]: https://openauthentication.org/token-specs/
    pub token_id: Option<String>,
}

#[derive(Debug)]
struct RawOtpSlotData {
    pub number: u8,
    pub name: CString,
    pub secret: CString,
    pub mode: OtpMode,
    pub use_enter: bool,
    pub use_token_id: bool,
    pub token_id: CString,
}

static TEMPORARY_PASSWORD_LENGTH: usize = 25;

/// A Nitrokey device.
///
/// This trait provides the commands that can be executed without
/// authentication.  The only exception are the [`get_hotp_code`][] and
/// [`get_totp_code`][] methods:  It depends on the device configuration
/// ([`get_config`][]) whether these commands require user authentication
/// or not.
///
/// [`get_config`]: #method.get_config
/// [`get_hotp_code`]: #method.get_hotp_code
/// [`get_totp_code`]: #method.get_totp_code
pub trait Device {
    /// Closes the connection to this device.  This method consumes the device.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// // perform tasks ...
    /// device.disconnect();
    /// #     Ok(())
    /// # }
    /// ```
    fn disconnect(self)
    where
        Self: std::marker::Sized,
    {
        unsafe {
            nitrokey_sys::NK_logout();
        }
    }

    /// Sets the time on the Nitrokey.  This command may set the time to
    /// arbitrary values.  `time` is the number of seconds since January 1st,
    /// 1970 (Unix timestamp).
    ///
    /// The time is used for TOTP generation (see [`get_totp_code`][]).
    ///
    /// # Errors
    ///
    /// - [`Timestamp`][] if the time could not be set
    ///
    /// [`get_totp_code`]: #method.get_totp_code
    /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp
    // TODO: example
    fn set_time(&self, time: u64) -> CommandStatus {
        unsafe { CommandStatus::from(nitrokey_sys::NK_totp_set_time(time)) }
    }

    /// Returns the serial number of the Nitrokey device.  The serial number
    /// is the string representation of a hex number.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// match device.get_serial_number() {
    ///     Ok(number) => println!("serial no: {:?}", number),
    ///     Err(err) => println!("Could not get serial number: {:?}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    fn get_serial_number(&self) -> Result<String, CommandError> {
        unsafe { result_from_string(nitrokey_sys::NK_device_serial_number()) }
    }

    /// Returns the name of the given HOTP slot.
    ///
    /// # Errors
    ///
    /// - [`SlotNotProgrammed`][] if the given slot is not configured
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{CommandError, Device};
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// match device.get_hotp_slot_name(1) {
    ///     Ok(name) => println!("HOTP slot 1: {:?}", name),
    ///     Err(CommandError::SlotNotProgrammed) => println!("HOTP slot 1 not programmed"),
    ///     Err(err) => println!("Could not get slot name: {:?}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    fn get_hotp_slot_name(&self, slot: u8) -> Result<String, CommandError> {
        unsafe { result_from_string(nitrokey_sys::NK_get_hotp_slot_name(slot)) }
    }

    /// Returns the name of the given TOTP slot.
    ///
    /// # Errors
    ///
    /// - [`SlotNotProgrammed`][] if the given slot is not configured
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{CommandError, Device};
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// match device.get_totp_slot_name(1) {
    ///     Ok(name) => println!("TOTP slot 1: {:?}", name),
    ///     Err(CommandError::SlotNotProgrammed) => println!("TOTP slot 1 not programmed"),
    ///     Err(err) => println!("Could not get slot name: {:?}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    fn get_totp_slot_name(&self, slot: u8) -> Result<String, CommandError> {
        unsafe { result_from_string(nitrokey_sys::NK_get_totp_slot_name(slot)) }
    }

    /// Returns the number of remaining authentication attempts for the user.  The
    /// total number of available attempts is three.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let count = device.get_user_retry_count();
    /// println!("{} remaining authentication attempts (user)", count);
    /// #     Ok(())
    /// # }
    /// ```
    fn get_user_retry_count(&self) -> u8 {
        unsafe { nitrokey_sys::NK_get_user_retry_count() }
    }

    /// Returns the number of remaining authentication attempts for the admin.  The
    /// total number of available attempts is three.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let count = device.get_admin_retry_count();
    /// println!("{} remaining authentication attempts (admin)", count);
    /// #     Ok(())
    /// # }
    /// ```
    fn get_admin_retry_count(&self) -> u8 {
        unsafe { nitrokey_sys::NK_get_admin_retry_count() }
    }

    /// Returns the major part of the firmware version (should be zero).
    /// Note that this method is buggy for libnitrokey older than v3.3.  For
    /// these versions, this method returns the minor part.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// println!("Firmware version: {}.x", device.get_major_firmware_version());
    /// #     Ok(())
    /// # }
    /// ```
    fn get_major_firmware_version(&self) -> i32 {
        unsafe { nitrokey_sys::NK_get_major_firmware_version() }
    }

    /// Returns the current configuration of the Nitrokey device.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let config = device.get_config()?;
    /// println!("numlock binding:          {:?}", config.numlock);
    /// println!("capslock binding:         {:?}", config.capslock);
    /// println!("scrollock binding:        {:?}", config.scrollock);
    /// println!("require password for OTP: {:?}", config.user_password);
    /// #     Ok(())
    /// # }
    /// ```
    fn get_config(&self) -> Result<Config, CommandError> {
        unsafe {
            let config_ptr = nitrokey_sys::NK_read_config();
            if config_ptr.is_null() {
                return Err(get_last_error());
            }
            let config_array_ptr = config_ptr as *const [u8; 5];
            let raw_config = RawConfig::from(*config_array_ptr);
            libc::free(config_ptr as *mut libc::c_void);
            return Ok(raw_config.into());
        }
    }

    /// Generates an HOTP code on the given slot.  This operation may require
    /// user authorization, depending on the device configuration (see
    /// [`get_config`][]).
    ///
    /// # Errors
    ///
    /// - [`NotAuthorized`][] if OTP generation requires user authentication
    /// - [`SlotNotProgrammed`][] if the given slot is not configured
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let code = device.get_hotp_code(1)?;
    /// println!("Generated HOTP code on slot 1: {:?}", code);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`get_config`]: #method.get_config
    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    fn get_hotp_code(&self, slot: u8) -> Result<String, CommandError> {
        unsafe {
            return result_from_string(nitrokey_sys::NK_get_hotp_code(slot));
        }
    }

    /// Generates a TOTP code on the given slot.  This operation may require
    /// user authorization, depending on the device configuration (see
    /// [`get_config`][]).
    ///
    /// To make sure that the Nitrokey’s time is in sync, consider calling
    /// [`set_time`][] before calling this method.
    ///
    /// # Errors
    ///
    /// - [`NotAuthorized`][] if OTP generation requires user authentication
    /// - [`SlotNotProgrammed`][] if the given slot is not configured
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let code = device.get_totp_code(1)?;
    /// println!("Generated TOTP code on slot 1: {:?}", code);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`set_time`]: #method.set_time
    /// [`get_config`]: #method.get_config
    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    fn get_totp_code(&self, slot: u8) -> Result<String, CommandError> {
        unsafe {
            return result_from_string(nitrokey_sys::NK_get_totp_code(slot, 0, 0, 0));
        }
    }
}

trait AuthenticatedDevice {
    fn new(device: UnauthenticatedDevice, temp_password: Vec<u8>) -> Self;
}

/// Connects to a Nitrokey device.  This method can be used to connect to any
/// connected device, both a Nitrokey Pro and a Nitrokey Storage.
///
/// # Example
///
/// ```
/// use nitrokey::UnauthenticatedDevice;
///
/// fn do_something(device: UnauthenticatedDevice) {}
///
/// match nitrokey::connect() {
///     Ok(device) => do_something(device),
///     Err(err) => println!("Could not connect to a Nitrokey: {:?}", err),
/// }
/// ```
pub fn connect() -> Result<UnauthenticatedDevice, CommandError> {
    unsafe {
        match nitrokey_sys::NK_login_auto() {
            1 => Ok(UnauthenticatedDevice {}),
            _ => Err(CommandError::Unknown),
        }
    }
}

/// Connects to a Nitrokey device of the given model.
///
/// # Example
///
/// ```
/// use nitrokey::{Model, UnauthenticatedDevice};
///
/// fn do_something(device: UnauthenticatedDevice) {}
///
/// match nitrokey::connect_model(Model::Pro) {
///     Ok(device) => do_something(device),
///     Err(err) => println!("Could not connect to a Nitrokey Pro: {:?}", err),
/// }
/// ```
pub fn connect_model(model: Model) -> Result<UnauthenticatedDevice, CommandError> {
    let model_string = match model {
        Model::Storage => "S",
        Model::Pro => "P",
    };
    let model_cstring = CString::new(model_string);
    if model_cstring.is_err() {
        return Err(CommandError::InvalidString);
    }
    let model = model_cstring.unwrap();
    unsafe {
        return match nitrokey_sys::NK_login(model.as_ptr()) {
            1 => Ok(UnauthenticatedDevice {}),
            rv => Err(CommandError::from(rv)),
        };
    }
}

/// Enables or disables debug output.  Calling this method with `true` is
/// equivalent to setting the log level to `Debug`; calling it with `false` is
/// equivalent to the log level `Error` (see [`set_log_level`][]).
///
/// If debug output is enabled, detailed information about the communication
/// with the Nitrokey device is printed to the standard output.
///
/// [`set_log_level`]: fn.set_log_level.html
pub fn set_debug(state: bool) {
    unsafe {
        nitrokey_sys::NK_set_debug(state);
    }
}

/// Sets the log level for libnitrokey.  All log messages are written to the
/// standard output or standard errror.
pub fn set_log_level(level: LogLevel) {
    unsafe {
        nitrokey_sys::NK_set_debug_level(level.into());
    }
}

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

fn option_to_config_otp_slot(value: Option<u8>) -> Result<u8, CommandError> {
    match value {
        Some(value) => {
            if value < 3 {
                Ok(value)
            } else {
                Err(CommandError::InvalidSlot)
            }
        }
        None => Ok(255),
    }
}

impl From<c_int> for CommandError {
    fn from(value: c_int) -> Self {
        match value {
            1 => CommandError::WrongCrc,
            2 => CommandError::WrongSlot,
            3 => CommandError::SlotNotProgrammed,
            4 => CommandError::WrongPassword,
            5 => CommandError::NotAuthorized,
            6 => CommandError::Timestamp,
            7 => CommandError::NoName,
            8 => CommandError::NotSupported,
            9 => CommandError::UnknownCommand,
            10 => CommandError::AesDecryptionFailed,
            _ => CommandError::Unknown,
        }
    }
}

impl From<c_int> for CommandStatus {
    fn from(value: c_int) -> Self {
        match value {
            0 => CommandStatus::Success,
            other => CommandStatus::Error(CommandError::from(other)),
        }
    }
}

impl Into<i32> for LogLevel {
    fn into(self) -> i32 {
        match self {
            LogLevel::Error => 0,
            LogLevel::Warning => 1,
            LogLevel::Info => 2,
            LogLevel::DebugL1 => 3,
            LogLevel::Debug => 4,
            LogLevel::DebugL2 => 5,
        }
    }
}

fn get_last_status() -> CommandStatus {
    unsafe {
        let status = nitrokey_sys::NK_get_last_command_status();
        return CommandStatus::from(status as c_int);
    }
}

fn get_last_error() -> CommandError {
    return match get_last_status() {
        CommandStatus::Success => CommandError::Unknown,
        CommandStatus::Error(err) => err,
    };
}

fn owned_str_from_ptr(ptr: *const std::os::raw::c_char) -> String {
    unsafe {
        return CStr::from_ptr(ptr).to_string_lossy().into_owned();
    }
}

fn result_from_string(ptr: *const std::os::raw::c_char) -> Result<String, CommandError> {
    if ptr.is_null() {
        return Err(CommandError::Unknown);
    }
    unsafe {
        let s = owned_str_from_ptr(ptr);
        libc::free(ptr as *mut libc::c_void);
        if s.is_empty() {
            return Err(get_last_error());
        }
        return Ok(s);
    }
}

fn generate_password(length: usize) -> std::io::Result<Vec<u8>> {
    let mut rng = match rand::OsRng::new() {
        Ok(rng) => rng,
        Err(err) => return Err(err),
    };
    let mut data = vec![0u8; length];
    rng.fill_bytes(&mut data[..]);
    return Ok(data);
}

impl OtpSlotData {
    /// Constructs a new instance of this struct.
    pub fn new(number: u8, name: &str, secret: &str, mode: OtpMode) -> OtpSlotData {
        OtpSlotData {
            number,
            name: String::from(name),
            secret: String::from(secret),
            mode,
            use_enter: false,
            token_id: None,
        }
    }
}

impl RawOtpSlotData {
    pub fn new(data: OtpSlotData) -> Result<RawOtpSlotData, CommandError> {
        let name = CString::new(data.name);
        let secret = CString::new(data.secret);
        let use_token_id = data.token_id.is_some();
        let token_id = CString::new(data.token_id.unwrap_or_else(String::new));
        if name.is_err() || secret.is_err() || token_id.is_err() {
            return Err(CommandError::InvalidString);
        }

        Ok(RawOtpSlotData {
            number: data.number,
            name: name.unwrap(),
            secret: secret.unwrap(),
            mode: data.mode,
            use_enter: data.use_enter,
            use_token_id,
            token_id: token_id.unwrap(),
        })
    }
}

impl Config {
    /// Constructs a new instance of this struct.
    pub fn new(
        numlock: Option<u8>,
        capslock: Option<u8>,
        scrollock: Option<u8>,
        user_password: bool,
    ) -> Config {
        Config {
            numlock,
            capslock,
            scrollock,
            user_password,
        }
    }
}

impl RawConfig {
    fn try_from(config: Config) -> Result<RawConfig, CommandError> {
        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,
        })
    }
}

impl From<[u8; 5]> for RawConfig {
    fn from(data: [u8; 5]) -> Self {
        RawConfig {
            numlock: data[0],
            capslock: data[1],
            scrollock: data[2],
            user_password: data[3] != 0,
        }
    }
}

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 UnauthenticatedDevice {
    fn authenticate<D, T>(
        self,
        password: &str,
        callback: T,
    ) -> Result<D, (UnauthenticatedDevice, CommandError)>
    where
        D: AuthenticatedDevice,
        T: Fn(*const i8, *const i8) -> c_int,
    {
        let temp_password = match generate_password(TEMPORARY_PASSWORD_LENGTH) {
            Ok(pw) => pw,
            Err(_) => return Err((self, CommandError::RngError)),
        };
        let password = CString::new(password);
        if password.is_err() {
            return Err((self, CommandError::InvalidString));
        }

        let pw = password.unwrap();
        let password_ptr = pw.as_ptr();
        let temp_password_ptr = temp_password.as_ptr() as *const i8;
        return match callback(password_ptr, temp_password_ptr) {
            0 => Ok(D::new(self, temp_password)),
            rv => Err((self, CommandError::from(rv))),
        };
    }

    /// 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.
    ///
    /// This method generates a random temporary password that is used for all
    /// operations that require user access.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if the provided user password contains a null byte
    /// - [`RngError`][] if the generation of the temporary password failed
    /// - [`WrongPassword`][] if the provided user password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{UnauthenticatedDevice, UserAuthenticatedDevice};
    /// # use nitrokey::CommandError;
    ///
    /// fn perform_user_task(device: &UserAuthenticatedDevice) {}
    /// fn perform_other_task(device: &UnauthenticatedDevice) {}
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let device = match device.authenticate_user("123456") {
    ///     Ok(user) => {
    ///         perform_user_task(&user);
    ///         user.device()
    ///     },
    ///     Err((device, err)) => {
    ///         println!("Could not authenticate as user: {:?}", err);
    ///         device
    ///     },
    /// };
    /// perform_other_task(&device);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
    /// [`RngError`]: enum.CommandError.html#variant.RngError
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn authenticate_user(
        self,
        password: &str,
    ) -> Result<UserAuthenticatedDevice, (UnauthenticatedDevice, CommandError)> {
        return self.authenticate(password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr)
        });
    }

    /// Performs admin authentication.  This method consumes the device.  If
    /// successful, an authenticated device is returned.  Otherwise, the
    /// current unauthenticated device and the error are returned.
    ///
    /// This method generates a random temporary password that is used for all
    /// operations that require admin access.
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if the provided admin password contains a null byte
    /// - [`RngError`][] if the generation of the temporary password failed
    /// - [`WrongPassword`][] if the provided admin password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{AdminAuthenticatedDevice, UnauthenticatedDevice};
    /// # use nitrokey::CommandError;
    ///
    /// fn perform_admin_task(device: &AdminAuthenticatedDevice) {}
    /// fn perform_other_task(device: &UnauthenticatedDevice) {}
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let device = match device.authenticate_admin("123456") {
    ///     Ok(admin) => {
    ///         perform_admin_task(&admin);
    ///         admin.device()
    ///     },
    ///     Err((device, err)) => {
    ///         println!("Could not authenticate as admin: {:?}", err);
    ///         device
    ///     },
    /// };
    /// perform_other_task(&device);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
    /// [`RngError`]: enum.CommandError.html#variant.RngError
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    pub fn authenticate_admin(
        self,
        password: &str,
    ) -> Result<AdminAuthenticatedDevice, (UnauthenticatedDevice, CommandError)> {
        return self.authenticate(password, |password_ptr, temp_password_ptr| unsafe {
            nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr)
        });
    }
}

impl Device for UnauthenticatedDevice {}

impl UserAuthenticatedDevice {
    /// Forgets the user authentication and returns an unauthenticated
    /// device.  This method consumes the authenticated device.  It does not
    /// perform any actual commands on the Nitrokey.
    pub fn device(self) -> UnauthenticatedDevice {
        self.device
    }
}

impl Device for UserAuthenticatedDevice {
    /// Generates an HOTP code on the given slot.  This operation may not
    /// require user authorization, depending on the device configuration (see
    /// [`get_config`][]).
    ///
    /// # Errors
    ///
    /// - [`SlotNotProgrammed`][] if the given slot is not configured
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// match device.authenticate_user("123456") {
    ///     Ok(user) => {
    ///         let code = user.get_hotp_code(1)?;
    ///         println!("Generated HOTP code on slot 1: {:?}", code);
    ///     },
    ///     Err(err) => println!("Could not authenticate: {:?}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`get_config`]: #method.get_config
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    fn get_hotp_code(&self, slot: u8) -> Result<String, CommandError> {
        unsafe {
            let temp_password_ptr = self.temp_password.as_ptr() as *const i8;
            return result_from_string(nitrokey_sys::NK_get_hotp_code_PIN(slot, temp_password_ptr));
        }
    }

    /// Generates a TOTP code on the given slot.  This operation may not
    /// require user authorization, depending on the device configuration (see
    /// [`get_config`][]).
    ///
    /// To make sure that the Nitrokey’s time is in sync, consider calling
    /// [`set_time`][] before calling this method.
    ///
    /// # Errors
    ///
    /// - [`SlotNotProgrammed`][] if the given slot is not configured
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Device;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// match device.authenticate_user("123456") {
    ///     Ok(user) => {
    ///         let code = user.get_totp_code(1)?;
    ///         println!("Generated TOTP code on slot 1: {:?}", code);
    ///     },
    ///     Err(err) => println!("Could not authenticate: {:?}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`get_config`]: #method.get_config
    /// [`set_time`]: trait.Device.html#method.set_time
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    fn get_totp_code(&self, slot: u8) -> Result<String, CommandError> {
        unsafe {
            let temp_password_ptr = self.temp_password.as_ptr() as *const i8;
            return result_from_string(nitrokey_sys::NK_get_totp_code_PIN(
                slot,
                0,
                0,
                0,
                temp_password_ptr,
            ));
        }
    }
}

impl AuthenticatedDevice for UserAuthenticatedDevice {
    fn new(device: UnauthenticatedDevice, temp_password: Vec<u8>) -> Self {
        UserAuthenticatedDevice {
            device,
            temp_password,
        }
    }
}

impl AdminAuthenticatedDevice {
    /// Forgets the user authentication and returns an unauthenticated
    /// device.  This method consumes the authenticated device.  It does not
    /// perform any actual commands on the Nitrokey.
    pub fn device(self) -> UnauthenticatedDevice {
        self.device
    }

    /// Writes the given configuration to the Nitrokey device.
    ///
    /// # Errors
    ///
    /// - [`InvalidSlot`][] if the provided numlock, capslock or scrolllock
    ///   slot is larger than two
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::Config;
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), CommandError> {
    /// let device = nitrokey::connect()?;
    /// let config = Config::new(None, None, None, false);
    /// match device.authenticate_admin("12345678") {
    ///     Ok(admin) => {
    ///         admin.write_config(config);
    ///         ()
    ///     },
    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot
    pub fn write_config(&self, config: Config) -> CommandStatus {
        let raw_config = match RawConfig::try_from(config) {
            Ok(raw_config) => raw_config,
            Err(err) => return CommandStatus::Error(err),
        };
        unsafe {
            let rv = nitrokey_sys::NK_write_config(
                raw_config.numlock,
                raw_config.capslock,
                raw_config.scrollock,
                raw_config.user_password,
                false,
                self.temp_password.as_ptr() as *const i8,
            );
            return CommandStatus::from(rv);
        }
    }

    fn write_otp_slot<T>(&self, data: OtpSlotData, callback: T) -> CommandStatus
    where
        T: Fn(RawOtpSlotData, *const i8) -> c_int,
    {
        let raw_data = match RawOtpSlotData::new(data) {
            Ok(raw_data) => raw_data,
            Err(err) => return CommandStatus::Error(err),
        };
        let temp_password_ptr = self.temp_password.as_ptr() as *const i8;
        let rv = callback(raw_data, temp_password_ptr);
        return CommandStatus::from(rv);
    }

    /// Configure an HOTP slot with the given data and set the HOTP counter to
    /// the given value (default 0).
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if the provided token ID contains a null byte
    /// - [`NoName`][] if the provided name is empty
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData};
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), (CommandError)> {
    /// let device = nitrokey::connect()?;
    /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits);
    /// match device.authenticate_admin("12345678") {
    ///     Ok(admin) => {
    ///         match admin.write_hotp_slot(slot_data, 0) {
    ///             CommandStatus::Success => println!("Successfully wrote slot."),
    ///             CommandStatus::Error(err) => println!("Could not write slot: {:?}", err),
    ///         }
    ///     },
    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err),
    /// }
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
    /// [`NoName`]: enum.CommandError.html#variant.NoName
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    pub fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> CommandStatus {
        return self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| 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(),
                temp_password_ptr,
            )
        });
    }

    /// Configure a TOTP slot with the given data and set the TOTP time window
    /// to the given value (default 30).
    ///
    /// # Errors
    ///
    /// - [`InvalidString`][] if the provided token ID contains a null byte
    /// - [`NoName`][] if the provided name is empty
    /// - [`WrongSlot`][] if there is no slot with the given number
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData};
    /// # use nitrokey::CommandError;
    ///
    /// # fn try_main() -> Result<(), (CommandError)> {
    /// let device = nitrokey::connect()?;
    /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::EightDigits);
    /// match device.authenticate_admin("12345678") {
    ///     Ok(admin) => {
    ///         match admin.write_totp_slot(slot_data, 30) {
    ///             CommandStatus::Success => println!("Successfully wrote slot."),
    ///             CommandStatus::Error(err) => println!("Could not write slot: {:?}", err),
    ///         }
    ///     },
    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err),
    /// }
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
    /// [`NoName`]: enum.CommandError.html#variant.NoName
    /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot
    pub fn write_totp_slot(&self, data: OtpSlotData, time_window: u16) -> CommandStatus {
        return self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| 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(),
                temp_password_ptr,
            )
        });
    }
}

impl Device for AdminAuthenticatedDevice {}

impl AuthenticatedDevice for AdminAuthenticatedDevice {
    fn new(device: UnauthenticatedDevice, temp_password: Vec<u8>) -> Self {
        AdminAuthenticatedDevice {
            device,
            temp_password,
        }
    }
}

A  => src/tests/mod.rs +2 -0
@@ 1,2 @@
mod no_device;
mod pro;

A  => src/tests/no_device.rs +9 -0
@@ 1,9 @@
use Model;

#[test]
#[cfg_attr(not(feature = "test-no-device"), ignore)]
fn connect() {
    assert!(::connect().is_err());
    assert!(::connect_model(Model::Storage).is_err());
    assert!(::connect_model(Model::Pro).is_err());
}

A  => src/tests/pro.rs +268 -0
@@ 1,268 @@
use std::ffi::CStr;
use std::marker::Sized;
use {set_debug, AdminAuthenticatedDevice, CommandError, CommandStatus, Config, Device, Model,
     OtpMode, OtpSlotData, UnauthenticatedDevice};

static ADMIN_PASSWORD: &str = "12345678";
static USER_PASSWORD: &str = "123456";

// test suite according to RFC 4226, Appendix D
static HOTP_SECRET: &str = "3132333435363738393031323334353637383930";
static HOTP_CODES: &[&str] = &[
    "755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871",
    "520489",
];

// test suite according to RFC 6238, Appendix B
static TOTP_SECRET: &str = "3132333435363738393031323334353637383930";
static TOTP_CODES: &[(u64, &str)] = &[
    (59, "94287082"),
    (1111111109, "07081804"),
    (1111111111, "14050471"),
    (1234567890, "89005924"),
    (2000000000, "69279037"),
    (20000000000, "65353130"),
];

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn connect() {
    set_debug(false);
    assert!(::connect().is_ok());
    assert!(::connect_model(Model::Pro).is_ok());
    assert!(::connect_model(Model::Storage).is_err());
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn disconnect() {
    set_debug(false);
    ::connect().unwrap().disconnect();
    unsafe {
        let ptr = ::nitrokey_sys::NK_device_serial_number();
        assert!(!ptr.is_null());
        let cstr = CStr::from_ptr(ptr);
        assert_eq!(cstr.to_string_lossy(), "");
    }
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn get_serial_number() {
    set_debug(false);
    let device = ::connect().unwrap();
    let result = device.get_serial_number();
    assert!(result.is_ok());
    let serial_number = result.unwrap();
    assert!(serial_number.is_ascii());
    assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit()));
}

fn configure_hotp(admin: &AdminAuthenticatedDevice) {
    let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits);
    assert_eq!(CommandStatus::Success, admin.write_hotp_slot(slot_data, 0));
}

fn check_hotp_codes<T: Device>(device: &T)
where
    T: Sized,
{
    for code in HOTP_CODES {
        let result = device.get_hotp_code(1);
        assert_eq!(code, &result.unwrap());
    }
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn hotp() {
    set_debug(false);
    let device = ::connect().unwrap();
    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
    let config = Config::new(None, None, None, false);
    assert_eq!(CommandStatus::Success, admin.write_config(config));

    configure_hotp(&admin);
    check_hotp_codes(&admin);

    configure_hotp(&admin);
    check_hotp_codes(&admin.device());
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn hotp_pin() {
    set_debug(false);
    let device = ::connect().unwrap();
    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
    let config = Config::new(None, None, None, true);
    assert_eq!(CommandStatus::Success, admin.write_config(config));

    configure_hotp(&admin);
    let user = admin.device().authenticate_user(USER_PASSWORD).unwrap();
    check_hotp_codes(&user);

    // TODO: enable for newer libnitrokey
    // assert!(user.device().get_hotp_code(1).is_err());
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn hotp_slot_name() {
    set_debug(false);
    let device = ::connect().unwrap();

    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
    let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits);
    assert_eq!(CommandStatus::Success, admin.write_hotp_slot(slot_data, 0));

    let result = admin.device().get_hotp_slot_name(1);
    assert_eq!("test-hotp", result.unwrap());
}

fn configure_totp(admin: &AdminAuthenticatedDevice) {
    let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits);
    assert_eq!(CommandStatus::Success, admin.write_totp_slot(slot_data, 30));
}

fn check_totp_codes<T: Device>(device: &T)
where
    T: Sized,
{
    for (i, &(time, code)) in TOTP_CODES.iter().enumerate() {
        assert_eq!(CommandStatus::Success, device.set_time(time));
        let result = device.get_totp_code(1);
        assert!(result.is_ok());
        let result_code = result.unwrap();
        assert_eq!(
            code, result_code,
            "TOTP code {} should be {} but is {}",
            i, code, result_code
        );
    }
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn totp() {
    // TODO: this test may fail due to bad timing --> find solution
    set_debug(false);
    let device = ::connect().unwrap();
    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
    let config = Config::new(None, None, None, false);
    assert_eq!(CommandStatus::Success, admin.write_config(config));

    configure_totp(&admin);
    check_totp_codes(&admin);

    configure_totp(&admin);
    check_totp_codes(&admin.device());
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn totp_pin() {
    // TODO: this test may fail due to bad timing --> find solution
    set_debug(false);
    let device = ::connect().unwrap();
    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
    let config = Config::new(None, None, None, true);
    assert_eq!(CommandStatus::Success, admin.write_config(config));

    configure_totp(&admin);
    let user = admin.device().authenticate_user(USER_PASSWORD).unwrap();
    check_totp_codes(&user);

    // TODO: enable for newer libnitrokey
    // assert!(user.device().get_totp_code(1).is_err());
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn totp_slot_name() {
    set_debug(false);
    let device = ::connect().unwrap();

    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
    let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits);
    assert_eq!(CommandStatus::Success, admin.write_totp_slot(slot_data, 0));

    let result = admin.device().get_totp_slot_name(1);
    assert!(result.is_ok());
    assert_eq!("test-totp", result.unwrap());
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn get_major_firmware_version() {
    set_debug(false);
    // TODO fix for different libnitrokey versions
    assert_eq!(8, ::connect().unwrap().get_major_firmware_version());
}

fn admin_retry(device: UnauthenticatedDevice, suffix: &str, count: u8) -> UnauthenticatedDevice {
    let result = device.authenticate_admin(&(ADMIN_PASSWORD.to_owned() + suffix));
    let device = match result {
        Ok(admin) => admin.device(),
        Err((device, _)) => device,
    };
    assert_eq!(count, device.get_admin_retry_count());
    return device;
}

fn user_retry(device: UnauthenticatedDevice, suffix: &str, count: u8) -> UnauthenticatedDevice {
    let result = device.authenticate_user(&(USER_PASSWORD.to_owned() + suffix));
    let device = match result {
        Ok(admin) => admin.device(),
        Err((device, _)) => device,
    };
    assert_eq!(count, device.get_user_retry_count());
    return device;
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn get_retry_count() {
    set_debug(false);
    let device = ::connect().unwrap();

    let device = admin_retry(device, "", 3);
    let device = admin_retry(device, "123", 2);
    let device = admin_retry(device, "456", 1);
    let device = admin_retry(device, "", 3);

    let device = user_retry(device, "", 3);
    let device = user_retry(device, "123", 2);
    let device = user_retry(device, "456", 1);
    user_retry(device, "", 3);
}

#[test]
#[cfg_attr(not(feature = "test-pro"), ignore)]
fn read_write_config() {
    set_debug(false);
    let device = ::connect().unwrap();

    let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();

    let config = Config::new(None, None, None, true);
    assert_eq!(CommandStatus::Success, admin.write_config(config));
    let get_config = admin.get_config().unwrap();
    assert_eq!(config, get_config);

    let config = Config::new(None, Some(9), None, true);
    assert_eq!(
        CommandStatus::Error(CommandError::InvalidSlot),
        admin.write_config(config)
    );

    let config = Config::new(Some(1), None, Some(0), false);
    assert_eq!(CommandStatus::Success, admin.write_config(config));
    let get_config = admin.get_config().unwrap();
    assert_eq!(config, get_config);

    let config = Config::new(None, None, None, false);
    assert_eq!(CommandStatus::Success, admin.write_config(config));
    let get_config = admin.get_config().unwrap();
    assert_eq!(config, get_config);
}