diff options
Diffstat (limited to 'nitrokey/src/otp.rs')
-rw-r--r-- | nitrokey/src/otp.rs | 407 |
1 files changed, 407 insertions, 0 deletions
diff --git a/nitrokey/src/otp.rs b/nitrokey/src/otp.rs new file mode 100644 index 0000000..00a5e5e --- /dev/null +++ b/nitrokey/src/otp.rs @@ -0,0 +1,407 @@ +use nitrokey_sys; +use std::ffi::CString; +use util::{get_command_result, get_cstring, result_from_string, CommandError}; + +/// 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::{Authenticate, 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) { + /// Ok(()) => println!("Successfully wrote slot."), + /// Err(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) -> Result<(), CommandError>; + + /// 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::{Authenticate, 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) { + /// Ok(()) => println!("Successfully wrote slot."), + /// Err(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) -> Result<(), CommandError>; + + /// Erases an HOTP slot. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, 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) { + /// Ok(()) => println!("Successfully erased slot."), + /// Err(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) -> Result<(), CommandError>; + + /// Erases a TOTP slot. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{Authenticate, 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) { + /// Ok(()) => println!("Successfully erased slot."), + /// Err(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) -> Result<(), CommandError>; +} + +/// Provides methods to generate OTP codes and to query OTP slots on a Nitrokey +/// device. +pub trait GenerateOtp { + /// 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`][]). + /// + /// # Example + /// + /// ```ignore + /// extern crate chrono; + /// + /// use chrono::Utc; + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let time = Utc::now().timestamp(); + /// if time < 0 { + /// println!("Timestamps before 1970-01-01 are not supported!"); + /// } else { + /// device.set_time(time as u64); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// - [`Timestamp`][] if the time could not be set + /// + /// [`get_totp_code`]: #method.get_totp_code + /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp + fn set_time(&self, time: u64) -> Result<(), CommandError> { + unsafe { get_command_result(nitrokey_sys::NK_totp_set_time(time)) } + } + + /// 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 + /// + /// ```ignore + /// extern crate chrono; + /// + /// use nitrokey::GenerateOtp; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let time = Utc::now().timestamp(); + /// if time < 0 { + /// println!("Timestamps before 1970-01-01 are not supported!"); + /// } else { + /// device.set_time(time as u64); + /// let code = device.get_totp_code(1)?; + /// println!("Generated TOTP code on slot 1: {}", code); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`set_time`]: #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, see [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<S: Into<String>, T: Into<String>>( + number: u8, + name: S, + secret: T, + mode: OtpMode, + ) -> OtpSlotData { + OtpSlotData { + number, + name: name.into(), + secret: secret.into(), + mode, + use_enter: false, + token_id: None, + } + } + + /// Enables pressing the enter key after sending an OTP code using double-pressed numlock, + /// capslock or scrollock. + pub fn use_enter(mut self) -> OtpSlotData { + self.use_enter = true; + self + } + + /// Sets the token ID, see [OATH Token Identifier Specification][tokspec], section “Class A”. + /// + /// [tokspec]: https://openauthentication.org/token-specs/ + pub fn token_id<S: Into<String>>(mut self, id: S) -> OtpSlotData { + self.token_id = Some(id.into()); + self + } +} + +impl RawOtpSlotData { + pub fn new(data: OtpSlotData) -> Result<RawOtpSlotData, CommandError> { + let name = get_cstring(data.name)?; + let secret = get_cstring(data.secret)?; + let use_token_id = data.token_id.is_some(); + let token_id = get_cstring(data.token_id.unwrap_or_else(String::new))?; + + Ok(RawOtpSlotData { + number: data.number, + name, + secret, + mode: data.mode, + use_enter: data.use_enter, + use_token_id, + token_id, + }) + } +} |