//! Provides access to a Nitrokey device using the native libnitrokey API. //! //! # Usage //! //! Operations on the Nitrokey require different authentication levels. Some //! operations can be performed without authentication, some require user //! access, and some require admin access. This is modelled using the types //! [`UnauthenticatedDevice`][], [`UserAuthenticatedDevice`][] and //! [`AdminAuthenticatedDevice`][]. //! //! Use [`connect`][] or [`connect_model`][] to obtain an //! [`UnauthenticatedDevice`][]. You can then use [`authenticate_user`][] or //! [`authenticate_admin`][] to get an authenticated device. You can then use //! [`device`][] to go back to the unauthenticated device. //! //! This makes sure that you can only execute a command if you have the //! required access rights. Otherwise, your code will not compile. The only //! exception are the methods to generate one-time passwords – //! [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick //! configuration, these operations are available without authentication or //! with user authentication. //! //! Per default, libnitrokey writes log messages, for example the packets that //! are sent to and received from the stick, to the standard output. To //! change this behaviour, use [`set_debug`][] or [`set_log_level`][]. //! //! # Examples //! //! Connect to any Nitrokey and print its serial number: //! //! ```no_run //! use nitrokey::Device; //! # use nitrokey::CommandError; //! //! # fn try_main() -> Result<(), CommandError> { //! let device = nitrokey::connect()?; //! println!("{}", device.get_serial_number()?); //! # Ok(()) //! # } //! ``` //! //! Configure an HOTP slot: //! //! ```no_run //! use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; //! # use nitrokey::CommandError; //! //! # fn try_main() -> Result<(), (CommandError)> { //! let device = nitrokey::connect()?; //! let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); //! match device.authenticate_admin("12345678") { //! Ok(admin) => { //! match admin.write_hotp_slot(slot_data, 0) { //! CommandStatus::Success => println!("Successfully wrote slot."), //! CommandStatus::Error(err) => println!("Could not write slot: {:?}", err), //! } //! }, //! Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), //! } //! # Ok(()) //! # } //! ``` //! //! Generate an HOTP one-time password: //! //! ```no_run //! use nitrokey::Device; //! # use nitrokey::CommandError; //! //! # fn try_main() -> Result<(), (CommandError)> { //! let device = nitrokey::connect()?; //! match device.get_hotp_code(1) { //! Ok(code) => println!("Generated HOTP code: {:?}", code), //! Err(err) => println!("Could not generate HOTP code: {:?}", err), //! } //! # Ok(()) //! # } //! ``` //! //! [`authenticate_admin`]: struct.UnauthenticatedDevice.html#method.authenticate_admin //! [`authenticate_user`]: struct.UnauthenticatedDevice.html#method.authenticate_user //! [`connect`]: fn.connect.html //! [`connect_model`]: fn.connect_model.html //! [`device`]: struct.AuthenticatedDevice.html#method.device //! [`get_hotp_code`]: trait.Device.html#method.get_hotp_code //! [`get_totp_code`]: trait.Device.html#method.get_totp_code //! [`set_debug`]: fn.set_debug.html //! [`set_log_level`]: fn.set_log_level.html //! [`AdminAuthenticatedDevice`]: struct.AdminAuthenticatedDevice.html //! [`UserAuthenticatedDevice`]: struct.UserAuthenticatedDevice.html //! [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html extern crate libc; extern crate nitrokey_sys; extern crate rand; use std::ffi::CString; use std::ffi::CStr; use libc::c_int; use rand::Rng; #[cfg(test)] mod tests; /// Modes for one-time password generation. #[derive(Debug, PartialEq)] pub enum OtpMode { /// Generate one-time passwords with six digits. SixDigits, /// Generate one-time passwords with eight digits. EightDigits, } /// Error types returned by Nitrokey device or by the library. #[derive(Debug, PartialEq)] pub enum CommandError { /// A packet with a wrong checksum has been sent or received. WrongCrc, /// A command tried to access an OTP slot that does not exist. WrongSlot, /// A command tried to generate an OTP on a slot that is not configured. SlotNotProgrammed, /// The provided password is wrong. WrongPassword, /// You are not authorized for this command or provided a wrong temporary /// password. NotAuthorized, /// An error occured when getting or setting the time. Timestamp, /// You did not provide a name for the OTP slot. NoName, /// This command is not supported by this device. NotSupported, /// This command is unknown. UnknownCommand, /// AES decryptionfailed. AesDecryptionFailed, /// An unknown error occured. Unknown, /// You passed a string containing a null byte. InvalidString, /// You passed an invalid slot. InvalidSlot, /// An error occured during random number generation. RngError, } /// Command execution status. #[derive(Debug, PartialEq)] pub enum CommandStatus { /// The command was successful. Success, /// An error occured during command execution. Error(CommandError), } /// Log level for libnitrokey. #[derive(Debug, PartialEq)] pub enum LogLevel { /// Only log error messages. Error, /// Log error messages and warnings. Warning, /// Log error messages, warnings and info messages. Info, /// Log error messages, warnings, info messages and debug messages. DebugL1, /// Log error messages, warnings, info messages and detailed debug /// messages. Debug, /// Log error messages, warnings, info messages and very detailed debug /// messages. DebugL2, } /// Available Nitrokey models. #[derive(Debug, PartialEq)] pub enum Model { /// The Nitrokey Storage. Storage, /// The Nitrokey Pro. Pro, } /// The configuration for a Nitrokey. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Config { /// If set, the stick will generate a code from the HOTP slot with the /// given number if numlock is pressed. The slot number must be 0, 1 or 2. pub numlock: Option, /// 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, /// 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, /// If set, OTP generation using [`get_hotp_code`][] or [`get_totp_code`][] /// requires user authentication. Otherwise, OTPs can be generated without /// authentication. /// /// [`get_hotp_code`]: struct.Device.html#method.get_hotp_code /// [`get_totp_code`]: struct.Device.html#method.get_totp_code pub user_password: bool, } #[derive(Debug)] struct RawConfig { pub numlock: u8, pub capslock: u8, pub scrollock: u8, pub user_password: bool, } #[derive(Debug)] /// A Nitrokey device without user or admin authentication. /// /// Use [`connect`][] or [`connect_model`][] to obtain an instance. If you /// want to execute a command that requires user or admin authentication, /// use [`authenticate_admin`][] or [`authenticate_user`][]. /// /// # Examples /// /// Authentication with error handling: /// /// ```no_run /// use nitrokey::{UnauthenticatedDevice, UserAuthenticatedDevice}; /// # use nitrokey::CommandError; /// /// fn perform_user_task(device: &UserAuthenticatedDevice) {} /// fn perform_other_task(device: &UnauthenticatedDevice) {} /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// let device = match device.authenticate_user("123456") { /// Ok(user) => { /// perform_user_task(&user); /// user.device() /// }, /// Err((device, err)) => { /// println!("Could not authenticate as user: {:?}", err); /// device /// }, /// }; /// perform_other_task(&device); /// # Ok(()) /// # } /// ``` /// /// [`authenticate_admin`]: #method.authenticate_admin /// [`authenticate_user`]: #method.authenticate_user /// [`connect`]: fn.connect.html /// [`connect_model`]: fn.connect_model.html pub struct UnauthenticatedDevice {} /// A Nitrokey device with user authentication. /// /// To obtain an instance of this struct, use the [`authenticate_user`][] /// method on an [`UnauthenticatedDevice`][]. To get back to an /// unauthenticated device, use the [`device`][] method. /// /// [`authenticate_admin`]: struct.UnauthenticatedDevice#method.authenticate_admin /// [`device`]: #method.device /// [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html pub struct UserAuthenticatedDevice { device: UnauthenticatedDevice, temp_password: Vec, } /// A Nitrokey device with admin authentication. /// /// To obtain an instance of this struct, use the [`authenticate_admin`][] /// method on an [`UnauthenticatedDevice`][]. To get back to an /// unauthenticated device, use the [`device`][] method. /// /// [`authenticate_admin`]: struct.UnauthenticatedDevice#method.authenticate_admin /// [`device`]: #method.device /// [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html pub struct AdminAuthenticatedDevice { device: UnauthenticatedDevice, temp_password: Vec, } /// 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, } #[derive(Debug)] struct RawOtpSlotData { pub number: u8, pub name: CString, pub secret: CString, pub mode: OtpMode, pub use_enter: bool, pub use_token_id: bool, pub token_id: CString, } static TEMPORARY_PASSWORD_LENGTH: usize = 25; /// A Nitrokey device. /// /// This trait provides the commands that can be executed without /// authentication. The only exception are the [`get_hotp_code`][] and /// [`get_totp_code`][] methods: It depends on the device configuration /// ([`get_config`][]) whether these commands require user authentication /// or not. /// /// [`get_config`]: #method.get_config /// [`get_hotp_code`]: #method.get_hotp_code /// [`get_totp_code`]: #method.get_totp_code pub trait Device { /// Closes the connection to this device. This method consumes the device. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// // perform tasks ... /// device.disconnect(); /// # Ok(()) /// # } /// ``` fn disconnect(self) where Self: std::marker::Sized, { unsafe { nitrokey_sys::NK_logout(); } } /// Sets the time on the Nitrokey. This command may set the time to /// arbitrary values. `time` is the number of seconds since January 1st, /// 1970 (Unix timestamp). /// /// The time is used for TOTP generation (see [`get_totp_code`][]). /// /// # Errors /// /// - [`Timestamp`][] if the time could not be set /// /// [`get_totp_code`]: #method.get_totp_code /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp // TODO: example fn set_time(&self, time: u64) -> CommandStatus { unsafe { CommandStatus::from(nitrokey_sys::NK_totp_set_time(time)) } } /// Returns the serial number of the Nitrokey device. The serial number /// is the string representation of a hex number. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// match device.get_serial_number() { /// Ok(number) => println!("serial no: {:?}", number), /// Err(err) => println!("Could not get serial number: {:?}", err), /// }; /// # Ok(()) /// # } /// ``` fn get_serial_number(&self) -> Result { unsafe { result_from_string(nitrokey_sys::NK_device_serial_number()) } } /// Returns the name of the given HOTP slot. /// /// # Errors /// /// - [`SlotNotProgrammed`][] if the given slot is not configured /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::{CommandError, Device}; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// match device.get_hotp_slot_name(1) { /// Ok(name) => println!("HOTP slot 1: {:?}", name), /// Err(CommandError::SlotNotProgrammed) => println!("HOTP slot 1 not programmed"), /// Err(err) => println!("Could not get slot name: {:?}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot fn get_hotp_slot_name(&self, slot: u8) -> Result { unsafe { result_from_string(nitrokey_sys::NK_get_hotp_slot_name(slot)) } } /// Returns the name of the given TOTP slot. /// /// # Errors /// /// - [`SlotNotProgrammed`][] if the given slot is not configured /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::{CommandError, Device}; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// match device.get_totp_slot_name(1) { /// Ok(name) => println!("TOTP slot 1: {:?}", name), /// Err(CommandError::SlotNotProgrammed) => println!("TOTP slot 1 not programmed"), /// Err(err) => println!("Could not get slot name: {:?}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot fn get_totp_slot_name(&self, slot: u8) -> Result { 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 { unsafe { let config_ptr = nitrokey_sys::NK_read_config(); if config_ptr.is_null() { return Err(get_last_error()); } let config_array_ptr = config_ptr as *const [u8; 5]; let raw_config = RawConfig::from(*config_array_ptr); libc::free(config_ptr as *mut libc::c_void); return Ok(raw_config.into()); } } /// Generates an HOTP code on the given slot. This operation may require /// user authorization, depending on the device configuration (see /// [`get_config`][]). /// /// # Errors /// /// - [`NotAuthorized`][] if OTP generation requires user authentication /// - [`SlotNotProgrammed`][] if the given slot is not configured /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// let code = device.get_hotp_code(1)?; /// println!("Generated HOTP code on slot 1: {:?}", code); /// # Ok(()) /// # } /// ``` /// /// [`get_config`]: #method.get_config /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot fn get_hotp_code(&self, slot: u8) -> Result { unsafe { return result_from_string(nitrokey_sys::NK_get_hotp_code(slot)); } } /// Generates a TOTP code on the given slot. This operation may require /// user authorization, depending on the device configuration (see /// [`get_config`][]). /// /// To make sure that the Nitrokey’s time is in sync, consider calling /// [`set_time`][] before calling this method. /// /// # Errors /// /// - [`NotAuthorized`][] if OTP generation requires user authentication /// - [`SlotNotProgrammed`][] if the given slot is not configured /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// let code = device.get_totp_code(1)?; /// println!("Generated TOTP code on slot 1: {:?}", code); /// # Ok(()) /// # } /// ``` /// /// [`set_time`]: #method.set_time /// [`get_config`]: #method.get_config /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot fn get_totp_code(&self, slot: u8) -> Result { unsafe { return result_from_string(nitrokey_sys::NK_get_totp_code(slot, 0, 0, 0)); } } } trait AuthenticatedDevice { fn new(device: UnauthenticatedDevice, temp_password: Vec) -> Self; } /// Connects to a Nitrokey device. This method can be used to connect to any /// connected device, both a Nitrokey Pro and a Nitrokey Storage. /// /// # Example /// /// ``` /// use nitrokey::UnauthenticatedDevice; /// /// fn do_something(device: UnauthenticatedDevice) {} /// /// match nitrokey::connect() { /// Ok(device) => do_something(device), /// Err(err) => println!("Could not connect to a Nitrokey: {:?}", err), /// } /// ``` pub fn connect() -> Result { unsafe { match nitrokey_sys::NK_login_auto() { 1 => Ok(UnauthenticatedDevice {}), _ => Err(CommandError::Unknown), } } } /// Connects to a Nitrokey device of the given model. /// /// # Example /// /// ``` /// use nitrokey::{Model, UnauthenticatedDevice}; /// /// fn do_something(device: UnauthenticatedDevice) {} /// /// match nitrokey::connect_model(Model::Pro) { /// Ok(device) => do_something(device), /// Err(err) => println!("Could not connect to a Nitrokey Pro: {:?}", err), /// } /// ``` pub fn connect_model(model: Model) -> Result { let model = match model { Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, }; unsafe { return match nitrokey_sys::NK_login_enum(model) { 1 => Ok(UnauthenticatedDevice {}), rv => Err(CommandError::from(rv)), }; } } /// Enables or disables debug output. Calling this method with `true` is /// equivalent to setting the log level to `Debug`; calling it with `false` is /// equivalent to the log level `Error` (see [`set_log_level`][]). /// /// If debug output is enabled, detailed information about the communication /// with the Nitrokey device is printed to the standard output. /// /// [`set_log_level`]: fn.set_log_level.html pub fn set_debug(state: bool) { unsafe { nitrokey_sys::NK_set_debug(state); } } /// Sets the log level for libnitrokey. All log messages are written to the /// standard output or standard errror. pub fn set_log_level(level: LogLevel) { unsafe { nitrokey_sys::NK_set_debug_level(level.into()); } } fn config_otp_slot_to_option(value: u8) -> Option { if value < 3 { return Some(value); } None } fn option_to_config_otp_slot(value: Option) -> Result { match value { Some(value) => { if value < 3 { Ok(value) } else { Err(CommandError::InvalidSlot) } } None => Ok(255), } } impl From for CommandError { fn from(value: c_int) -> Self { match value { 1 => CommandError::WrongCrc, 2 => CommandError::WrongSlot, 3 => CommandError::SlotNotProgrammed, 4 => CommandError::WrongPassword, 5 => CommandError::NotAuthorized, 6 => CommandError::Timestamp, 7 => CommandError::NoName, 8 => CommandError::NotSupported, 9 => CommandError::UnknownCommand, 10 => CommandError::AesDecryptionFailed, _ => CommandError::Unknown, } } } impl From for CommandStatus { fn from(value: c_int) -> Self { match value { 0 => CommandStatus::Success, other => CommandStatus::Error(CommandError::from(other)), } } } impl Into 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 { if ptr.is_null() { return Err(CommandError::Unknown); } unsafe { let s = owned_str_from_ptr(ptr); libc::free(ptr as *mut libc::c_void); if s.is_empty() { return Err(get_last_error()); } return Ok(s); } } fn generate_password(length: usize) -> std::io::Result> { 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 { 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, capslock: Option, scrollock: Option, user_password: bool, ) -> Config { Config { numlock, capslock, scrollock, user_password, } } } impl RawConfig { fn try_from(config: Config) -> Result { 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 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( self, password: &str, callback: T, ) -> Result 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 { 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 { return self.authenticate(password, |password_ptr, temp_password_ptr| unsafe { nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr) }); } } impl Device for UnauthenticatedDevice {} impl UserAuthenticatedDevice { /// Forgets the user authentication and returns an unauthenticated /// device. This method consumes the authenticated device. It does not /// perform any actual commands on the Nitrokey. pub fn device(self) -> UnauthenticatedDevice { self.device } } impl Device for UserAuthenticatedDevice { /// Generates an HOTP code on the given slot. This operation may not /// require user authorization, depending on the device configuration (see /// [`get_config`][]). /// /// # Errors /// /// - [`SlotNotProgrammed`][] if the given slot is not configured /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), CommandError> { /// let device = nitrokey::connect()?; /// match device.authenticate_user("123456") { /// Ok(user) => { /// let code = user.get_hotp_code(1)?; /// println!("Generated HOTP code on slot 1: {:?}", code); /// }, /// Err(err) => println!("Could not authenticate: {:?}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`get_config`]: #method.get_config /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot fn get_hotp_code(&self, slot: u8) -> Result { 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 { 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) -> 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(&self, data: OtpSlotData, callback: T) -> CommandStatus where T: Fn(RawOtpSlotData, *const i8) -> c_int, { let raw_data = match RawOtpSlotData::new(data) { Ok(raw_data) => raw_data, Err(err) => return CommandStatus::Error(err), }; let temp_password_ptr = self.temp_password.as_ptr() as *const i8; let rv = callback(raw_data, temp_password_ptr); return CommandStatus::from(rv); } /// Configure an HOTP slot with the given data and set the HOTP counter to /// the given value (default 0). /// /// # Errors /// /// - [`InvalidString`][] if the provided token ID contains a null byte /// - [`NoName`][] if the provided name is empty /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), (CommandError)> { /// let device = nitrokey::connect()?; /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); /// match device.authenticate_admin("12345678") { /// Ok(admin) => { /// match admin.write_hotp_slot(slot_data, 0) { /// CommandStatus::Success => println!("Successfully wrote slot."), /// CommandStatus::Error(err) => println!("Could not write slot: {:?}", err), /// } /// }, /// Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), /// } /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`NoName`]: enum.CommandError.html#variant.NoName /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot pub fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> CommandStatus { return self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| unsafe { nitrokey_sys::NK_write_hotp_slot( raw_data.number, raw_data.name.as_ptr(), raw_data.secret.as_ptr(), counter, raw_data.mode == OtpMode::EightDigits, raw_data.use_enter, raw_data.use_token_id, raw_data.token_id.as_ptr(), temp_password_ptr, ) }); } /// Configure a TOTP slot with the given data and set the TOTP time window /// to the given value (default 30). /// /// # Errors /// /// - [`InvalidString`][] if the provided token ID contains a null byte /// - [`NoName`][] if the provided name is empty /// - [`WrongSlot`][] if there is no slot with the given number /// /// # Example /// /// ```no_run /// use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; /// # use nitrokey::CommandError; /// /// # fn try_main() -> Result<(), (CommandError)> { /// let device = nitrokey::connect()?; /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::EightDigits); /// match device.authenticate_admin("12345678") { /// Ok(admin) => { /// match admin.write_totp_slot(slot_data, 30) { /// CommandStatus::Success => println!("Successfully wrote slot."), /// CommandStatus::Error(err) => println!("Could not write slot: {:?}", err), /// } /// }, /// Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), /// } /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString /// [`NoName`]: enum.CommandError.html#variant.NoName /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot pub fn write_totp_slot(&self, data: OtpSlotData, time_window: u16) -> CommandStatus { return self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| unsafe { nitrokey_sys::NK_write_totp_slot( raw_data.number, raw_data.name.as_ptr(), raw_data.secret.as_ptr(), time_window, raw_data.mode == OtpMode::EightDigits, raw_data.use_enter, raw_data.use_token_id, raw_data.token_id.as_ptr(), temp_password_ptr, ) }); } } impl Device for AdminAuthenticatedDevice {} impl AuthenticatedDevice for AdminAuthenticatedDevice { fn new(device: UnauthenticatedDevice, temp_password: Vec) -> Self { AdminAuthenticatedDevice { device, temp_password, } } }