diff options
| author | Robin Krahl <robin.krahl@ireas.org> | 2018-05-19 19:09:42 +0000 | 
|---|---|---|
| committer | Robin Krahl <me@robin-krahl.de> | 2018-05-19 21:10:43 +0200 | 
| commit | a8517d9707e5ef313d6f4d69b51d21251c82ea91 (patch) | |
| tree | 6769c8a1bfcdd2e0abcafdc95e8940f9212208ae /src | |
| download | nitrokey-rs-a8517d9707e5ef313d6f4d69b51d21251c82ea91.tar.gz nitrokey-rs-a8517d9707e5ef313d6f4d69b51d21251c82ea91.tar.bz2 | |
Initial commit
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 1303 | ||||
| -rw-r--r-- | src/tests/mod.rs | 2 | ||||
| -rw-r--r-- | src/tests/no_device.rs | 9 | ||||
| -rw-r--r-- | src/tests/pro.rs | 268 | 
4 files changed, 1582 insertions, 0 deletions
| diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fdc7954 --- /dev/null +++ b/src/lib.rs @@ -0,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, +        } +    } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..5f01b67 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod no_device; +mod pro; diff --git a/src/tests/no_device.rs b/src/tests/no_device.rs new file mode 100644 index 0000000..04c5c8a --- /dev/null +++ b/src/tests/no_device.rs @@ -0,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()); +} diff --git a/src/tests/pro.rs b/src/tests/pro.rs new file mode 100644 index 0000000..7fa8ec3 --- /dev/null +++ b/src/tests/pro.rs @@ -0,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); +} | 
