diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 100 | ||||
| -rw-r--r-- | src/device.rs | 711 | ||||
| -rw-r--r-- | src/lib.rs | 1303 | ||||
| -rw-r--r-- | src/otp.rs | 350 | ||||
| -rw-r--r-- | src/tests/pro.rs | 19 | ||||
| -rw-r--r-- | src/util.rs | 154 | 
6 files changed, 1334 insertions, 1303 deletions
| diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..b37c9d3 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,100 @@ +use util::CommandError; + +/// 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`]: trait.ProvideOtp.html#method.get_hotp_code +    /// [`get_totp_code`]: trait.ProvideOtp.html#method.get_totp_code +    pub user_password: bool, +} + +#[derive(Debug)] +pub struct RawConfig { +    pub numlock: u8, +    pub capslock: u8, +    pub scrollock: u8, +    pub user_password: bool, +} + +fn config_otp_slot_to_option(value: u8) -> Option<u8> { +    if value < 3 { +        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 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 { +    pub 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, +        } +    } +} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..6c1a957 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,711 @@ +use config::{Config, RawConfig}; +use libc; +use nitrokey_sys; +use std::ffi::CString; +use std::os::raw::c_int; +use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData, RawOtpSlotData}; +use util::{generate_password, get_last_error, result_from_string, CommandError, CommandStatus}; + +static TEMPORARY_PASSWORD_LENGTH: usize = 25; + +/// Available Nitrokey models. +#[derive(Debug, PartialEq)] +pub enum Model { +    /// The Nitrokey Storage. +    Storage, +    /// The Nitrokey Pro. +    Pro, +} + +/// 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 +#[derive(Debug)] +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 +#[derive(Debug)] +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 +#[derive(Debug)] +pub struct AdminAuthenticatedDevice { +    device: UnauthenticatedDevice, +    temp_password: Vec<u8>, +} + +/// A Nitrokey device. +/// +/// This trait provides the commands that can be executed without +/// authentication. +pub trait Device { +    /// 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`]: trait.ProvideOtp.html#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 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). +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::Device; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), CommandError> { +    /// let device = nitrokey::connect()?; +    /// println!( +    ///     "Firmware version: {}.{}", +    ///     device.get_major_firmware_version(), +    ///     device.get_minor_firmware_version(), +    /// ); +    /// #     Ok(()) +    /// # } +    /// ``` +    fn get_major_firmware_version(&self) -> i32 { +        unsafe { nitrokey_sys::NK_get_major_firmware_version() } +    } + +    /// Returns the minor part of the firmware version (for example 8 for +    /// version 0.8). +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::Device; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), CommandError> { +    /// let device = nitrokey::connect()?; +    /// println!( +    ///     "Firmware version: {}.{}", +    ///     device.get_major_firmware_version(), +    ///     device.get_minor_firmware_version(), +    /// ); +    /// #     Ok(()) +    /// # } +    fn get_minor_firmware_version(&self) -> i32 { +        unsafe { nitrokey_sys::NK_get_minor_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()); +        } +    } + +    /// Changes the administrator PIN. +    /// +    /// # Errors +    /// +    /// - [`InvalidString`][] if one of the provided passwords contains a null byte +    /// - [`WrongPassword`][] if the current admin password is wrong +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandStatus, Device}; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), CommandError> { +    /// let device = nitrokey::connect()?; +    /// match device.change_admin_pin("12345678", "12345679") { +    ///     CommandStatus::Success => println!("Updated admin PIN."), +    ///     CommandStatus::Error(err) => println!("Failed to update admin PIN: {:?}", err), +    /// }; +    /// #     Ok(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString +    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword +    fn change_admin_pin(&self, current: &str, new: &str) -> CommandStatus { +        let current_string = CString::new(current); +        let new_string = CString::new(new); +        if current_string.is_err() || new_string.is_err() { +            return CommandStatus::Error(CommandError::InvalidString); +        } +        let current_string = current_string.unwrap(); +        let new_string = new_string.unwrap(); +        unsafe { +            CommandStatus::from(nitrokey_sys::NK_change_admin_PIN( +                current_string.as_ptr(), +                new_string.as_ptr(), +            )) +        } +    } + +    /// Changes the user PIN. +    /// +    /// # Errors +    /// +    /// - [`InvalidString`][] if one of the provided passwords contains a null byte +    /// - [`WrongPassword`][] if the current user password is wrong +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandStatus, Device}; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), CommandError> { +    /// let device = nitrokey::connect()?; +    /// match device.change_user_pin("123456", "123457") { +    ///     CommandStatus::Success => println!("Updated admin PIN."), +    ///     CommandStatus::Error(err) => println!("Failed to update admin PIN: {:?}", err), +    /// }; +    /// #     Ok(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString +    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword +    fn change_user_pin(&self, current: &str, new: &str) -> CommandStatus { +        let current_string = CString::new(current); +        let new_string = CString::new(new); +        if current_string.is_err() || new_string.is_err() { +            return CommandStatus::Error(CommandError::InvalidString); +        } +        let current_string = current_string.unwrap(); +        let new_string = new_string.unwrap(); +        unsafe { +            CommandStatus::from(nitrokey_sys::NK_change_user_PIN( +                current_string.as_ptr(), +                new_string.as_ptr(), +            )) +        } +    } +} + +trait AuthenticatedDevice { +    fn new(device: UnauthenticatedDevice, temp_password: Vec<u8>) -> Self; +} + +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 Drop for UnauthenticatedDevice { +    fn drop(&mut self) { +        unsafe { +            nitrokey_sys::NK_logout(); +        } +    } +} + +impl Device for UnauthenticatedDevice {} + +impl GenerateOtp 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 {} + +impl GenerateOtp 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, GenerateOtp}; +    /// # 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, GenerateOtp}; +    /// # 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); +    } +} + +impl Device for AdminAuthenticatedDevice {} + +impl ConfigureOtp for AdminAuthenticatedDevice { +    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, +            ) +        }); +    } + +    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, +            ) +        }); +    } + +    fn erase_hotp_slot(&self, slot: u8) -> CommandStatus { +        let temp_password_ptr = self.temp_password.as_ptr() as *const i8; +        unsafe { CommandStatus::from(nitrokey_sys::NK_erase_hotp_slot(slot, temp_password_ptr)) } +    } + +    fn erase_totp_slot(&self, slot: u8) -> CommandStatus { +        let temp_password_ptr = self.temp_password.as_ptr() as *const i8; +        unsafe { CommandStatus::from(nitrokey_sys::NK_erase_totp_slot(slot, temp_password_ptr)) } +    } +} + +impl GenerateOtp for AdminAuthenticatedDevice {} + +impl AuthenticatedDevice for AdminAuthenticatedDevice { +    fn new(device: UnauthenticatedDevice, temp_password: Vec<u8>) -> Self { +        AdminAuthenticatedDevice { +            device, +            temp_password, +        } +    } +} @@ -42,7 +42,7 @@  //! Configure an HOTP slot:  //!  //! ```no_run -//! use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; +//! use nitrokey::{CommandStatus, ConfigureOtp, Device, OtpMode, OtpSlotData};  //! # use nitrokey::CommandError;  //!  //! # fn try_main() -> Result<(), (CommandError)> { @@ -64,7 +64,7 @@  //! Generate an HOTP one-time password:  //!  //! ```no_run -//! use nitrokey::Device; +//! use nitrokey::{Device, GenerateOtp};  //! # use nitrokey::CommandError;  //!  //! # fn try_main() -> Result<(), (CommandError)> { @@ -82,8 +82,8 @@  //! [`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 +//! [`get_hotp_code`]: trait.ProvideOtp.html#method.get_hotp_code +//! [`get_totp_code`]: trait.ProvideOtp.html#method.get_totp_code  //! [`set_debug`]: fn.set_debug.html  //! [`set_log_level`]: fn.set_log_level.html  //! [`AdminAuthenticatedDevice`]: struct.AdminAuthenticatedDevice.html @@ -94,611 +94,18 @@ 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; - +mod config; +mod device; +mod otp; +mod util;  #[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, -} - -/// 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 -#[derive(Debug)] -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 -#[derive(Debug)] -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 -#[derive(Debug)] -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 { -    /// 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 -    /// -    /// - [`InvalidSlot`][] if there is no slot with the given number -    /// - [`SlotNotProgrammed`][] if the given slot is not configured -    /// -    /// # 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(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed -    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 -    /// -    /// - [`InvalidSlot`][] if there is no slot with the given number -    /// - [`SlotNotProgrammed`][] if the given slot is not configured -    /// -    /// # 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(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed -    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). -    /// -    /// # Example -    /// -    /// ```no_run -    /// use nitrokey::Device; -    /// # use nitrokey::CommandError; -    /// -    /// # fn try_main() -> Result<(), CommandError> { -    /// let device = nitrokey::connect()?; -    /// println!( -    ///     "Firmware version: {}.{}", -    ///     device.get_major_firmware_version(), -    ///     device.get_minor_firmware_version(), -    /// ); -    /// #     Ok(()) -    /// # } -    /// ``` -    fn get_major_firmware_version(&self) -> i32 { -        unsafe { nitrokey_sys::NK_get_major_firmware_version() } -    } - -    /// Returns the minor part of the firmware version (for example 8 for -    /// version 0.8). -    /// -    /// # Example -    /// -    /// ```no_run -    /// use nitrokey::Device; -    /// # use nitrokey::CommandError; -    /// -    /// # fn try_main() -> Result<(), CommandError> { -    /// let device = nitrokey::connect()?; -    /// println!( -    ///     "Firmware version: {}.{}", -    ///     device.get_major_firmware_version(), -    ///     device.get_minor_firmware_version(), -    /// ); -    /// #     Ok(()) -    /// # } -    fn get_minor_firmware_version(&self) -> i32 { -        unsafe { nitrokey_sys::NK_get_minor_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 -    /// -    /// - [`InvalidSlot`][] if there is no slot with the given number -    /// - [`NotAuthorized`][] if OTP generation requires user authentication -    /// - [`SlotNotProgrammed`][] if the given slot is not configured -    /// -    /// # 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 -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized -    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed -    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 -    /// -    /// - [`InvalidSlot`][] if there is no slot with the given number -    /// - [`NotAuthorized`][] if OTP generation requires user authentication -    /// - [`SlotNotProgrammed`][] if the given slot is not configured -    /// -    /// # 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 -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized -    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed -    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)); -        } -    } - -    /// Changes the administrator PIN. -    /// -    /// # Errors -    /// -    /// - [`InvalidString`][] if one of the provided passwords contains a null byte -    /// - [`WrongPassword`][] if the current admin password is wrong -    /// -    /// # Example -    /// -    /// ```no_run -    /// use nitrokey::{CommandStatus, Device}; -    /// # use nitrokey::CommandError; -    /// -    /// # fn try_main() -> Result<(), CommandError> { -    /// let device = nitrokey::connect()?; -    /// match device.change_admin_pin("12345678", "12345679") { -    ///     CommandStatus::Success => println!("Updated admin PIN."), -    ///     CommandStatus::Error(err) => println!("Failed to update admin PIN: {:?}", err), -    /// }; -    /// #     Ok(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString -    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword -    fn change_admin_pin(&self, current: &str, new: &str) -> CommandStatus { -        let current_string = CString::new(current); -        let new_string = CString::new(new); -        if current_string.is_err() || new_string.is_err() { -            return CommandStatus::Error(CommandError::InvalidString); -        } -        let current_string = current_string.unwrap(); -        let new_string = new_string.unwrap(); -        unsafe { -            CommandStatus::from(nitrokey_sys::NK_change_admin_PIN( -                current_string.as_ptr(), -                new_string.as_ptr(), -            )) -        } -    } - -    /// Changes the user PIN. -    /// -    /// # Errors -    /// -    /// - [`InvalidString`][] if one of the provided passwords contains a null byte -    /// - [`WrongPassword`][] if the current user password is wrong -    /// -    /// # Example -    /// -    /// ```no_run -    /// use nitrokey::{CommandStatus, Device}; -    /// # use nitrokey::CommandError; -    /// -    /// # fn try_main() -> Result<(), CommandError> { -    /// let device = nitrokey::connect()?; -    /// match device.change_user_pin("123456", "123457") { -    ///     CommandStatus::Success => println!("Updated admin PIN."), -    ///     CommandStatus::Error(err) => println!("Failed to update admin PIN: {:?}", err), -    /// }; -    /// #     Ok(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString -    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword -    fn change_user_pin(&self, current: &str, new: &str) -> CommandStatus { -        let current_string = CString::new(current); -        let new_string = CString::new(new); -        if current_string.is_err() || new_string.is_err() { -            return CommandStatus::Error(CommandError::InvalidString); -        } -        let current_string = current_string.unwrap(); -        let new_string = new_string.unwrap(); -        unsafe { -            CommandStatus::from(nitrokey_sys::NK_change_user_PIN( -                current_string.as_ptr(), -                new_string.as_ptr(), -            )) -        } -    } -} - -trait AuthenticatedDevice { -    fn new(device: UnauthenticatedDevice, temp_password: Vec<u8>) -> Self; -} +pub use config::Config; +pub use device::{AdminAuthenticatedDevice, Device, Model, UnauthenticatedDevice, +                 UserAuthenticatedDevice}; +pub use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; +pub use util::{CommandError, CommandStatus, LogLevel};  /// Connects to a Nitrokey device.  This method can be used to connect to any  /// connected device, both a Nitrokey Pro and a Nitrokey Storage. @@ -772,687 +179,3 @@ pub fn set_log_level(level: LogLevel) {          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, -            201 => CommandError::InvalidSlot, -            _ => 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); -        if s.is_empty() { -            return Err(get_last_error()); -        } -        // TODO: move up for newer libnitrokey versions -        libc::free(ptr as *mut libc::c_void); -        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 Drop for UnauthenticatedDevice { -    fn drop(&mut self) { -        unsafe { -            nitrokey_sys::NK_logout(); -        } -    } -} - -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 -    /// -    /// - [`InvalidSlot`][] if there is no slot with the given number -    /// - [`InvalidString`][] if the provided token ID contains a null byte -    /// - [`NoName`][] if the provided name is empty -    /// -    /// # 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(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString -    /// [`NoName`]: enum.CommandError.html#variant.NoName -    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 -    /// -    /// - [`InvalidSlot`][] if there is no slot with the given number -    /// - [`InvalidString`][] if the provided token ID contains a null byte -    /// - [`NoName`][] if the provided name is empty -    /// -    /// # 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(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString -    /// [`NoName`]: enum.CommandError.html#variant.NoName -    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, -            ) -        }); -    } - -    /// Erase an HOTP slot. -    /// -    /// # Errors -    /// -    /// - [`InvalidSlot`][] 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()?; -    /// match device.authenticate_admin("12345678") { -    ///     Ok(admin) => { -    ///         match admin.erase_hotp_slot(1) { -    ///             CommandStatus::Success => println!("Successfully erased slot."), -    ///             CommandStatus::Error(err) => println!("Could not erase slot: {:?}", err), -    ///         } -    ///     }, -    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), -    /// } -    /// #     Ok(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    pub fn erase_hotp_slot(&self, slot: u8) -> CommandStatus { -        let temp_password_ptr = self.temp_password.as_ptr() as *const i8; -        unsafe { CommandStatus::from(nitrokey_sys::NK_erase_hotp_slot(slot, temp_password_ptr)) } -    } - -    /// Erase a TOTP slot. -    /// -    /// # Errors -    /// -    /// - [`InvalidSlot`][] 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()?; -    /// match device.authenticate_admin("12345678") { -    ///     Ok(admin) => { -    ///         match admin.erase_totp_slot(1) { -    ///             CommandStatus::Success => println!("Successfully erased slot."), -    ///             CommandStatus::Error(err) => println!("Could not erase slot: {:?}", err), -    ///         } -    ///     }, -    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), -    /// } -    /// #     Ok(()) -    /// # } -    /// ``` -    /// -    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot -    pub fn erase_totp_slot(&self, slot: u8) -> CommandStatus { -        let temp_password_ptr = self.temp_password.as_ptr() as *const i8; -        unsafe { CommandStatus::from(nitrokey_sys::NK_erase_totp_slot(slot, 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/otp.rs b/src/otp.rs new file mode 100644 index 0000000..c0a470f --- /dev/null +++ b/src/otp.rs @@ -0,0 +1,350 @@ +use nitrokey_sys; +use std::ffi::CString; +use util::{result_from_string, CommandError, CommandStatus}; + +/// 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, +} + +/// Provides methods to configure and erase OTP slots on a Nitrokey device. +pub trait ConfigureOtp { +    /// Configure an HOTP slot with the given data and set the HOTP counter to +    /// the given value (default 0). +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// - [`InvalidString`][] if the provided token ID contains a null byte +    /// - [`NoName`][] if the provided name is empty +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandStatus, ConfigureOtp, 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(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString +    /// [`NoName`]: enum.CommandError.html#variant.NoName +    fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> CommandStatus; + +    /// Configure a TOTP slot with the given data and set the TOTP time window +    /// to the given value (default 30). +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// - [`InvalidString`][] if the provided token ID contains a null byte +    /// - [`NoName`][] if the provided name is empty +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandStatus, ConfigureOtp, 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(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString +    /// [`NoName`]: enum.CommandError.html#variant.NoName +    fn write_totp_slot(&self, data: OtpSlotData, time_window: u16) -> CommandStatus; + +    /// Erases an HOTP slot. +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandStatus, ConfigureOtp}; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), (CommandError)> { +    /// let device = nitrokey::connect()?; +    /// match device.authenticate_admin("12345678") { +    ///     Ok(admin) => { +    ///         match admin.erase_hotp_slot(1) { +    ///             CommandStatus::Success => println!("Successfully erased slot."), +    ///             CommandStatus::Error(err) => println!("Could not erase slot: {:?}", err), +    ///         } +    ///     }, +    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), +    /// } +    /// #     Ok(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    fn erase_hotp_slot(&self, slot: u8) -> CommandStatus; + +    /// Erases a TOTP slot. +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandStatus, ConfigureOtp}; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), (CommandError)> { +    /// let device = nitrokey::connect()?; +    /// match device.authenticate_admin("12345678") { +    ///     Ok(admin) => { +    ///         match admin.erase_totp_slot(1) { +    ///             CommandStatus::Success => println!("Successfully erased slot."), +    ///             CommandStatus::Error(err) => println!("Could not erase slot: {:?}", err), +    ///         } +    ///     }, +    ///     Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), +    /// } +    /// #     Ok(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    fn erase_totp_slot(&self, slot: u8) -> CommandStatus; +} + +/// Provides methods to generate OTP codes and to query OTP slots on a Nitrokey +/// device. +pub trait GenerateOtp { +    /// Returns the name of the given HOTP slot. +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// - [`SlotNotProgrammed`][] if the given slot is not configured +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandError, GenerateOtp}; +    /// +    /// # 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(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed +    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 +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// - [`SlotNotProgrammed`][] if the given slot is not configured +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{CommandError, GenerateOtp}; +    /// +    /// # 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(()) +    /// # } +    /// ``` +    /// +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed +    fn get_totp_slot_name(&self, slot: u8) -> Result<String, CommandError> { +        unsafe { result_from_string(nitrokey_sys::NK_get_totp_slot_name(slot)) } +    } + +    /// Generates an HOTP code on the given slot.  This operation may require +    /// user authorization, depending on the device configuration (see +    /// [`get_config`][]). +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// - [`NotAuthorized`][] if OTP generation requires user authentication +    /// - [`SlotNotProgrammed`][] if the given slot is not configured +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::GenerateOtp; +    /// # 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`]: trait.Device.html#method.get_config +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized +    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed +    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 +    /// +    /// - [`InvalidSlot`][] if there is no slot with the given number +    /// - [`NotAuthorized`][] if OTP generation requires user authentication +    /// - [`SlotNotProgrammed`][] if the given slot is not configured +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::GenerateOtp; +    /// # 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`]: trait.Device.html#method.set_time +    /// [`get_config`]: trait.Device.html#method.get_config +    /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot +    /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized +    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed +    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)); +        } +    } +} + +/// 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)] +pub 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, +} + +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(), +        }) +    } +} diff --git a/src/tests/pro.rs b/src/tests/pro.rs index e39c95a..ece94bf 100644 --- a/src/tests/pro.rs +++ b/src/tests/pro.rs @@ -1,7 +1,6 @@  use std::ffi::CStr; -use std::marker::Sized; -use {set_debug, AdminAuthenticatedDevice, CommandError, CommandStatus, Config, Device, Model, -     OtpMode, OtpSlotData, UnauthenticatedDevice}; +use {set_debug, AdminAuthenticatedDevice, CommandError, CommandStatus, Config, ConfigureOtp, +     Device, GenerateOtp, Model, OtpMode, OtpSlotData, UnauthenticatedDevice};  static ADMIN_PASSWORD: &str = "12345678";  static ADMIN_NEW_PASSWORD: &str = "1234567890"; @@ -84,15 +83,12 @@ fn get_serial_number() {      assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit()));  } -fn configure_hotp(admin: &AdminAuthenticatedDevice) { +fn configure_hotp(admin: &ConfigureOtp) {      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, -{ +fn check_hotp_codes<T: GenerateOtp>(device: &T) {      for code in HOTP_CODES {          let result = device.get_hotp_code(1);          assert_eq!(code, &result.unwrap()); @@ -181,15 +177,12 @@ fn hotp_erase() {      assert_eq!("test2", device.get_hotp_slot_name(2).unwrap());  } -fn configure_totp(admin: &AdminAuthenticatedDevice) { +fn configure_totp(admin: &ConfigureOtp) {      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, -{ +fn check_totp_codes<T: Device + GenerateOtp>(device: &T) {      for (i, &(time, code)) in TOTP_CODES.iter().enumerate() {          assert_eq!(CommandStatus::Success, device.set_time(time));          let result = device.get_totp_code(1); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..8a6c411 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,154 @@ +use libc::{c_void, free}; +use nitrokey_sys; +use rand::{OsRng, Rng}; +use std; +use std::ffi::CStr; +use std::os::raw::{c_char, c_int}; + +/// 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, +} + +pub fn owned_str_from_ptr(ptr: *const c_char) -> String { +    unsafe { +        return CStr::from_ptr(ptr).to_string_lossy().into_owned(); +    } +} + +pub fn result_from_string(ptr: *const c_char) -> Result<String, CommandError> { +    if ptr.is_null() { +        return Err(CommandError::Unknown); +    } +    unsafe { +        let s = owned_str_from_ptr(ptr); +        if s.is_empty() { +            return Err(get_last_error()); +        } +        // TODO: move up for newer libnitrokey versions +        free(ptr as *mut c_void); +        return Ok(s); +    } +} + +pub fn get_last_status() -> CommandStatus { +    unsafe { +        let status = nitrokey_sys::NK_get_last_command_status(); +        return CommandStatus::from(status as c_int); +    } +} + +pub fn get_last_error() -> CommandError { +    return match get_last_status() { +        CommandStatus::Success => CommandError::Unknown, +        CommandStatus::Error(err) => err, +    }; +} + +pub fn generate_password(length: usize) -> std::io::Result<Vec<u8>> { +    let mut rng = match 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 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, +            201 => CommandError::InvalidSlot, +            _ => 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, +        } +    } +} | 
