diff options
-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, + } + } +} |