aboutsummaryrefslogtreecommitdiff
path: root/nitrokey/src
diff options
context:
space:
mode:
authorRobin Krahl <me@robin-krahl.de>2018-12-11 23:50:45 +0100
committerDaniel Mueller <deso@posteo.net>2018-12-17 07:52:13 -0800
commit986ad2f782cf944990e4eda8bf88ea1821233302 (patch)
tree1717075a4eb11861c32e5c45d01e47360fb1264d /nitrokey/src
parente97c287c01cf22a1b582a7da9b309b58f3935d0e (diff)
downloadnitrocli-986ad2f782cf944990e4eda8bf88ea1821233302.tar.gz
nitrocli-986ad2f782cf944990e4eda8bf88ea1821233302.tar.bz2
Add nitrokey as a dependency to nitrocli
The nitrokey crate provides a simple interface to the Nitrokey Storage and the Nitrokey Pro based on the libnitrokey library developed by Nitrokey UG. The low-level bindings to this library are available in the nitrokey-sys crate. This patch adds version v0.2.1 of the nitrokey crate as a dependency for nitrocli. It includes the indirect dependencies nitrokey-sys (version 3.4.1) and rand (version 0.4.3). Import subrepo nitrokey/:nitrokey at 2eccc96ceec2282b868891befe9cda7f941fbe7b Import subrepo nitrokey-sys/:nitrokey-sys at f1a11ebf72610fb9cf80ac7f9f147b4ba1a5336f Import subrepo rand/:rand at d7d5da49daf7ceb3e5940072940d495cced3a1b3
Diffstat (limited to 'nitrokey/src')
-rw-r--r--nitrokey/src/auth.rs412
-rw-r--r--nitrokey/src/config.rs99
-rw-r--r--nitrokey/src/device.rs749
-rw-r--r--nitrokey/src/lib.rs127
-rw-r--r--nitrokey/src/otp.rs407
-rw-r--r--nitrokey/src/pws.rs351
-rw-r--r--nitrokey/src/tests/device.rs304
-rw-r--r--nitrokey/src/tests/mod.rs4
-rw-r--r--nitrokey/src/tests/otp.rs283
-rw-r--r--nitrokey/src/tests/pws.rs143
-rw-r--r--nitrokey/src/tests/util.rs11
-rw-r--r--nitrokey/src/util.rs171
12 files changed, 3061 insertions, 0 deletions
diff --git a/nitrokey/src/auth.rs b/nitrokey/src/auth.rs
new file mode 100644
index 0000000..0918222
--- /dev/null
+++ b/nitrokey/src/auth.rs
@@ -0,0 +1,412 @@
+use config::{Config, RawConfig};
+use device::{Device, DeviceWrapper, Pro, Storage};
+use nitrokey_sys;
+use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData, RawOtpSlotData};
+use std::ops::Deref;
+use std::os::raw::c_int;
+use util::{generate_password, get_command_result, get_cstring, result_from_string, CommandError};
+
+static TEMPORARY_PASSWORD_LENGTH: usize = 25;
+
+/// Provides methods to authenticate as a user or as an admin using a PIN. The authenticated
+/// methods will consume the current device instance. On success, they return the authenticated
+/// device. Otherwise, they return the current unauthenticated device and the error code.
+pub trait Authenticate {
+ /// 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::{Authenticate, DeviceWrapper, User};
+ /// # use nitrokey::CommandError;
+ ///
+ /// fn perform_user_task(device: &User<DeviceWrapper>) {}
+ /// fn perform_other_task(device: &DeviceWrapper) {}
+ ///
+ /// # 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
+ fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)>
+ where
+ Self: Device + Sized;
+
+ /// 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::{Authenticate, Admin, DeviceWrapper};
+ /// # use nitrokey::CommandError;
+ ///
+ /// fn perform_admin_task(device: &Admin<DeviceWrapper>) {}
+ /// fn perform_other_task(device: &DeviceWrapper) {}
+ ///
+ /// # 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
+ fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)>
+ where
+ Self: Device + Sized;
+}
+
+trait AuthenticatedDevice<T> {
+ fn new(device: T, temp_password: Vec<u8>) -> Self;
+}
+
+/// A Nitrokey device with user authentication.
+///
+/// To obtain an instance of this struct, use the [`authenticate_user`][] method from the
+/// [`Authenticate`][] trait. To get back to an unauthenticated device, use the [`device`][]
+/// method.
+///
+/// [`Authenticate`]: trait.Authenticate.html
+/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin
+/// [`device`]: #method.device
+#[derive(Debug)]
+pub struct User<T: Device> {
+ device: T,
+ temp_password: Vec<u8>,
+}
+
+/// A Nitrokey device with admin authentication.
+///
+/// To obtain an instance of this struct, use the [`authenticate_admin`][] method from the
+/// [`Authenticate`][] trait. To get back to an unauthenticated device, use the [`device`][]
+/// method.
+///
+/// [`Authenticate`]: trait.Authenticate.html
+/// [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin
+/// [`device`]: #method.device
+#[derive(Debug)]
+pub struct Admin<T: Device> {
+ device: T,
+ temp_password: Vec<u8>,
+}
+
+fn authenticate<D, A, T>(device: D, password: &str, callback: T) -> Result<A, (D, CommandError)>
+where
+ D: Device,
+ A: AuthenticatedDevice<D>,
+ T: Fn(*const i8, *const i8) -> c_int,
+{
+ let temp_password = match generate_password(TEMPORARY_PASSWORD_LENGTH) {
+ Ok(pw) => pw,
+ Err(_) => return Err((device, CommandError::RngError)),
+ };
+ let password = match get_cstring(password) {
+ Ok(password) => password,
+ Err(err) => return Err((device, err)),
+ };
+ let password_ptr = password.as_ptr();
+ let temp_password_ptr = temp_password.as_ptr() as *const i8;
+ return match callback(password_ptr, temp_password_ptr) {
+ 0 => Ok(A::new(device, temp_password)),
+ rv => Err((device, CommandError::from(rv))),
+ };
+}
+
+fn authenticate_user_wrapper<T, C>(
+ device: T,
+ constructor: C,
+ password: &str,
+) -> Result<User<DeviceWrapper>, (DeviceWrapper, CommandError)>
+where
+ T: Device,
+ C: Fn(T) -> DeviceWrapper,
+{
+ let result = device.authenticate_user(password);
+ match result {
+ Ok(user) => Ok(User::new(constructor(user.device), user.temp_password)),
+ Err((device, err)) => Err((constructor(device), err)),
+ }
+}
+
+fn authenticate_admin_wrapper<T, C>(
+ device: T,
+ constructor: C,
+ password: &str,
+) -> Result<Admin<DeviceWrapper>, (DeviceWrapper, CommandError)>
+where
+ T: Device,
+ C: Fn(T) -> DeviceWrapper,
+{
+ let result = device.authenticate_admin(password);
+ match result {
+ Ok(user) => Ok(Admin::new(constructor(user.device), user.temp_password)),
+ Err((device, err)) => Err((constructor(device), err)),
+ }
+}
+
+impl<T: Device> User<T> {
+ /// 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) -> T {
+ self.device
+ }
+}
+
+impl<T: Device> Deref for User<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.device
+ }
+}
+
+impl<T: Device> GenerateOtp for User<T> {
+ 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));
+ }
+ }
+
+ 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<T: Device> AuthenticatedDevice<T> for User<T> {
+ fn new(device: T, temp_password: Vec<u8>) -> Self {
+ User {
+ device,
+ temp_password,
+ }
+ }
+}
+
+impl<T: Device> Deref for Admin<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.device
+ }
+}
+
+impl<T: Device> Admin<T> {
+ /// 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) -> T {
+ 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::{Authenticate, 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) -> Result<(), CommandError> {
+ let raw_config = RawConfig::try_from(config)?;
+ unsafe {
+ get_command_result(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,
+ ))
+ }
+ }
+
+ fn write_otp_slot<C>(&self, data: OtpSlotData, callback: C) -> Result<(), CommandError>
+ where
+ C: Fn(RawOtpSlotData, *const i8) -> c_int,
+ {
+ let raw_data = RawOtpSlotData::new(data)?;
+ let temp_password_ptr = self.temp_password.as_ptr() as *const i8;
+ get_command_result(callback(raw_data, temp_password_ptr))
+ }
+}
+
+impl<T: Device> ConfigureOtp for Admin<T> {
+ fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> Result<(), CommandError> {
+ 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) -> Result<(), CommandError> {
+ 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) -> Result<(), CommandError> {
+ let temp_password_ptr = self.temp_password.as_ptr() as *const i8;
+ unsafe { get_command_result(nitrokey_sys::NK_erase_hotp_slot(slot, temp_password_ptr)) }
+ }
+
+ fn erase_totp_slot(&self, slot: u8) -> Result<(), CommandError> {
+ let temp_password_ptr = self.temp_password.as_ptr() as *const i8;
+ unsafe { get_command_result(nitrokey_sys::NK_erase_totp_slot(slot, temp_password_ptr)) }
+ }
+}
+
+impl<T: Device> AuthenticatedDevice<T> for Admin<T> {
+ fn new(device: T, temp_password: Vec<u8>) -> Self {
+ Admin {
+ device,
+ temp_password,
+ }
+ }
+}
+
+impl Authenticate for DeviceWrapper {
+ fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> {
+ match self {
+ DeviceWrapper::Storage(storage) => {
+ authenticate_user_wrapper(storage, DeviceWrapper::Storage, password)
+ }
+ DeviceWrapper::Pro(pro) => authenticate_user_wrapper(pro, DeviceWrapper::Pro, password),
+ }
+ }
+
+ fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> {
+ match self {
+ DeviceWrapper::Storage(storage) => {
+ authenticate_admin_wrapper(storage, DeviceWrapper::Storage, password)
+ }
+ DeviceWrapper::Pro(pro) => {
+ authenticate_admin_wrapper(pro, DeviceWrapper::Pro, password)
+ }
+ }
+ }
+}
+
+impl Authenticate for Pro {
+ fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> {
+ authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
+ nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr)
+ })
+ }
+
+ fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> {
+ authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
+ nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr)
+ })
+ }
+}
+
+impl Authenticate for Storage {
+ fn authenticate_user(self, password: &str) -> Result<User<Self>, (Self, CommandError)> {
+ authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
+ nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr)
+ })
+ }
+
+ fn authenticate_admin(self, password: &str) -> Result<Admin<Self>, (Self, CommandError)> {
+ authenticate(self, password, |password_ptr, temp_password_ptr| unsafe {
+ nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr)
+ })
+ }
+}
diff --git a/nitrokey/src/config.rs b/nitrokey/src/config.rs
new file mode 100644
index 0000000..33bf256
--- /dev/null
+++ b/nitrokey/src/config.rs
@@ -0,0 +1,99 @@
+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/nitrokey/src/device.rs b/nitrokey/src/device.rs
new file mode 100644
index 0000000..f135261
--- /dev/null
+++ b/nitrokey/src/device.rs
@@ -0,0 +1,749 @@
+use auth::Authenticate;
+use config::{Config, RawConfig};
+use libc;
+use nitrokey_sys;
+use otp::GenerateOtp;
+use pws::GetPasswordSafe;
+use util::{get_command_result, get_cstring, get_last_error, result_from_string, CommandError};
+
+/// Available Nitrokey models.
+#[derive(Debug, PartialEq)]
+enum Model {
+ /// The Nitrokey Storage.
+ Storage,
+ /// The Nitrokey Pro.
+ Pro,
+}
+
+/// A wrapper for a Nitrokey device of unknown type.
+///
+/// Use the function [`connect`][] to obtain a wrapped instance. The wrapper implements all traits
+/// that are shared between all Nitrokey devices so that the shared functionality can be used
+/// without knowing the type of the underlying device. If you want to use functionality that is
+/// not available for all devices, you have to extract the device.
+///
+/// # Examples
+///
+/// Authentication with error handling:
+///
+/// ```no_run
+/// use nitrokey::{Authenticate, DeviceWrapper, User};
+/// # use nitrokey::CommandError;
+///
+/// fn perform_user_task(device: &User<DeviceWrapper>) {}
+/// fn perform_other_task(device: &DeviceWrapper) {}
+///
+/// # 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(())
+/// # }
+/// ```
+///
+/// Device-specific commands:
+///
+/// ```no_run
+/// use nitrokey::{DeviceWrapper, Storage};
+/// # use nitrokey::CommandError;
+///
+/// fn perform_common_task(device: &DeviceWrapper) {}
+/// fn perform_storage_task(device: &Storage) {}
+///
+/// # fn try_main() -> Result<(), CommandError> {
+/// let device = nitrokey::connect()?;
+/// perform_common_task(&device);
+/// match device {
+/// DeviceWrapper::Storage(storage) => perform_storage_task(&storage),
+/// _ => (),
+/// };
+/// # Ok(())
+/// # }
+/// ```
+///
+/// [`connect`]: fn.connect.html
+// TODO: add example for Storage-specific code
+#[derive(Debug)]
+pub enum DeviceWrapper {
+ /// A Nitrokey Storage device.
+ Storage(Storage),
+ /// A Nitrokey Pro device.
+ Pro(Pro),
+}
+
+/// A Nitrokey Pro device without user or admin authentication.
+///
+/// Use the global function [`connect`][] to obtain an instance wrapper or the method
+/// [`connect`][`Pro::connect`] to directly 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::{Authenticate, User, Pro};
+/// # use nitrokey::CommandError;
+///
+/// fn perform_user_task(device: &User<Pro>) {}
+/// fn perform_other_task(device: &Pro) {}
+///
+/// # fn try_main() -> Result<(), CommandError> {
+/// let device = nitrokey::Pro::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`]: trait.Authenticate.html#method.authenticate_admin
+/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
+/// [`connect`]: fn.connect.html
+/// [`Pro::connect`]: #method.connect
+#[derive(Debug)]
+pub struct Pro {}
+
+/// A Nitrokey Storage device without user or admin authentication.
+///
+/// Use the global function [`connect`][] to obtain an instance wrapper or the method
+/// [`connect`][`Storage::connect`] to directly 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::{Authenticate, User, Storage};
+/// # use nitrokey::CommandError;
+///
+/// fn perform_user_task(device: &User<Storage>) {}
+/// fn perform_other_task(device: &Storage) {}
+///
+/// # fn try_main() -> Result<(), CommandError> {
+/// let device = nitrokey::Storage::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`]: trait.Authenticate.html#method.authenticate_admin
+/// [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
+/// [`connect`]: fn.connect.html
+/// [`Storage::connect`]: #method.connect
+#[derive(Debug)]
+pub struct Storage {}
+
+/// The status of a volume on a Nitrokey Storage device.
+#[derive(Debug)]
+pub struct VolumeStatus {
+ /// Indicates whether the volume is read-only.
+ pub read_only: bool,
+ /// Indicates whether the volume is active.
+ pub active: bool,
+}
+
+/// The status of a Nitrokey Storage device.
+#[derive(Debug)]
+pub struct StorageStatus {
+ /// The status of the unencrypted volume.
+ pub unencrypted_volume: VolumeStatus,
+ /// The status of the encrypted volume.
+ pub encrypted_volume: VolumeStatus,
+ /// The status of the hidden volume.
+ pub hidden_volume: VolumeStatus,
+ /// The major firmware version, e. g. 0 in v0.40.
+ pub firmware_version_major: u8,
+ /// The minor firmware version, e. g. 40 in v0.40.
+ pub firmware_version_minor: u8,
+ /// Indicates whether the firmware is locked.
+ pub firmware_locked: bool,
+ /// The serial number of the SD card in the Storage stick.
+ pub serial_number_sd_card: u32,
+ /// The serial number of the smart card in the Storage stick.
+ pub serial_number_smart_card: u32,
+ /// The number of remaining login attempts for the user PIN.
+ pub user_retry_count: u8,
+ /// The number of remaining login attempts for the admin PIN.
+ pub admin_retry_count: u8,
+ /// Indicates whether a new SD card was found.
+ pub new_sd_card_found: bool,
+ /// Indicates whether the SD card is filled with random characters.
+ pub filled_with_random: bool,
+ /// Indicates whether the stick has been initialized by generating
+ /// the AES keys.
+ pub stick_initialized: bool,
+}
+
+/// A Nitrokey device.
+///
+/// This trait provides the commands that can be executed without authentication and that are
+/// present on all supported Nitrokey devices.
+pub trait Device: Authenticate + GetPasswordSafe + GenerateOtp {
+ /// 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::Device;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// match device.change_admin_pin("12345678", "12345679") {
+ /// Ok(()) => println!("Updated admin PIN."),
+ /// Err(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) -> Result<(), CommandError> {
+ let current_string = get_cstring(current)?;
+ let new_string = get_cstring(new)?;
+ unsafe {
+ get_command_result(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::Device;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// match device.change_user_pin("123456", "123457") {
+ /// Ok(()) => println!("Updated admin PIN."),
+ /// Err(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) -> Result<(), CommandError> {
+ let current_string = get_cstring(current)?;
+ let new_string = get_cstring(new)?;
+ unsafe {
+ get_command_result(nitrokey_sys::NK_change_user_PIN(
+ current_string.as_ptr(),
+ new_string.as_ptr(),
+ ))
+ }
+ }
+
+ /// Unlocks the user PIN after three failed login attempts and sets it to the given value.
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidString`][] if one of the provided passwords contains a null byte
+ /// - [`WrongPassword`][] if the admin password is wrong
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// match device.unlock_user_pin("12345678", "123456") {
+ /// Ok(()) => println!("Unlocked user PIN."),
+ /// Err(err) => println!("Failed to unlock user PIN: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ fn unlock_user_pin(&self, admin_pin: &str, user_pin: &str) -> Result<(), CommandError> {
+ let admin_pin_string = get_cstring(admin_pin)?;
+ let user_pin_string = get_cstring(user_pin)?;
+ unsafe {
+ get_command_result(nitrokey_sys::NK_unlock_user_password(
+ admin_pin_string.as_ptr(),
+ user_pin_string.as_ptr(),
+ ))
+ }
+ }
+
+ /// Locks the Nitrokey device.
+ ///
+ /// This disables the password store if it has been unlocked. On the Nitrokey Storage, this
+ /// also disables the volumes if they have been enabled.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::Device;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// match device.lock() {
+ /// Ok(()) => println!("Locked the Nitrokey device."),
+ /// Err(err) => println!("Could not lock the Nitrokey device: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ fn lock(&self) -> Result<(), CommandError> {
+ unsafe { get_command_result(nitrokey_sys::NK_lock_device()) }
+ }
+}
+
+/// Connects to a Nitrokey device. This method can be used to connect to any connected device,
+/// both a Nitrokey Pro and a Nitrokey Storage.
+///
+/// # Example
+///
+/// ```
+/// use nitrokey::DeviceWrapper;
+///
+/// fn do_something(device: DeviceWrapper) {}
+///
+/// match nitrokey::connect() {
+/// Ok(device) => do_something(device),
+/// Err(err) => println!("Could not connect to a Nitrokey: {}", err),
+/// }
+/// ```
+pub fn connect() -> Result<DeviceWrapper, CommandError> {
+ unsafe {
+ match nitrokey_sys::NK_login_auto() {
+ 1 => match get_connected_device() {
+ Some(wrapper) => Ok(wrapper),
+ None => Err(CommandError::Unknown),
+ },
+ _ => Err(CommandError::Unknown),
+ }
+ }
+}
+
+fn get_connected_model() -> Option<Model> {
+ unsafe {
+ match nitrokey_sys::NK_get_device_model() {
+ nitrokey_sys::NK_device_model_NK_PRO => Some(Model::Pro),
+ nitrokey_sys::NK_device_model_NK_STORAGE => Some(Model::Storage),
+ _ => None,
+ }
+ }
+}
+
+fn create_device_wrapper(model: Model) -> DeviceWrapper {
+ match model {
+ Model::Pro => DeviceWrapper::Pro(Pro {}),
+ Model::Storage => DeviceWrapper::Storage(Storage {}),
+ }
+}
+
+fn get_connected_device() -> Option<DeviceWrapper> {
+ get_connected_model().map(create_device_wrapper)
+}
+
+fn connect_model(model: Model) -> bool {
+ let model = match model {
+ Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE,
+ Model::Pro => nitrokey_sys::NK_device_model_NK_PRO,
+ };
+ unsafe { nitrokey_sys::NK_login_enum(model) == 1 }
+}
+
+impl DeviceWrapper {
+ fn device(&self) -> &Device {
+ match *self {
+ DeviceWrapper::Storage(ref storage) => storage,
+ DeviceWrapper::Pro(ref pro) => pro,
+ }
+ }
+}
+
+impl GenerateOtp for DeviceWrapper {
+ fn get_hotp_slot_name(&self, slot: u8) -> Result<String, CommandError> {
+ self.device().get_hotp_slot_name(slot)
+ }
+
+ fn get_totp_slot_name(&self, slot: u8) -> Result<String, CommandError> {
+ self.device().get_totp_slot_name(slot)
+ }
+
+ fn get_hotp_code(&self, slot: u8) -> Result<String, CommandError> {
+ self.device().get_hotp_code(slot)
+ }
+
+ fn get_totp_code(&self, slot: u8) -> Result<String, CommandError> {
+ self.device().get_totp_code(slot)
+ }
+}
+
+impl Device for DeviceWrapper {}
+
+impl Pro {
+ pub fn connect() -> Result<Pro, CommandError> {
+ // TODO: maybe Option instead of Result?
+ match connect_model(Model::Pro) {
+ true => Ok(Pro {}),
+ false => Err(CommandError::Unknown),
+ }
+ }
+}
+
+impl Drop for Pro {
+ fn drop(&mut self) {
+ unsafe {
+ nitrokey_sys::NK_logout();
+ }
+ }
+}
+
+impl Device for Pro {}
+
+impl GenerateOtp for Pro {}
+
+impl Storage {
+ pub fn connect() -> Result<Storage, CommandError> {
+ // TODO: maybe Option instead of Result?
+ match connect_model(Model::Storage) {
+ true => Ok(Storage {}),
+ false => Err(CommandError::Unknown),
+ }
+ }
+
+ /// Enables the encrypted storage volume.
+ ///
+ /// Once the encrypted volume is enabled, it is presented to the operating system as a block
+ /// device. The API does not provide any information on the name or path of this block device.
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidString`][] if the provided password contains a null byte
+ /// - [`WrongPassword`][] if the provided user password is wrong
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::Storage::connect()?;
+ /// match device.enable_encrypted_volume("123456") {
+ /// Ok(()) => println!("Enabled the encrypted volume."),
+ /// Err(err) => println!("Could not enable the encrypted volume: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ pub fn enable_encrypted_volume(&self, user_pin: &str) -> Result<(), CommandError> {
+ let user_pin = get_cstring(user_pin)?;
+ unsafe { get_command_result(nitrokey_sys::NK_unlock_encrypted_volume(user_pin.as_ptr())) }
+ }
+
+ /// Disables the encrypted storage volume.
+ ///
+ /// Once the volume is disabled, it can be no longer accessed as a block device. If the
+ /// encrypted volume has not been enabled, this method still returns a success.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use nitrokey::CommandError;
+ ///
+ /// fn use_volume() {}
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::Storage::connect()?;
+ /// match device.enable_encrypted_volume("123456") {
+ /// Ok(()) => {
+ /// println!("Enabled the encrypted volume.");
+ /// use_volume();
+ /// match device.disable_encrypted_volume() {
+ /// Ok(()) => println!("Disabled the encrypted volume."),
+ /// Err(err) => {
+ /// println!("Could not disable the encrypted volume: {}", err);
+ /// },
+ /// };
+ /// },
+ /// Err(err) => println!("Could not enable the encrypted volume: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn disable_encrypted_volume(&self) -> Result<(), CommandError> {
+ unsafe { get_command_result(nitrokey_sys::NK_lock_encrypted_volume()) }
+ }
+
+
+ /// Returns the status of the connected storage device.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use nitrokey::CommandError;
+ ///
+ /// fn use_volume() {}
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::Storage::connect()?;
+ /// match device.get_status() {
+ /// Ok(status) => {
+ /// println!("SD card ID: {:#x}", status.serial_number_sd_card);
+ /// },
+ /// Err(err) => println!("Could not get Storage status: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn get_status(&self) -> Result<StorageStatus, CommandError> {
+ let mut raw_status = nitrokey_sys::NK_storage_status {
+ unencrypted_volume_read_only: false,
+ unencrypted_volume_active: false,
+ encrypted_volume_read_only: false,
+ encrypted_volume_active: false,
+ hidden_volume_read_only: false,
+ hidden_volume_active: false,
+ firmware_version_major: 0,
+ firmware_version_minor: 0,
+ firmware_locked: false,
+ serial_number_sd_card: 0,
+ serial_number_smart_card: 0,
+ user_retry_count: 0,
+ admin_retry_count: 0,
+ new_sd_card_found: false,
+ filled_with_random: false,
+ stick_initialized: false,
+ };
+ let raw_result = unsafe { nitrokey_sys::NK_get_status_storage(&mut raw_status) };
+ let result = get_command_result(raw_result);
+ result.and(Ok(StorageStatus::from(raw_status)))
+ }
+}
+
+impl Drop for Storage {
+ fn drop(&mut self) {
+ unsafe {
+ nitrokey_sys::NK_logout();
+ }
+ }
+}
+
+impl Device for Storage {}
+
+impl GenerateOtp for Storage {}
+
+impl From<nitrokey_sys::NK_storage_status> for StorageStatus {
+ fn from(status: nitrokey_sys::NK_storage_status) -> Self {
+ StorageStatus {
+ unencrypted_volume: VolumeStatus {
+ read_only: status.unencrypted_volume_read_only,
+ active: status.unencrypted_volume_active,
+ },
+ encrypted_volume: VolumeStatus {
+ read_only: status.encrypted_volume_read_only,
+ active: status.encrypted_volume_active,
+ },
+ hidden_volume: VolumeStatus {
+ read_only: status.hidden_volume_read_only,
+ active: status.hidden_volume_active,
+ },
+ firmware_version_major: status.firmware_version_major,
+ firmware_version_minor: status.firmware_version_minor,
+ firmware_locked: status.firmware_locked,
+ serial_number_sd_card: status.serial_number_sd_card,
+ serial_number_smart_card: status.serial_number_smart_card,
+ user_retry_count: status.user_retry_count,
+ admin_retry_count: status.admin_retry_count,
+ new_sd_card_found: status.new_sd_card_found,
+ filled_with_random: status.filled_with_random,
+ stick_initialized: status.stick_initialized,
+ }
+ }
+}
diff --git a/nitrokey/src/lib.rs b/nitrokey/src/lib.rs
new file mode 100644
index 0000000..e70aa73
--- /dev/null
+++ b/nitrokey/src/lib.rs
@@ -0,0 +1,127 @@
+//! Provides access to a Nitrokey device using the native libnitrokey API.
+//!
+//! # Usage
+//!
+//! Operations on the Nitrokey require different authentication levels. Some operations can be
+//! performed without authentication, some require user access, and some require admin access.
+//! This is modelled using the types [`User`][] and [`Admin`][].
+//!
+//! Use [`connect`][] to connect to any Nitrokey device. The method will return a
+//! [`DeviceWrapper`][] that abstracts over the supported Nitrokey devices. You can also use
+//! [`Pro::connect`][] or [`Storage::connect`][] to connect to a specific device.
+//!
+//! You can then use [`authenticate_user`][] or [`authenticate_admin`][] to get an authenticated
+//! device that can perform operations that require authentication. You can use [`device`][] to go
+//! back to the unauthenticated device.
+//!
+//! This makes sure that you can only execute a command if you have the required access rights.
+//! Otherwise, your code will not compile. The only exception are the methods to generate one-time
+//! passwords – [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick configuration,
+//! these operations are available without authentication or with user authentication.
+//!
+//! # Examples
+//!
+//! Connect to any Nitrokey and print its serial number:
+//!
+//! ```no_run
+//! use nitrokey::Device;
+//! # use nitrokey::CommandError;
+//!
+//! # fn try_main() -> Result<(), CommandError> {
+//! let device = nitrokey::connect()?;
+//! println!("{}", device.get_serial_number()?);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! Configure an HOTP slot:
+//!
+//! ```no_run
+//! use nitrokey::{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(())
+//! # }
+//! ```
+//!
+//! Generate an HOTP one-time password:
+//!
+//! ```no_run
+//! use nitrokey::{Device, GenerateOtp};
+//! # use nitrokey::CommandError;
+//!
+//! # fn try_main() -> Result<(), (CommandError)> {
+//! let device = nitrokey::connect()?;
+//! match device.get_hotp_code(1) {
+//! Ok(code) => println!("Generated HOTP code: {}", code),
+//! Err(err) => println!("Could not generate HOTP code: {}", err),
+//! }
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! [`authenticate_admin`]: trait.Authenticate.html#method.authenticate_admin
+//! [`authenticate_user`]: trait.Authenticate.html#method.authenticate_user
+//! [`connect`]: fn.connect.html
+//! [`Pro::connect`]: struct.Pro.html#fn.connect.html
+//! [`Storage::connect`]: struct.Storage.html#fn.connect.html
+//! [`device`]: struct.User.html#method.device
+//! [`get_hotp_code`]: trait.GenerateOtp.html#method.get_hotp_code
+//! [`get_totp_code`]: trait.GenerateOtp.html#method.get_totp_code
+//! [`Admin`]: struct.Admin.html
+//! [`DeviceWrapper`]: enum.DeviceWrapper.html
+//! [`User`]: struct.User.html
+
+extern crate libc;
+extern crate nitrokey_sys;
+extern crate rand;
+
+mod auth;
+mod config;
+mod device;
+mod otp;
+mod pws;
+#[cfg(test)]
+mod tests;
+mod util;
+
+pub use auth::{Admin, Authenticate, User};
+pub use config::Config;
+pub use device::{connect, Device, DeviceWrapper, Pro, Storage, StorageStatus, VolumeStatus};
+pub use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData};
+pub use pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT};
+pub use util::{CommandError, LogLevel};
+
+/// Enables or disables debug output. Calling this method with `true` is equivalent to setting the
+/// log level to `Debug`; calling it with `false` is equivalent to the log level `Error` (see
+/// [`set_log_level`][]).
+///
+/// If debug output is enabled, detailed information about the communication with the Nitrokey
+/// device is printed to the standard output.
+///
+/// [`set_log_level`]: fn.set_log_level.html
+pub fn set_debug(state: bool) {
+ unsafe {
+ nitrokey_sys::NK_set_debug(state);
+ }
+}
+
+/// Sets the log level for libnitrokey. All log messages are written to the standard error stream.
+/// Setting the log level enables all log messages on the same or on a higher log level.
+pub fn set_log_level(level: LogLevel) {
+ unsafe {
+ nitrokey_sys::NK_set_debug_level(level.into());
+ }
+}
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,
+ })
+ }
+}
diff --git a/nitrokey/src/pws.rs b/nitrokey/src/pws.rs
new file mode 100644
index 0000000..c20fe9d
--- /dev/null
+++ b/nitrokey/src/pws.rs
@@ -0,0 +1,351 @@
+use device::{Device, DeviceWrapper, Pro, Storage};
+use libc;
+use nitrokey_sys;
+use util::{get_command_result, get_cstring, get_last_error, result_from_string, CommandError};
+
+/// The number of slots in a [`PasswordSafe`][].
+///
+/// [`PasswordSafe`]: struct.PasswordSafe.html
+pub const SLOT_COUNT: u8 = 16;
+
+/// A password safe on a Nitrokey device.
+///
+/// The password safe stores a tuple consisting of a name, a login and a password on a slot. The
+/// number of available slots is [`SLOT_COUNT`][]. The slots are addressed starting with zero. To
+/// retrieve a password safe from a Nitrokey device, use the [`get_password_safe`][] method from
+/// the [`GetPasswordSafe`][] trait. Note that the device must live at least as long as the
+/// password safe.
+///
+/// Once the password safe has been unlocked, it can be accessed without a password. Therefore it
+/// is mandatory to call [`lock`][] on the corresponding device after the password store is used.
+/// As this command may have side effects on the Nitrokey Storage, it cannot be called
+/// automatically once the password safe is destroyed.
+///
+/// # Examples
+///
+/// Open a password safe and access a password:
+///
+/// ```no_run
+/// use nitrokey::{Device, GetPasswordSafe, PasswordSafe};
+/// # use nitrokey::CommandError;
+///
+/// fn use_password_safe(pws: &PasswordSafe) -> Result<(), CommandError> {
+/// let name = pws.get_slot_name(0)?;
+/// let login = pws.get_slot_login(0)?;
+/// let password = pws.get_slot_login(0)?;
+/// println!("Credentials for {}: login {}, password {}", name, login, password);
+/// Ok(())
+/// }
+///
+/// # fn try_main() -> Result<(), CommandError> {
+/// let device = nitrokey::connect()?;
+/// let pws = device.get_password_safe("123456")?;
+/// use_password_safe(&pws);
+/// device.lock()?;
+/// # Ok(())
+/// # }
+/// ```
+///
+/// [`SLOT_COUNT`]: constant.SLOT_COUNT.html
+/// [`get_password_safe`]: trait.GetPasswordSafe.html#method.get_password_safe
+/// [`lock`]: trait.Device.html#method.lock
+/// [`GetPasswordSafe`]: trait.GetPasswordSafe.html
+pub struct PasswordSafe<'a> {
+ _device: &'a Device,
+}
+
+/// Provides access to a [`PasswordSafe`][].
+///
+/// The device that implements this trait must always live at least as long as a password safe
+/// retrieved from it.
+///
+/// [`PasswordSafe`]: struct.PasswordSafe.html
+pub trait GetPasswordSafe {
+ /// Enables and returns the password safe.
+ ///
+ /// The underlying device must always live at least as long as a password safe retrieved from
+ /// it. It is mandatory to lock the underlying device using [`lock`][] after the password safe
+ /// has been used. Otherwise, other applications can access the password store without
+ /// authentication.
+ ///
+ /// # 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::{Device, GetPasswordSafe, PasswordSafe};
+ /// # use nitrokey::CommandError;
+ ///
+ /// fn use_password_safe(pws: &PasswordSafe) {}
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// match device.get_password_safe("123456") {
+ /// Ok(pws) => {
+ /// use_password_safe(&pws);
+ /// device.lock()?;
+ /// },
+ /// Err(err) => println!("Could not open the password safe: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`device`]: struct.PasswordSafe.html#method.device
+ /// [`lock`]: trait.Device.html#method.lock
+ /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
+ /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
+ fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError>;
+}
+
+fn get_password_safe<'a>(
+ device: &'a Device,
+ user_pin: &str,
+) -> Result<PasswordSafe<'a>, CommandError> {
+ let user_pin_string = get_cstring(user_pin)?;
+ let result = unsafe {
+ get_command_result(nitrokey_sys::NK_enable_password_safe(
+ user_pin_string.as_ptr(),
+ ))
+ };
+ result.map(|()| PasswordSafe { _device: device })
+}
+
+impl<'a> PasswordSafe<'a> {
+ /// Returns the status of all password slots.
+ ///
+ /// The status indicates whether a slot is programmed or not.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::{GetPasswordSafe, SLOT_COUNT};
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// let pws = device.get_password_safe("123456")?;
+ /// pws.get_slot_status()?.iter().enumerate().for_each(|(slot, programmed)| {
+ /// let status = match *programmed {
+ /// true => "programmed",
+ /// false => "not programmed",
+ /// };
+ /// println!("Slot {}: {}", slot, status);
+ /// });
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn get_slot_status(&self) -> Result<[bool; SLOT_COUNT as usize], CommandError> {
+ let status_ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_status() };
+ if status_ptr.is_null() {
+ return Err(get_last_error());
+ }
+ let status_array_ptr = status_ptr as *const [u8; SLOT_COUNT as usize];
+ let status_array = unsafe { *status_array_ptr };
+ let mut result = [false; SLOT_COUNT as usize];
+ for i in 0..SLOT_COUNT {
+ result[i as usize] = status_array[i as usize] == 1;
+ }
+ unsafe {
+ libc::free(status_ptr as *mut libc::c_void);
+ }
+ Ok(result)
+ }
+
+ /// Returns the name of the given slot (if it is programmed).
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidSlot`][] if the given slot is out of range
+ /// - [`Unknown`][] if the slot is not programmed
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::GetPasswordSafe;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// match device.get_password_safe("123456") {
+ /// Ok(pws) => {
+ /// let name = pws.get_slot_name(0)?;
+ /// let login = pws.get_slot_login(0)?;
+ /// let password = pws.get_slot_login(0)?;
+ /// println!("Credentials for {}: login {}, password {}", name, login, password);
+ /// },
+ /// Err(err) => println!("Could not open the password safe: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot
+ /// [`Unknown`]: enum.CommandError.html#variant.Unknown
+ pub fn get_slot_name(&self, slot: u8) -> Result<String, CommandError> {
+ unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_name(slot)) }
+ }
+
+ /// Returns the login for the given slot (if it is programmed).
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidSlot`][] if the given slot is out of range
+ /// - [`Unknown`][] if the slot is not programmed
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::GetPasswordSafe;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// let pws = device.get_password_safe("123456")?;
+ /// let name = pws.get_slot_name(0)?;
+ /// let login = pws.get_slot_login(0)?;
+ /// let password = pws.get_slot_login(0)?;
+ /// println!("Credentials for {}: login {}, password {}", name, login, password);
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot
+ /// [`Unknown`]: enum.CommandError.html#variant.Unknown
+ pub fn get_slot_login(&self, slot: u8) -> Result<String, CommandError> {
+ unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_login(slot)) }
+ }
+
+ /// Returns the password for the given slot (if it is programmed).
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidSlot`][] if the given slot is out of range
+ /// - [`Unknown`][] if the slot is not programmed
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::GetPasswordSafe;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// let pws = device.get_password_safe("123456")?;
+ /// let name = pws.get_slot_name(0)?;
+ /// let login = pws.get_slot_login(0)?;
+ /// let password = pws.get_slot_login(0)?;
+ /// println!("Credentials for {}: login {}, password {}", name, login, password);
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot
+ /// [`Unknown`]: enum.CommandError.html#variant.Unknown
+ pub fn get_slot_password(&self, slot: u8) -> Result<String, CommandError> {
+ unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_password(slot)) }
+ }
+
+ /// Writes the given slot with the given name, login and password.
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidSlot`][] if the given slot is out of range
+ /// - [`InvalidString`][] if the provided token ID contains a null byte
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::GetPasswordSafe;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// let pws = device.get_password_safe("123456")?;
+ /// let name = pws.get_slot_name(0)?;
+ /// let login = pws.get_slot_login(0)?;
+ /// let password = pws.get_slot_login(0)?;
+ /// println!("Credentials for {}: login {}, password {}", name, login, password);
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot
+ /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString
+ pub fn write_slot(
+ &self,
+ slot: u8,
+ name: &str,
+ login: &str,
+ password: &str,
+ ) -> Result<(), CommandError> {
+ let name_string = get_cstring(name)?;
+ let login_string = get_cstring(login)?;
+ let password_string = get_cstring(password)?;
+ unsafe {
+ get_command_result(nitrokey_sys::NK_write_password_safe_slot(
+ slot,
+ name_string.as_ptr(),
+ login_string.as_ptr(),
+ password_string.as_ptr(),
+ ))
+ }
+ }
+
+ /// Erases the given slot. Erasing clears the stored name, login and password (if the slot was
+ /// programmed).
+ ///
+ /// # Errors
+ ///
+ /// - [`InvalidSlot`][] if the given slot is out of range
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use nitrokey::GetPasswordSafe;
+ /// # use nitrokey::CommandError;
+ ///
+ /// # fn try_main() -> Result<(), CommandError> {
+ /// let device = nitrokey::connect()?;
+ /// let pws = device.get_password_safe("123456")?;
+ /// match pws.erase_slot(0) {
+ /// Ok(()) => println!("Erased slot 0."),
+ /// Err(err) => println!("Could not erase slot 0: {}", err),
+ /// };
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot
+ pub fn erase_slot(&self, slot: u8) -> Result<(), CommandError> {
+ unsafe { get_command_result(nitrokey_sys::NK_erase_password_safe_slot(slot)) }
+ }
+}
+
+impl<'a> Drop for PasswordSafe<'a> {
+ fn drop(&mut self) {
+ // TODO: disable the password safe -- NK_lock_device has side effects on the Nitrokey
+ // Storage, see https://github.com/Nitrokey/nitrokey-storage-firmware/issues/65
+ }
+}
+
+impl GetPasswordSafe for Pro {
+ fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError> {
+ get_password_safe(self, user_pin)
+ }
+}
+
+impl GetPasswordSafe for Storage {
+ fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError> {
+ get_password_safe(self, user_pin)
+ }
+}
+
+impl GetPasswordSafe for DeviceWrapper {
+ fn get_password_safe(&self, user_pin: &str) -> Result<PasswordSafe, CommandError> {
+ get_password_safe(self, user_pin)
+ }
+}
diff --git a/nitrokey/src/tests/device.rs b/nitrokey/src/tests/device.rs
new file mode 100644
index 0000000..fed465d
--- /dev/null
+++ b/nitrokey/src/tests/device.rs
@@ -0,0 +1,304 @@
+use std::ffi::CStr;
+use std::process::Command;
+use std::{thread, time};
+use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD};
+use {Authenticate, CommandError, Config, Device, Storage};
+
+static ADMIN_NEW_PASSWORD: &str = "1234567890";
+static USER_NEW_PASSWORD: &str = "abcdefghij";
+
+fn count_nitrokey_block_devices() -> usize {
+ thread::sleep(time::Duration::from_secs(2));
+ let output = Command::new("lsblk")
+ .args(&["-o", "MODEL"])
+ .output()
+ .expect("Could not list block devices");
+ String::from_utf8_lossy(&output.stdout)
+ .split("\n")
+ .filter(|&s| s == "Nitrokey Storage")
+ .count()
+}
+
+#[test]
+#[cfg_attr(not(feature = "test-no-device"), ignore)]
+fn connect_no_device() {
+ assert!(::connect().is_err());
+ assert!(::Pro::connect().is_err());
+ assert!(::Storage::connect().is_err());
+}
+
+#[test]
+#[cfg_attr(not(feature = "test-pro"), ignore)]
+fn connect_pro() {
+ assert!(::connect().is_ok());
+ assert!(::Pro::connect().is_ok());
+ assert!(::Storage::connect().is_err());
+ match ::connect().unwrap() {
+ ::DeviceWrapper::Pro(_) => assert!(true),
+ ::DeviceWrapper::Storage(_) => assert!(false),
+ };
+}
+
+#[test]
+#[cfg_attr(not(feature = "test-storage"), ignore)]
+fn connect_storage() {
+ assert!(::connect().is_ok());
+ assert!(::Pro::connect().is_err());
+ assert!(::Storage::connect().is_ok());
+ match ::connect().unwrap() {
+ ::DeviceWrapper::Pro(_) => assert!(false),
+ ::DeviceWrapper::Storage(_) => assert!(true),
+ };
+}
+
+fn assert_empty_serial_number() {
+ unsafe {
+ let ptr = ::nitrokey_sys::NK_device_serial_number();
+ assert!(!ptr.is_null());
+ let cstr = CStr::from_ptr(ptr);
+ assert_eq!(cstr.to_string_lossy(), "");
+ }
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn disconnect() {
+ Target::connect().unwrap();
+ assert_empty_serial_number();
+ Target::connect()
+ .unwrap()
+ .authenticate_admin(ADMIN_PASSWORD)
+ .unwrap();
+ assert_empty_serial_number();
+ Target::connect()
+ .unwrap()
+ .authenticate_user(USER_PASSWORD)
+ .unwrap();
+ assert_empty_serial_number();
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn get_serial_number() {
+ let device = Target::connect().unwrap();
+ let result = device.get_serial_number();
+ assert!(result.is_ok());
+ let serial_number = result.unwrap();
+ assert!(serial_number.is_ascii());
+ assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit()));
+}
+#[test]
+#[cfg_attr(not(feature = "test-pro"), ignore)]
+fn get_firmware_version() {
+ let device = Target::connect().unwrap();
+ assert_eq!(0, device.get_major_firmware_version());
+ let minor = device.get_minor_firmware_version();
+ assert!(minor > 0);
+}
+
+fn admin_retry<T: Authenticate + Device>(device: T, suffix: &str, count: u8) -> T {
+ let result = device.authenticate_admin(&(ADMIN_PASSWORD.to_owned() + suffix));
+ let device = match result {
+ Ok(admin) => admin.device(),
+ Err((device, _)) => device,
+ };
+ assert_eq!(count, device.get_admin_retry_count());
+ return device;
+}
+
+fn user_retry<T: Authenticate + Device>(device: T, suffix: &str, count: u8) -> T {
+ let result = device.authenticate_user(&(USER_PASSWORD.to_owned() + suffix));
+ let device = match result {
+ Ok(admin) => admin.device(),
+ Err((device, _)) => device,
+ };
+ assert_eq!(count, device.get_user_retry_count());
+ return device;
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn get_retry_count() {
+ let device = Target::connect().unwrap();
+
+ let device = admin_retry(device, "", 3);
+ let device = admin_retry(device, "123", 2);
+ let device = admin_retry(device, "456", 1);
+ let device = admin_retry(device, "", 3);
+
+ let device = user_retry(device, "", 3);
+ let device = user_retry(device, "123", 2);
+ let device = user_retry(device, "456", 1);
+ user_retry(device, "", 3);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn config() {
+ let device = Target::connect().unwrap();
+ let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap();
+ let config = Config::new(None, None, None, true);
+ assert!(admin.write_config(config).is_ok());
+ let get_config = admin.get_config().unwrap();
+ assert_eq!(config, get_config);
+
+ let config = Config::new(None, Some(9), None, true);
+ assert_eq!(Err(CommandError::InvalidSlot), admin.write_config(config));
+
+ let config = Config::new(Some(1), None, Some(0), false);
+ assert!(admin.write_config(config).is_ok());
+ let get_config = admin.get_config().unwrap();
+ assert_eq!(config, get_config);
+
+ let config = Config::new(None, None, None, false);
+ assert!(admin.write_config(config).is_ok());
+ let get_config = admin.get_config().unwrap();
+ assert_eq!(config, get_config);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn change_user_pin() {
+ let device = Target::connect().unwrap();
+ let device = device.authenticate_user(USER_PASSWORD).unwrap().device();
+ let device = device.authenticate_user(USER_NEW_PASSWORD).unwrap_err().0;
+
+ assert!(
+ device
+ .change_user_pin(USER_PASSWORD, USER_NEW_PASSWORD)
+ .is_ok()
+ );
+
+ let device = device.authenticate_user(USER_PASSWORD).unwrap_err().0;
+ let device = device
+ .authenticate_user(USER_NEW_PASSWORD)
+ .unwrap()
+ .device();
+
+ let result = device.change_user_pin(USER_PASSWORD, USER_PASSWORD);
+ assert_eq!(Err(CommandError::WrongPassword), result);
+
+ assert!(
+ device
+ .change_user_pin(USER_NEW_PASSWORD, USER_PASSWORD)
+ .is_ok()
+ );
+
+ let device = device.authenticate_user(USER_PASSWORD).unwrap().device();
+ assert!(device.authenticate_user(USER_NEW_PASSWORD).is_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn change_admin_pin() {
+ let device = Target::connect().unwrap();
+ let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap().device();
+ let device = device.authenticate_admin(ADMIN_NEW_PASSWORD).unwrap_err().0;
+
+ assert!(
+ device
+ .change_admin_pin(ADMIN_PASSWORD, ADMIN_NEW_PASSWORD)
+ .is_ok()
+ );
+
+ let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap_err().0;
+ let device = device
+ .authenticate_admin(ADMIN_NEW_PASSWORD)
+ .unwrap()
+ .device();
+
+ assert_eq!(
+ Err(CommandError::WrongPassword),
+ device.change_admin_pin(ADMIN_PASSWORD, ADMIN_PASSWORD)
+ );
+
+ assert!(
+ device
+ .change_admin_pin(ADMIN_NEW_PASSWORD, ADMIN_PASSWORD)
+ .is_ok()
+ );
+
+ let device = device.authenticate_admin(ADMIN_PASSWORD).unwrap().device();
+ device.authenticate_admin(ADMIN_NEW_PASSWORD).unwrap_err();
+}
+
+fn require_failed_user_login(device: Target, password: &str, error: CommandError) -> Target {
+ let result = device.authenticate_user(password);
+ assert!(result.is_err());
+ let err = result.unwrap_err();
+ assert_eq!(error, err.1);
+ err.0
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn unlock_user_pin() {
+ let device = Target::connect().unwrap();
+ let device = device.authenticate_user(USER_PASSWORD).unwrap().device();
+ assert!(
+ device
+ .unlock_user_pin(ADMIN_PASSWORD, USER_PASSWORD)
+ .is_ok()
+ );
+ assert_eq!(
+ Err(CommandError::WrongPassword),
+ device.unlock_user_pin(USER_PASSWORD, USER_PASSWORD)
+ );
+
+ let wrong_password = USER_PASSWORD.to_owned() + "foo";
+ let device = require_failed_user_login(device, &wrong_password, CommandError::WrongPassword);
+ let device = require_failed_user_login(device, &wrong_password, CommandError::WrongPassword);
+ let device = require_failed_user_login(device, &wrong_password, CommandError::WrongPassword);
+ let device = require_failed_user_login(device, USER_PASSWORD, CommandError::WrongPassword);
+
+ assert_eq!(
+ Err(CommandError::WrongPassword),
+ device.unlock_user_pin(USER_PASSWORD, USER_PASSWORD)
+ );
+ assert!(
+ device
+ .unlock_user_pin(ADMIN_PASSWORD, USER_PASSWORD)
+ .is_ok()
+ );
+ device.authenticate_user(USER_PASSWORD).unwrap();
+}
+
+#[test]
+#[cfg_attr(not(feature = "test-storage"), ignore)]
+fn encrypted_volume() {
+ let device = Storage::connect().unwrap();
+ assert!(device.lock().is_ok());
+
+ assert_eq!(1, count_nitrokey_block_devices());
+ assert!(device.disable_encrypted_volume().is_ok());
+ assert_eq!(1, count_nitrokey_block_devices());
+ assert_eq!(
+ Err(CommandError::WrongPassword),
+ device.enable_encrypted_volume("123")
+ );
+ assert_eq!(1, count_nitrokey_block_devices());
+ assert!(device.enable_encrypted_volume(USER_PASSWORD).is_ok());
+ assert_eq!(2, count_nitrokey_block_devices());
+ assert!(device.disable_encrypted_volume().is_ok());
+ assert_eq!(1, count_nitrokey_block_devices());
+}
+
+#[test]
+#[cfg_attr(not(feature = "test-storage"), ignore)]
+fn lock() {
+ let device = Storage::connect().unwrap();
+
+ assert!(device.enable_encrypted_volume(USER_PASSWORD).is_ok());
+ assert!(device.lock().is_ok());
+ assert_eq!(1, count_nitrokey_block_devices());
+}
+
+#[test]
+#[cfg_attr(not(feature = "test-storage"), ignore)]
+fn get_storage_status() {
+ let device = Storage::connect().unwrap();
+ let status = device.get_status().unwrap();
+
+ assert!(status.serial_number_sd_card > 0);
+ assert!(status.serial_number_smart_card > 0);
+}
diff --git a/nitrokey/src/tests/mod.rs b/nitrokey/src/tests/mod.rs
new file mode 100644
index 0000000..34ca0aa
--- /dev/null
+++ b/nitrokey/src/tests/mod.rs
@@ -0,0 +1,4 @@
+mod device;
+mod otp;
+mod pws;
+mod util;
diff --git a/nitrokey/src/tests/otp.rs b/nitrokey/src/tests/otp.rs
new file mode 100644
index 0000000..cf71d9d
--- /dev/null
+++ b/nitrokey/src/tests/otp.rs
@@ -0,0 +1,283 @@
+use std::ops::Deref;
+use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD};
+use {Admin, Authenticate, CommandError, Config, ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData};
+
+// test suite according to RFC 4226, Appendix D
+static HOTP_SECRET: &str = "3132333435363738393031323334353637383930";
+static HOTP_CODES: &[&str] = &[
+ "755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871",
+ "520489",
+];
+
+// test suite according to RFC 6238, Appendix B
+static TOTP_SECRET: &str = "3132333435363738393031323334353637383930";
+static TOTP_CODES: &[(u64, &str)] = &[
+ (59, "94287082"),
+ (1111111109, "07081804"),
+ (1111111111, "14050471"),
+ (1234567890, "89005924"),
+ (2000000000, "69279037"),
+ (20000000000, "65353130"),
+];
+
+#[derive(PartialEq)]
+enum TotpTimestampSize {
+ U32,
+ U64,
+}
+
+fn get_admin_test_device() -> Admin<Target> {
+ Target::connect()
+ .expect("Could not connect to the Nitrokey.")
+ .authenticate_admin(ADMIN_PASSWORD)
+ .expect("Could not login as admin.")
+}
+
+fn configure_hotp(admin: &ConfigureOtp, counter: u8) {
+ let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits);
+ assert!(admin.write_hotp_slot(slot_data, counter.into()).is_ok());
+}
+
+fn check_hotp_codes(device: &GenerateOtp, offset: u8) {
+ HOTP_CODES.iter().enumerate().for_each(|(i, code)| {
+ if i >= offset as usize {
+ let result = device.get_hotp_code(1);
+ assert_eq!(code, &result.unwrap());
+ }
+ });
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn hotp_no_pin() {
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, false);
+ assert!(admin.write_config(config).is_ok());
+
+ configure_hotp(&admin, 0);
+ check_hotp_codes(admin.deref(), 0);
+
+ configure_hotp(&admin, 5);
+ check_hotp_codes(admin.deref(), 5);
+
+ configure_hotp(&admin, 0);
+ check_hotp_codes(&admin.device(), 0);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn hotp_pin() {
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, true);
+ assert!(admin.write_config(config).is_ok());
+
+ configure_hotp(&admin, 0);
+ let user = admin.device().authenticate_user(USER_PASSWORD).unwrap();
+ check_hotp_codes(&user, 0);
+
+ assert!(user.device().get_hotp_code(1).is_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn hotp_slot_name() {
+ let admin = get_admin_test_device();
+ let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits);
+ assert!(admin.write_hotp_slot(slot_data, 0).is_ok());
+
+ let device = admin.device();
+ let result = device.get_hotp_slot_name(1);
+ assert_eq!("test-hotp", result.unwrap());
+ let result = device.get_hotp_slot_name(4);
+ assert_eq!(CommandError::InvalidSlot, result.unwrap_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn hotp_error() {
+ let admin = get_admin_test_device();
+ let slot_data = OtpSlotData::new(1, "", HOTP_SECRET, OtpMode::SixDigits);
+ assert_eq!(
+ Err(CommandError::NoName),
+ admin.write_hotp_slot(slot_data, 0)
+ );
+ let slot_data = OtpSlotData::new(4, "test", HOTP_SECRET, OtpMode::SixDigits);
+ assert_eq!(
+ Err(CommandError::InvalidSlot),
+ admin.write_hotp_slot(slot_data, 0)
+ );
+ let code = admin.get_hotp_code(4);
+ assert_eq!(CommandError::InvalidSlot, code.unwrap_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn hotp_erase() {
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, false);
+ assert!(admin.write_config(config).is_ok());
+ let slot_data = OtpSlotData::new(1, "test1", HOTP_SECRET, OtpMode::SixDigits);
+ assert!(admin.write_hotp_slot(slot_data, 0).is_ok());
+ let slot_data = OtpSlotData::new(2, "test2", HOTP_SECRET, OtpMode::SixDigits);
+ assert!(admin.write_hotp_slot(slot_data, 0).is_ok());
+
+ assert!(admin.erase_hotp_slot(1).is_ok());
+
+ let device = admin.device();
+ let result = device.get_hotp_slot_name(1);
+ assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err());
+ let result = device.get_hotp_code(1);
+ assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err());
+
+ assert_eq!("test2", device.get_hotp_slot_name(2).unwrap());
+}
+
+fn configure_totp(admin: &ConfigureOtp, factor: u64) {
+ let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits);
+ let time_window = 30u64.checked_mul(factor).unwrap();
+ assert!(admin.write_totp_slot(slot_data, time_window as u16).is_ok());
+}
+
+fn check_totp_codes(device: &GenerateOtp, factor: u64, timestamp_size: TotpTimestampSize) {
+ for (i, &(base_time, code)) in TOTP_CODES.iter().enumerate() {
+ let time = base_time.checked_mul(factor).unwrap();
+ let is_u64 = time > u32::max_value() as u64;
+ if is_u64 != (timestamp_size == TotpTimestampSize::U64) {
+ continue;
+ }
+
+ assert!(device.set_time(time).is_ok());
+ let result = device.get_totp_code(1);
+ assert!(result.is_ok());
+ let result_code = result.unwrap();
+ assert_eq!(
+ code, result_code,
+ "TOTP code {} should be {} but is {}",
+ i, code, result_code
+ );
+ }
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn totp_no_pin() {
+ // TODO: this test may fail due to bad timing --> find solution
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, false);
+ assert!(admin.write_config(config).is_ok());
+
+ configure_totp(&admin, 1);
+ check_totp_codes(admin.deref(), 1, TotpTimestampSize::U32);
+
+ configure_totp(&admin, 2);
+ check_totp_codes(admin.deref(), 2, TotpTimestampSize::U32);
+
+ configure_totp(&admin, 1);
+ check_totp_codes(&admin.device(), 1, TotpTimestampSize::U32);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+#[cfg_attr(feature = "test-storage", should_panic(expected = "assertion failed"))]
+// Nitrokey Storage does only support timestamps that fit in a 32-bit unsigned integer. Therefore
+// the last RFC test case is expected to fail.
+fn totp_no_pin_64() {
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, false);
+ assert!(admin.write_config(config).is_ok());
+
+ configure_totp(&admin, 1);
+ check_totp_codes(admin.deref(), 1, TotpTimestampSize::U64);
+
+ configure_totp(&admin, 2);
+ check_totp_codes(admin.deref(), 2, TotpTimestampSize::U64);
+
+ configure_totp(&admin, 1);
+ check_totp_codes(&admin.device(), 1, TotpTimestampSize::U64);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn totp_pin() {
+ // TODO: this test may fail due to bad timing --> find solution
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, true);
+ assert!(admin.write_config(config).is_ok());
+
+ configure_totp(&admin, 1);
+ let user = admin.device().authenticate_user(USER_PASSWORD).unwrap();
+ check_totp_codes(&user, 1, TotpTimestampSize::U32);
+
+ assert!(user.device().get_totp_code(1).is_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+#[cfg_attr(feature = "test-storage", should_panic(expected = "assertion failed"))]
+// See comment for totp_no_pin_64.
+fn totp_pin_64() {
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, true);
+ assert!(admin.write_config(config).is_ok());
+
+ configure_totp(&admin, 1);
+ let user = admin.device().authenticate_user(USER_PASSWORD).unwrap();
+ check_totp_codes(&user, 1, TotpTimestampSize::U64);
+
+ assert!(user.device().get_totp_code(1).is_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn totp_slot_name() {
+ let admin = get_admin_test_device();
+ let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits);
+ assert!(admin.write_totp_slot(slot_data, 0).is_ok());
+
+ let device = admin.device();
+ let result = device.get_totp_slot_name(1);
+ assert!(result.is_ok());
+ assert_eq!("test-totp", result.unwrap());
+ let result = device.get_totp_slot_name(16);
+ assert_eq!(CommandError::InvalidSlot, result.unwrap_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn totp_error() {
+ let admin = get_admin_test_device();
+ let slot_data = OtpSlotData::new(1, "", HOTP_SECRET, OtpMode::SixDigits);
+ assert_eq!(
+ Err(CommandError::NoName),
+ admin.write_hotp_slot(slot_data, 0)
+ );
+ let slot_data = OtpSlotData::new(4, "test", HOTP_SECRET, OtpMode::SixDigits);
+ assert_eq!(
+ Err(CommandError::InvalidSlot),
+ admin.write_hotp_slot(slot_data, 0)
+ );
+ let code = admin.get_hotp_code(4);
+ assert_eq!(CommandError::InvalidSlot, code.unwrap_err());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn totp_erase() {
+ let admin = get_admin_test_device();
+ let config = Config::new(None, None, None, false);
+ assert!(admin.write_config(config).is_ok());
+ let slot_data = OtpSlotData::new(1, "test1", TOTP_SECRET, OtpMode::SixDigits);
+ assert!(admin.write_totp_slot(slot_data, 0).is_ok());
+ let slot_data = OtpSlotData::new(2, "test2", TOTP_SECRET, OtpMode::SixDigits);
+ assert!(admin.write_totp_slot(slot_data, 0).is_ok());
+
+ assert!(admin.erase_totp_slot(1).is_ok());
+
+ let device = admin.device();
+ let result = device.get_totp_slot_name(1);
+ assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err());
+ let result = device.get_totp_code(1);
+ assert_eq!(CommandError::SlotNotProgrammed, result.unwrap_err());
+
+ assert_eq!("test2", device.get_totp_slot_name(2).unwrap());
+}
diff --git a/nitrokey/src/tests/pws.rs b/nitrokey/src/tests/pws.rs
new file mode 100644
index 0000000..f581515
--- /dev/null
+++ b/nitrokey/src/tests/pws.rs
@@ -0,0 +1,143 @@
+use device::Device;
+use nitrokey_sys;
+use pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT};
+use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD};
+use util::{result_from_string, CommandError};
+
+fn get_pws(device: &Target) -> PasswordSafe {
+ device.get_password_safe(USER_PASSWORD).unwrap()
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn enable() {
+ let device = Target::connect().unwrap();
+ assert!(
+ device
+ .get_password_safe(&(USER_PASSWORD.to_owned() + "123"))
+ .is_err()
+ );
+ assert!(device.get_password_safe(USER_PASSWORD).is_ok());
+ assert!(device.get_password_safe(ADMIN_PASSWORD).is_err());
+ assert!(device.get_password_safe(USER_PASSWORD).is_ok());
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn drop() {
+ let device = Target::connect().unwrap();
+ {
+ let pws = get_pws(&device);
+ assert!(pws.write_slot(1, "name", "login", "password").is_ok());
+ assert_eq!("name", pws.get_slot_name(1).unwrap());
+ let result = result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(1) });
+ assert_eq!(Ok(String::from("name")), result);
+ }
+ let result = result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(1) });
+ assert_eq!(Ok(String::from("name")), result);
+ assert!(device.lock().is_ok());
+ let result = result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(1) });
+ assert_eq!(Err(CommandError::NotAuthorized), result);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn get_status() {
+ let device = Target::connect().unwrap();
+ let pws = get_pws(&device);
+ for i in 0..SLOT_COUNT {
+ assert!(pws.erase_slot(i).is_ok(), "Could not erase slot {}", i);
+ }
+ let status = pws.get_slot_status().unwrap();
+ assert_eq!(status, [false; SLOT_COUNT as usize]);
+
+ assert!(pws.write_slot(1, "name", "login", "password").is_ok());
+ let status = pws.get_slot_status().unwrap();
+ for i in 0..SLOT_COUNT {
+ assert_eq!(i == 1, status[i as usize]);
+ }
+
+ for i in 0..SLOT_COUNT {
+ assert!(pws.write_slot(i, "name", "login", "password").is_ok());
+ }
+ let status = pws.get_slot_status().unwrap();
+ assert_eq!(status, [true; SLOT_COUNT as usize]);
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn get_data() {
+ let device = Target::connect().unwrap();
+ let pws = get_pws(&device);
+ assert!(pws.write_slot(1, "name", "login", "password").is_ok());
+ assert_eq!("name", pws.get_slot_name(1).unwrap());
+ assert_eq!("login", pws.get_slot_login(1).unwrap());
+ assert_eq!("password", pws.get_slot_password(1).unwrap());
+
+ assert!(pws.erase_slot(1).is_ok());
+ // TODO: check error codes
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(1));
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_login(1));
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_password(1));
+
+ let name = "with å";
+ let login = "pär@test.com";
+ let password = "'i3lJc[09?I:,[u7dWz9";
+ assert!(pws.write_slot(1, name, login, password).is_ok());
+ assert_eq!(name, pws.get_slot_name(1).unwrap());
+ assert_eq!(login, pws.get_slot_login(1).unwrap());
+ assert_eq!(password, pws.get_slot_password(1).unwrap());
+
+ assert_eq!(
+ Err(CommandError::InvalidSlot),
+ pws.get_slot_name(SLOT_COUNT)
+ );
+ assert_eq!(
+ Err(CommandError::InvalidSlot),
+ pws.get_slot_login(SLOT_COUNT)
+ );
+ assert_eq!(
+ Err(CommandError::InvalidSlot),
+ pws.get_slot_password(SLOT_COUNT)
+ );
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn write() {
+ let device = Target::connect().unwrap();
+ let pws = get_pws(&device);
+
+ assert_eq!(
+ Err(CommandError::InvalidSlot),
+ pws.write_slot(SLOT_COUNT, "name", "login", "password")
+ );
+
+ assert!(pws.write_slot(0, "", "login", "password").is_ok());
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(0));
+ assert_eq!(Ok(String::from("login")), pws.get_slot_login(0));
+ assert_eq!(Ok(String::from("password")), pws.get_slot_password(0));
+
+ assert!(pws.write_slot(0, "name", "", "password").is_ok());
+ assert_eq!(Ok(String::from("name")), pws.get_slot_name(0));
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_login(0));
+ assert_eq!(Ok(String::from("password")), pws.get_slot_password(0));
+
+ assert!(pws.write_slot(0, "name", "login", "").is_ok());
+ assert_eq!(Ok(String::from("name")), pws.get_slot_name(0));
+ assert_eq!(Ok(String::from("login")), pws.get_slot_login(0));
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_password(0));
+}
+
+#[test]
+#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)]
+fn erase() {
+ let device = Target::connect().unwrap();
+ let pws = get_pws(&device);
+ assert_eq!(Err(CommandError::InvalidSlot), pws.erase_slot(SLOT_COUNT));
+
+ assert!(pws.write_slot(0, "name", "login", "password").is_ok());
+ assert!(pws.erase_slot(0).is_ok());
+ assert!(pws.erase_slot(0).is_ok());
+ assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(0));
+}
diff --git a/nitrokey/src/tests/util.rs b/nitrokey/src/tests/util.rs
new file mode 100644
index 0000000..c6fbb8f
--- /dev/null
+++ b/nitrokey/src/tests/util.rs
@@ -0,0 +1,11 @@
+pub static ADMIN_PASSWORD: &str = "12345678";
+pub static USER_PASSWORD: &str = "123456";
+
+#[cfg(feature = "test-no-device")]
+pub type Target = ::Pro;
+
+#[cfg(feature = "test-pro")]
+pub type Target = ::Pro;
+
+#[cfg(feature = "test-storage")]
+pub type Target = ::Storage;
diff --git a/nitrokey/src/util.rs b/nitrokey/src/util.rs
new file mode 100644
index 0000000..6f4fbb0
--- /dev/null
+++ b/nitrokey/src/util.rs
@@ -0,0 +1,171 @@
+use libc::{c_void, free};
+use nitrokey_sys;
+use rand::{OsRng, Rng};
+use std;
+use std::ffi::{CStr, CString};
+use std::fmt;
+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 decryption failed.
+ 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,
+}
+
+/// Log level for libnitrokey.
+///
+/// Setting the log level to a lower level enables all output from higher levels too. Currently,
+/// only the log levels `Warning`, `DebugL1`, `Debug` and `DebugL2` are actually used.
+#[derive(Debug, PartialEq)]
+pub enum LogLevel {
+ /// Error messages. Currently not used.
+ Error,
+ /// Warning messages.
+ Warning,
+ /// Informational messages. Currently not used.
+ Info,
+ /// Basic debug messages, especially basic information on the sent and received packets.
+ DebugL1,
+ /// Detailed debug messages, especially detailed information on the sent and received packets.
+ Debug,
+ /// Very detailed debug messages, especially detailed information about the control flow for
+ /// device communication (for example function entries and exits).
+ 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);
+ free(ptr as *mut c_void);
+ if s.is_empty() {
+ return Err(get_last_error());
+ }
+ return Ok(s);
+ }
+}
+
+pub fn get_command_result(value: c_int) -> Result<(), CommandError> {
+ match value {
+ 0 => Ok(()),
+ other => Err(CommandError::from(other)),
+ }
+}
+
+pub fn get_last_result() -> Result<(), CommandError> {
+ let value = unsafe { nitrokey_sys::NK_get_last_command_status() } as c_int;
+ get_command_result(value)
+}
+
+pub fn get_last_error() -> CommandError {
+ return match get_last_result() {
+ Ok(()) => CommandError::Unknown,
+ Err(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);
+}
+
+pub fn get_cstring<T: Into<Vec<u8>>>(s: T) -> Result<CString, CommandError> {
+ CString::new(s).or(Err(CommandError::InvalidString))
+}
+
+impl fmt::Display for CommandError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let msg = match *self {
+ CommandError::WrongCrc => "A packet with a wrong checksum has been sent or received",
+ CommandError::WrongSlot => "The given OTP slot does not exist",
+ CommandError::SlotNotProgrammed => "The given OTP slot is not programmed",
+ CommandError::WrongPassword => "The given password is wrong",
+ CommandError::NotAuthorized => {
+ "You are not authorized for this command or provided a wrong temporary password"
+ }
+ CommandError::Timestamp => "An error occured when getting or setting the time",
+ CommandError::NoName => "You did not provide a name for the OTP slot",
+ CommandError::NotSupported => "This command is not supported by this device",
+ CommandError::UnknownCommand => "This command is unknown",
+ CommandError::AesDecryptionFailed => "AES decryption failed",
+ CommandError::Unknown => "An unknown error occured",
+ CommandError::InvalidString => "You passed a string containing a null byte",
+ CommandError::InvalidSlot => "The given slot is invalid",
+ CommandError::RngError => "An error occured during random number generation",
+ };
+ write!(f, "{}", msg)
+ }
+}
+
+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 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,
+ }
+ }
+}