aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2018-05-22 00:32:12 +0000
committerRobin Krahl <robin.krahl@ireas.org>2018-05-22 02:35:28 +0200
commita9657c48c8c92f6c82834a2abd90e27904e2cf6b (patch)
tree6991987c5fd21a169a233c30b17706330ae085c6
parent2a736067581dd44288b43a01bf47e0d28801e0a8 (diff)
downloadnitrokey-rs-a9657c48c8c92f6c82834a2abd90e27904e2cf6b.tar.gz
nitrokey-rs-a9657c48c8c92f6c82834a2abd90e27904e2cf6b.tar.bz2
Restructure code by functionality
In future versions, we want to support not only the Nitrokey Pro, but also the Nitrokey Storage. This requires a better code layout. This patch introduces two main changes: First, the OTP-specific methods are moved from the Device trait and the AdminAuthenticatedDevice struct to the functionality-based traits ConfigureOtp and GenerateOtp. This will hopefully make it easier to integrate the Nitrokey Storage. Secondly, the code is split into separate modules. These modules are currently all private and re-exported in the lib module, but we can consider making them public in the future.
-rw-r--r--src/config.rs100
-rw-r--r--src/device.rs711
-rw-r--r--src/lib.rs1303
-rw-r--r--src/otp.rs350
-rw-r--r--src/tests/pro.rs19
-rw-r--r--src/util.rs154
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,
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 235c81b..6101cee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
+ }
+ }
+}