From a8517d9707e5ef313d6f4d69b51d21251c82ea91 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 19 May 2018 19:09:42 +0000 Subject: Initial commit --- .gitignore | 5 + Cargo.toml | 20 + LICENSE | 21 + README.md | 64 +++ TODO.md | 42 ++ nitrokey-sys/Cargo.toml | 16 + nitrokey-sys/build.rs | 17 + nitrokey-sys/src/lib.rs | 29 ++ nitrokey-sys/wrapper.h | 2 + src/lib.rs | 1303 +++++++++++++++++++++++++++++++++++++++++++++++ src/tests/mod.rs | 2 + src/tests/no_device.rs | 9 + src/tests/pro.rs | 268 ++++++++++ 13 files changed, 1798 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TODO.md create mode 100644 nitrokey-sys/Cargo.toml create mode 100644 nitrokey-sys/build.rs create mode 100644 nitrokey-sys/src/lib.rs create mode 100644 nitrokey-sys/wrapper.h create mode 100644 src/lib.rs create mode 100644 src/tests/mod.rs create mode 100644 src/tests/no_device.rs create mode 100644 src/tests/pro.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd437bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + +/target +/nitrokey-sys/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..652f454 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nitrokey" +version = "0.1.0" +authors = ["Robin Krahl "] +homepage = "https://code.ireas.org/nitrokey-rs/" +repository = "https://git.ireas.org/nitrokey-rs/" +description = "Bindings to libnitrokey for communication with Nitrokey devices" +keywords = ["nitrokey", "otp"] +categories = ["api-bindings"] +license = "MIT" + +[features] +default = ["test-no-device"] +test-no-device = [] +test-pro = [] + +[dependencies] +libc = "0.2" +nitrokey-sys = { path = "nitrokey-sys" } +rand = "0.4" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a3601d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Robin Krahl + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cd8e24 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# nitrokey-rs + +A libnitrokey wrapper for Rust providing access to Nitrokey devices. + +[Documentation][] + +```toml +[dependencies] +nitrokey = "0.1.0" +``` + +## Compatibility + +In order to use this crate, a [`libnitrokey`][] installation is required +(both development headers and library). The crate is developed using version +3.2, but any newer version should work too. + +As I only have access to a Nitrokey Pro, this crate only provides support for +the Nitrokey Pro methods. If you want to contribute for the Nitrokey Storage, +please send a mail to [nitrokey-rs-dev@ireas.org][]. + +### Unsupported Functions + +The following functions provided by `libnitrokey` are deliberately not +supported by `nitrokey-rs`: + +- `NK_get_time()`. This method is useless as it will always cause a timestamp + error on the device (see [pull request #114][] for `libnitrokey` for details). +- `NK_get_status()`. This method only provides a string representation of + data that can be accessed by other methods (firmware version, seriel number, + configuration). + +## Tests + +The default test suite assumes that no Nitrokey device is connected and only +performs minor sanity checks. There is another test suite that assumes that a +Nitrokey Pro is connected (admin password `12345678`, user password `123456`). +To execute this test suite, run `cargo test --no-default-features --features +test-pro -- --test-threads 1`. Note that this test suite might lock your stick +if you have different passwords! + +The `totp` and `totp_pin` tests can occasionally fail due to bad timing. Also +make sure to run the tests sequentially (`--test-threads 1`), otherwise they +might interfere. + +The `get_major_firmware_version` test will fail for newer `libnitrokey` +versions as it relies on buggy behavior in version 3.2. + +## Contact + +For bug reports, patches, feature requests or other messages, please send a +mail to [nitrokey-rs-dev@ireas.org][]. + +## License + +This project is licensed under the [MIT License][]. `libnitrokey` is licensed +under the [LGPL-3.0][]. + +[Documentation]: https://docs.rs/nitrokey +[`libnitrokey`]: https://github.com/nitrokey/libnitrokey +[nitrokey-rs-dev@ireas.org]: mailto:nitrokey-rs-dev@ireas.org +[pull request #114]: https://github.com/Nitrokey/libnitrokey/pull/114 +[MIT license]: https://opensource.org/licenses/MIT +[LGPL-3.0]: https://opensource.org/licenses/lgpl-3.0.html diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ca239c8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +- Fix segmentation faults when freeing string literals with old Nitrokey + versions. +- Add support and tests for the Nitrokey Storage. +- Add support for the currently unsupported commands: + - `NK_lock_device` + - `NK_factory_reset` + - `NK_build_aes_key` + - `NK_unlock_user_password` + - `NK_erase_hotp_slot` + - `NK_erase_totp_slot` + - `NK_change_admin_PIN` + - `NK_change_user_PIN` + - `NK_enable_password_safe` + - `NK_get_password_safe_slot_status` + - `NK_get_password_safe_slot_name` + - `NK_get_password_safe_slot_login` + - `NK_get_password_safe_slot_password` + - `NK_write_password_safe_slot` + - `NK_erase_password_safe_slot` + - `NK_is_AES_supported` + - `NK_send_startup` + - `NK_unlock_encrypted_volume` + - `NK_lock_encrypted_volume` + - `NK_unlock_hidden_volume` + - `NK_lock_hidden_volume` + - `NK_create_hidden_volume` + - `NK_set_unencrypted_read_only` + - `NK_set_unencrypted_read_write` + - `NK_export_firmware` + - `NK_clear_new_sd_card_warning` + - `NK_fill_SD_card_with_random_data` + - `NK_change_update_password` + - `NK_get_status_storage_as_string` + - `NK_get_SD_usage_data_as_string` + - `NK_get_progress_bar_value` +- Fix timing issues with the `totp` and `totp_pin` test cases. +- Fix the inconsistent method `get_major_firmware_version`. +- Consider implementing `Drop` instead of the method `disconnect`. +- Find an example for `set_time`, also adapt `get_totp_code`. +- Improve log level documentation. +- Clear passwords from memory. +- Find a nicer syntax for the `write_config` test. diff --git a/nitrokey-sys/Cargo.toml b/nitrokey-sys/Cargo.toml new file mode 100644 index 0000000..e8ff489 --- /dev/null +++ b/nitrokey-sys/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nitrokey-sys" +version = "0.1.0" +authors = ["Robin Krahl "] +homepage = "https://code.ireas.org/nitrokey-rs/" +repository = "https://git.ireas.org/nitrokey-rs/" +description = "Bindings to libnitrokey for communication with Nitrokey devices" +categories = ["external-ffi-bindings"] +license = "MIT" +links = "nitrokey" +build = "build.rs" + +[dependencies] + +[build-dependencies] +bindgen = "0.26.3" diff --git a/nitrokey-sys/build.rs b/nitrokey-sys/build.rs new file mode 100644 index 0000000..7b3325c --- /dev/null +++ b/nitrokey-sys/build.rs @@ -0,0 +1,17 @@ +extern crate bindgen; + +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rustc-link-lib=nitrokey"); + + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .generate() + .expect("Unable to generate bindings"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Could not write bindings"); +} diff --git a/nitrokey-sys/src/lib.rs b/nitrokey-sys/src/lib.rs new file mode 100644 index 0000000..0641e13 --- /dev/null +++ b/nitrokey-sys/src/lib.rs @@ -0,0 +1,29 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CString; + + #[test] + fn login_auto() { + unsafe { + assert_eq!(0, NK_login_auto()); + } + } + + #[test] + fn login() { + unsafe { + // Unconnected + assert_eq!(0, NK_login(CString::new("S").unwrap().as_ptr())); + assert_eq!(0, NK_login(CString::new("P").unwrap().as_ptr())); + // Unsupported model + assert_eq!(0, NK_login(CString::new("T").unwrap().as_ptr())); + } + } +} diff --git a/nitrokey-sys/wrapper.h b/nitrokey-sys/wrapper.h new file mode 100644 index 0000000..3c8d7cf --- /dev/null +++ b/nitrokey-sys/wrapper.h @@ -0,0 +1,2 @@ +#include +#include diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fdc7954 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1303 @@ +//! Provides access to a Nitrokey device using the native libnitrokey API. +//! +//! # Usage +//! +//! Operations on the Nitrokey require different authentication levels. Some +//! operations can be performed without authentication, some require user +//! access, and some require admin access. This is modelled using the types +//! [`UnauthenticatedDevice`][], [`UserAuthenticatedDevice`][] and +//! [`AdminAuthenticatedDevice`][]. +//! +//! Use [`connect`][] or [`connect_model`][] to obtain an +//! [`UnauthenticatedDevice`][]. You can then use [`authenticate_user`][] or +//! [`authenticate_admin`][] to get an authenticated device. You can then use +//! [`device`][] to go back to the unauthenticated device. +//! +//! This makes sure that you can only execute a command if you have the +//! required access rights. Otherwise, your code will not compile. The only +//! exception are the methods to generate one-time passwords – +//! [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick +//! configuration, these operations are available without authentication or +//! with user authentication. +//! +//! Per default, libnitrokey writes log messages, for example the packets that +//! are sent to and received from the stick, to the standard output. To +//! change this behaviour, use [`set_debug`][] or [`set_log_level`][]. +//! +//! # Examples +//! +//! Connect to any Nitrokey and print its serial number: +//! +//! ```no_run +//! use nitrokey::Device; +//! # use nitrokey::CommandError; +//! +//! # fn try_main() -> Result<(), CommandError> { +//! let device = nitrokey::connect()?; +//! println!("{}", device.get_serial_number()?); +//! # Ok(()) +//! # } +//! ``` +//! +//! Configure an HOTP slot: +//! +//! ```no_run +//! use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; +//! # use nitrokey::CommandError; +//! +//! # fn try_main() -> Result<(), (CommandError)> { +//! let device = nitrokey::connect()?; +//! let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); +//! match device.authenticate_admin("12345678") { +//! Ok(admin) => { +//! match admin.write_hotp_slot(slot_data, 0) { +//! CommandStatus::Success => println!("Successfully wrote slot."), +//! CommandStatus::Error(err) => println!("Could not write slot: {:?}", err), +//! } +//! }, +//! Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! Generate an HOTP one-time password: +//! +//! ```no_run +//! use nitrokey::Device; +//! # use nitrokey::CommandError; +//! +//! # fn try_main() -> Result<(), (CommandError)> { +//! let device = nitrokey::connect()?; +//! match device.get_hotp_code(1) { +//! Ok(code) => println!("Generated HOTP code: {:?}", code), +//! Err(err) => println!("Could not generate HOTP code: {:?}", err), +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! [`authenticate_admin`]: struct.UnauthenticatedDevice.html#method.authenticate_admin +//! [`authenticate_user`]: struct.UnauthenticatedDevice.html#method.authenticate_user +//! [`connect`]: fn.connect.html +//! [`connect_model`]: fn.connect_model.html +//! [`device`]: struct.AuthenticatedDevice.html#method.device +//! [`get_hotp_code`]: trait.Device.html#method.get_hotp_code +//! [`get_totp_code`]: trait.Device.html#method.get_totp_code +//! [`set_debug`]: fn.set_debug.html +//! [`set_log_level`]: fn.set_log_level.html +//! [`AdminAuthenticatedDevice`]: struct.AdminAuthenticatedDevice.html +//! [`UserAuthenticatedDevice`]: struct.UserAuthenticatedDevice.html +//! [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html + +extern crate libc; +extern crate nitrokey_sys; +extern crate rand; + +use std::ffi::CString; +use std::ffi::CStr; +use libc::c_int; +use rand::Rng; + +#[cfg(test)] +mod tests; + +/// Modes for one-time password generation. +#[derive(Debug, PartialEq)] +pub enum OtpMode { + /// Generate one-time passwords with six digits. + SixDigits, + /// Generate one-time passwords with eight digits. + EightDigits, +} + +/// Error types returned by Nitrokey device or by the library. +#[derive(Debug, PartialEq)] +pub enum CommandError { + /// A packet with a wrong checksum has been sent or received. + WrongCrc, + /// A command tried to access an OTP slot that does not exist. + WrongSlot, + /// A command tried to generate an OTP on a slot that is not configured. + SlotNotProgrammed, + /// The provided password is wrong. + WrongPassword, + /// You are not authorized for this command or provided a wrong temporary + /// password. + NotAuthorized, + /// An error occured when getting or setting the time. + Timestamp, + /// You did not provide a name for the OTP slot. + NoName, + /// This command is not supported by this device. + NotSupported, + /// This command is unknown. + UnknownCommand, + /// AES decryptionfailed. + AesDecryptionFailed, + /// An unknown error occured. + Unknown, + /// You passed a string containing a null byte. + InvalidString, + /// You passed an invalid slot. + InvalidSlot, + /// An error occured during random number generation. + RngError, +} + +/// Command execution status. +#[derive(Debug, PartialEq)] +pub enum CommandStatus { + /// The command was successful. + Success, + /// An error occured during command execution. + Error(CommandError), +} + +/// Log level for libnitrokey. +#[derive(Debug, PartialEq)] +pub enum LogLevel { + /// Only log error messages. + Error, + /// Log error messages and warnings. + Warning, + /// Log error messages, warnings and info messages. + Info, + /// Log error messages, warnings, info messages and debug messages. + DebugL1, + /// Log error messages, warnings, info messages and detailed debug + /// messages. + Debug, + /// Log error messages, warnings, info messages and very detailed debug + /// messages. + DebugL2, +} + +/// Available Nitrokey models. +#[derive(Debug, PartialEq)] +pub enum Model { + /// The Nitrokey Storage. + Storage, + /// The Nitrokey Pro. + Pro, +} + +/// The configuration for a Nitrokey. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Config { + /// If set, the stick will generate a code from the HOTP slot with the + /// given number if numlock is pressed. The slot number must be 0, 1 or 2. + pub numlock: Option, + /// If set, the stick will generate a code from the HOTP slot with the + /// given number if capslock is pressed. The slot number must be 0, 1 or 2. + pub capslock: Option, + /// If set, the stick will generate a code from the HOTP slot with the + /// given number if scrollock is pressed. The slot number must be 0, 1 or 2. + pub scrollock: Option, + /// If set, OTP generation using [`get_hotp_code`][] or [`get_totp_code`][] + /// requires user authentication. Otherwise, OTPs can be generated without + /// authentication. + /// + /// [`get_hotp_code`]: struct.Device.html#method.get_hotp_code + /// [`get_totp_code`]: struct.Device.html#method.get_totp_code + pub user_password: bool, +} + +#[derive(Debug)] +struct RawConfig { + pub numlock: u8, + pub capslock: u8, + pub scrollock: u8, + pub user_password: bool, +} + +#[derive(Debug)] +/// A Nitrokey device without user or admin authentication. +/// +/// Use [`connect`][] or [`connect_model`][] to obtain an instance. If you +/// want to execute a command that requires user or admin authentication, +/// use [`authenticate_admin`][] or [`authenticate_user`][]. +/// +/// # Examples +/// +/// Authentication with error handling: +/// +/// ```no_run +/// use nitrokey::{UnauthenticatedDevice, UserAuthenticatedDevice}; +/// # use nitrokey::CommandError; +/// +/// fn perform_user_task(device: &UserAuthenticatedDevice) {} +/// fn perform_other_task(device: &UnauthenticatedDevice) {} +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::connect()?; +/// let device = match device.authenticate_user("123456") { +/// Ok(user) => { +/// perform_user_task(&user); +/// user.device() +/// }, +/// Err((device, err)) => { +/// println!("Could not authenticate as user: {:?}", err); +/// device +/// }, +/// }; +/// perform_other_task(&device); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`authenticate_admin`]: #method.authenticate_admin +/// [`authenticate_user`]: #method.authenticate_user +/// [`connect`]: fn.connect.html +/// [`connect_model`]: fn.connect_model.html +pub struct UnauthenticatedDevice {} + +/// A Nitrokey device with user authentication. +/// +/// To obtain an instance of this struct, use the [`authenticate_user`][] +/// method on an [`UnauthenticatedDevice`][]. To get back to an +/// unauthenticated device, use the [`device`][] method. +/// +/// [`authenticate_admin`]: struct.UnauthenticatedDevice#method.authenticate_admin +/// [`device`]: #method.device +/// [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html +pub struct UserAuthenticatedDevice { + device: UnauthenticatedDevice, + temp_password: Vec, +} + +/// A Nitrokey device with admin authentication. +/// +/// To obtain an instance of this struct, use the [`authenticate_admin`][] +/// method on an [`UnauthenticatedDevice`][]. To get back to an +/// unauthenticated device, use the [`device`][] method. +/// +/// [`authenticate_admin`]: struct.UnauthenticatedDevice#method.authenticate_admin +/// [`device`]: #method.device +/// [`UnauthenticatedDevice`]: struct.UnauthenticatedDevice.html +pub struct AdminAuthenticatedDevice { + device: UnauthenticatedDevice, + temp_password: Vec, +} + +/// The configuration for an OTP slot. +#[derive(Debug)] +pub struct OtpSlotData { + /// The number of the slot – must be less than three for HOTP and less than + /// 15 for TOTP. + pub number: u8, + /// The name of the slot – must not be empty. + pub name: String, + /// The secret for the slot. + pub secret: String, + /// The OTP generation mode. + pub mode: OtpMode, + /// If true, press the enter key after sending an OTP code using double-pressed + /// numlock, capslock or scrolllock. + pub use_enter: bool, + /// Set the token ID [OATH Token Identifier Specification][tokspec], section + /// “Class A”. + /// + /// [tokspec]: https://openauthentication.org/token-specs/ + pub token_id: Option, +} + +#[derive(Debug)] +struct RawOtpSlotData { + pub number: u8, + pub name: CString, + pub secret: CString, + pub mode: OtpMode, + pub use_enter: bool, + pub use_token_id: bool, + pub token_id: CString, +} + +static TEMPORARY_PASSWORD_LENGTH: usize = 25; + +/// A Nitrokey device. +/// +/// This trait provides the commands that can be executed without +/// authentication. The only exception are the [`get_hotp_code`][] and +/// [`get_totp_code`][] methods: It depends on the device configuration +/// ([`get_config`][]) whether these commands require user authentication +/// or not. +/// +/// [`get_config`]: #method.get_config +/// [`get_hotp_code`]: #method.get_hotp_code +/// [`get_totp_code`]: #method.get_totp_code +pub trait Device { + /// Closes the connection to this device. This method consumes the device. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// // perform tasks ... + /// device.disconnect(); + /// # Ok(()) + /// # } + /// ``` + fn disconnect(self) + where + Self: std::marker::Sized, + { + unsafe { + nitrokey_sys::NK_logout(); + } + } + + /// Sets the time on the Nitrokey. This command may set the time to + /// arbitrary values. `time` is the number of seconds since January 1st, + /// 1970 (Unix timestamp). + /// + /// The time is used for TOTP generation (see [`get_totp_code`][]). + /// + /// # Errors + /// + /// - [`Timestamp`][] if the time could not be set + /// + /// [`get_totp_code`]: #method.get_totp_code + /// [`Timestamp`]: enum.CommandError.html#variant.Timestamp + // TODO: example + fn set_time(&self, time: u64) -> CommandStatus { + unsafe { CommandStatus::from(nitrokey_sys::NK_totp_set_time(time)) } + } + + /// Returns the serial number of the Nitrokey device. The serial number + /// is the string representation of a hex number. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_serial_number() { + /// Ok(number) => println!("serial no: {:?}", number), + /// Err(err) => println!("Could not get serial number: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + fn get_serial_number(&self) -> Result { + unsafe { result_from_string(nitrokey_sys::NK_device_serial_number()) } + } + + /// Returns the name of the given HOTP slot. + /// + /// # Errors + /// + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{CommandError, Device}; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_hotp_slot_name(1) { + /// Ok(name) => println!("HOTP slot 1: {:?}", name), + /// Err(CommandError::SlotNotProgrammed) => println!("HOTP slot 1 not programmed"), + /// Err(err) => println!("Could not get slot name: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + fn get_hotp_slot_name(&self, slot: u8) -> Result { + unsafe { result_from_string(nitrokey_sys::NK_get_hotp_slot_name(slot)) } + } + + /// Returns the name of the given TOTP slot. + /// + /// # Errors + /// + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{CommandError, Device}; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_totp_slot_name(1) { + /// Ok(name) => println!("TOTP slot 1: {:?}", name), + /// Err(CommandError::SlotNotProgrammed) => println!("TOTP slot 1 not programmed"), + /// Err(err) => println!("Could not get slot name: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + fn get_totp_slot_name(&self, slot: u8) -> Result { + unsafe { result_from_string(nitrokey_sys::NK_get_totp_slot_name(slot)) } + } + + /// Returns the number of remaining authentication attempts for the user. The + /// total number of available attempts is three. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let count = device.get_user_retry_count(); + /// println!("{} remaining authentication attempts (user)", count); + /// # Ok(()) + /// # } + /// ``` + fn get_user_retry_count(&self) -> u8 { + unsafe { nitrokey_sys::NK_get_user_retry_count() } + } + + /// Returns the number of remaining authentication attempts for the admin. The + /// total number of available attempts is three. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let count = device.get_admin_retry_count(); + /// println!("{} remaining authentication attempts (admin)", count); + /// # Ok(()) + /// # } + /// ``` + fn get_admin_retry_count(&self) -> u8 { + unsafe { nitrokey_sys::NK_get_admin_retry_count() } + } + + /// Returns the major part of the firmware version (should be zero). + /// Note that this method is buggy for libnitrokey older than v3.3. For + /// these versions, this method returns the minor part. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// println!("Firmware version: {}.x", device.get_major_firmware_version()); + /// # Ok(()) + /// # } + /// ``` + fn get_major_firmware_version(&self) -> i32 { + unsafe { nitrokey_sys::NK_get_major_firmware_version() } + } + + /// Returns the current configuration of the Nitrokey device. + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let config = device.get_config()?; + /// println!("numlock binding: {:?}", config.numlock); + /// println!("capslock binding: {:?}", config.capslock); + /// println!("scrollock binding: {:?}", config.scrollock); + /// println!("require password for OTP: {:?}", config.user_password); + /// # Ok(()) + /// # } + /// ``` + fn get_config(&self) -> Result { + unsafe { + let config_ptr = nitrokey_sys::NK_read_config(); + if config_ptr.is_null() { + return Err(get_last_error()); + } + let config_array_ptr = config_ptr as *const [u8; 5]; + let raw_config = RawConfig::from(*config_array_ptr); + libc::free(config_ptr as *mut libc::c_void); + return Ok(raw_config.into()); + } + } + + /// Generates an HOTP code on the given slot. This operation may require + /// user authorization, depending on the device configuration (see + /// [`get_config`][]). + /// + /// # Errors + /// + /// - [`NotAuthorized`][] if OTP generation requires user authentication + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let code = device.get_hotp_code(1)?; + /// println!("Generated HOTP code on slot 1: {:?}", code); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`get_config`]: #method.get_config + /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + fn get_hotp_code(&self, slot: u8) -> Result { + unsafe { + return result_from_string(nitrokey_sys::NK_get_hotp_code(slot)); + } + } + + /// Generates a TOTP code on the given slot. This operation may require + /// user authorization, depending on the device configuration (see + /// [`get_config`][]). + /// + /// To make sure that the Nitrokey’s time is in sync, consider calling + /// [`set_time`][] before calling this method. + /// + /// # Errors + /// + /// - [`NotAuthorized`][] if OTP generation requires user authentication + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let code = device.get_totp_code(1)?; + /// println!("Generated TOTP code on slot 1: {:?}", code); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`set_time`]: #method.set_time + /// [`get_config`]: #method.get_config + /// [`NotAuthorized`]: enum.CommandError.html#variant.NotAuthorized + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + fn get_totp_code(&self, slot: u8) -> Result { + unsafe { + return result_from_string(nitrokey_sys::NK_get_totp_code(slot, 0, 0, 0)); + } + } +} + +trait AuthenticatedDevice { + fn new(device: UnauthenticatedDevice, temp_password: Vec) -> Self; +} + +/// Connects to a Nitrokey device. This method can be used to connect to any +/// connected device, both a Nitrokey Pro and a Nitrokey Storage. +/// +/// # Example +/// +/// ``` +/// use nitrokey::UnauthenticatedDevice; +/// +/// fn do_something(device: UnauthenticatedDevice) {} +/// +/// match nitrokey::connect() { +/// Ok(device) => do_something(device), +/// Err(err) => println!("Could not connect to a Nitrokey: {:?}", err), +/// } +/// ``` +pub fn connect() -> Result { + unsafe { + match nitrokey_sys::NK_login_auto() { + 1 => Ok(UnauthenticatedDevice {}), + _ => Err(CommandError::Unknown), + } + } +} + +/// Connects to a Nitrokey device of the given model. +/// +/// # Example +/// +/// ``` +/// use nitrokey::{Model, UnauthenticatedDevice}; +/// +/// fn do_something(device: UnauthenticatedDevice) {} +/// +/// match nitrokey::connect_model(Model::Pro) { +/// Ok(device) => do_something(device), +/// Err(err) => println!("Could not connect to a Nitrokey Pro: {:?}", err), +/// } +/// ``` +pub fn connect_model(model: Model) -> Result { + let model_string = match model { + Model::Storage => "S", + Model::Pro => "P", + }; + let model_cstring = CString::new(model_string); + if model_cstring.is_err() { + return Err(CommandError::InvalidString); + } + let model = model_cstring.unwrap(); + unsafe { + return match nitrokey_sys::NK_login(model.as_ptr()) { + 1 => Ok(UnauthenticatedDevice {}), + rv => Err(CommandError::from(rv)), + }; + } +} + +/// Enables or disables debug output. Calling this method with `true` is +/// equivalent to setting the log level to `Debug`; calling it with `false` is +/// equivalent to the log level `Error` (see [`set_log_level`][]). +/// +/// If debug output is enabled, detailed information about the communication +/// with the Nitrokey device is printed to the standard output. +/// +/// [`set_log_level`]: fn.set_log_level.html +pub fn set_debug(state: bool) { + unsafe { + nitrokey_sys::NK_set_debug(state); + } +} + +/// Sets the log level for libnitrokey. All log messages are written to the +/// standard output or standard errror. +pub fn set_log_level(level: LogLevel) { + unsafe { + nitrokey_sys::NK_set_debug_level(level.into()); + } +} + +fn config_otp_slot_to_option(value: u8) -> Option { + if value < 3 { + return Some(value); + } + None +} + +fn option_to_config_otp_slot(value: Option) -> Result { + match value { + Some(value) => { + if value < 3 { + Ok(value) + } else { + Err(CommandError::InvalidSlot) + } + } + None => Ok(255), + } +} + +impl From for CommandError { + fn from(value: c_int) -> Self { + match value { + 1 => CommandError::WrongCrc, + 2 => CommandError::WrongSlot, + 3 => CommandError::SlotNotProgrammed, + 4 => CommandError::WrongPassword, + 5 => CommandError::NotAuthorized, + 6 => CommandError::Timestamp, + 7 => CommandError::NoName, + 8 => CommandError::NotSupported, + 9 => CommandError::UnknownCommand, + 10 => CommandError::AesDecryptionFailed, + _ => CommandError::Unknown, + } + } +} + +impl From for CommandStatus { + fn from(value: c_int) -> Self { + match value { + 0 => CommandStatus::Success, + other => CommandStatus::Error(CommandError::from(other)), + } + } +} + +impl Into for LogLevel { + fn into(self) -> i32 { + match self { + LogLevel::Error => 0, + LogLevel::Warning => 1, + LogLevel::Info => 2, + LogLevel::DebugL1 => 3, + LogLevel::Debug => 4, + LogLevel::DebugL2 => 5, + } + } +} + +fn get_last_status() -> CommandStatus { + unsafe { + let status = nitrokey_sys::NK_get_last_command_status(); + return CommandStatus::from(status as c_int); + } +} + +fn get_last_error() -> CommandError { + return match get_last_status() { + CommandStatus::Success => CommandError::Unknown, + CommandStatus::Error(err) => err, + }; +} + +fn owned_str_from_ptr(ptr: *const std::os::raw::c_char) -> String { + unsafe { + return CStr::from_ptr(ptr).to_string_lossy().into_owned(); + } +} + +fn result_from_string(ptr: *const std::os::raw::c_char) -> Result { + if ptr.is_null() { + return Err(CommandError::Unknown); + } + unsafe { + let s = owned_str_from_ptr(ptr); + libc::free(ptr as *mut libc::c_void); + if s.is_empty() { + return Err(get_last_error()); + } + return Ok(s); + } +} + +fn generate_password(length: usize) -> std::io::Result> { + let mut rng = match rand::OsRng::new() { + Ok(rng) => rng, + Err(err) => return Err(err), + }; + let mut data = vec![0u8; length]; + rng.fill_bytes(&mut data[..]); + return Ok(data); +} + +impl OtpSlotData { + /// Constructs a new instance of this struct. + pub fn new(number: u8, name: &str, secret: &str, mode: OtpMode) -> OtpSlotData { + OtpSlotData { + number, + name: String::from(name), + secret: String::from(secret), + mode, + use_enter: false, + token_id: None, + } + } +} + +impl RawOtpSlotData { + pub fn new(data: OtpSlotData) -> Result { + let name = CString::new(data.name); + let secret = CString::new(data.secret); + let use_token_id = data.token_id.is_some(); + let token_id = CString::new(data.token_id.unwrap_or_else(String::new)); + if name.is_err() || secret.is_err() || token_id.is_err() { + return Err(CommandError::InvalidString); + } + + Ok(RawOtpSlotData { + number: data.number, + name: name.unwrap(), + secret: secret.unwrap(), + mode: data.mode, + use_enter: data.use_enter, + use_token_id, + token_id: token_id.unwrap(), + }) + } +} + +impl Config { + /// Constructs a new instance of this struct. + pub fn new( + numlock: Option, + capslock: Option, + scrollock: Option, + user_password: bool, + ) -> Config { + Config { + numlock, + capslock, + scrollock, + user_password, + } + } +} + +impl RawConfig { + fn try_from(config: Config) -> Result { + Ok(RawConfig { + numlock: option_to_config_otp_slot(config.numlock)?, + capslock: option_to_config_otp_slot(config.capslock)?, + scrollock: option_to_config_otp_slot(config.scrollock)?, + user_password: config.user_password, + }) + } +} + +impl From<[u8; 5]> for RawConfig { + fn from(data: [u8; 5]) -> Self { + RawConfig { + numlock: data[0], + capslock: data[1], + scrollock: data[2], + user_password: data[3] != 0, + } + } +} + +impl Into for RawConfig { + fn into(self) -> Config { + Config { + numlock: config_otp_slot_to_option(self.numlock), + capslock: config_otp_slot_to_option(self.capslock), + scrollock: config_otp_slot_to_option(self.scrollock), + user_password: self.user_password, + } + } +} + +impl UnauthenticatedDevice { + fn authenticate( + self, + password: &str, + callback: T, + ) -> Result + where + D: AuthenticatedDevice, + T: Fn(*const i8, *const i8) -> c_int, + { + let temp_password = match generate_password(TEMPORARY_PASSWORD_LENGTH) { + Ok(pw) => pw, + Err(_) => return Err((self, CommandError::RngError)), + }; + let password = CString::new(password); + if password.is_err() { + return Err((self, CommandError::InvalidString)); + } + + let pw = password.unwrap(); + let password_ptr = pw.as_ptr(); + let temp_password_ptr = temp_password.as_ptr() as *const i8; + return match callback(password_ptr, temp_password_ptr) { + 0 => Ok(D::new(self, temp_password)), + rv => Err((self, CommandError::from(rv))), + }; + } + + /// Performs user authentication. This method consumes the device. If + /// successful, an authenticated device is returned. Otherwise, the + /// current unauthenticated device and the error are returned. + /// + /// This method generates a random temporary password that is used for all + /// operations that require user access. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided user password contains a null byte + /// - [`RngError`][] if the generation of the temporary password failed + /// - [`WrongPassword`][] if the provided user password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{UnauthenticatedDevice, UserAuthenticatedDevice}; + /// # use nitrokey::CommandError; + /// + /// fn perform_user_task(device: &UserAuthenticatedDevice) {} + /// fn perform_other_task(device: &UnauthenticatedDevice) {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let device = match device.authenticate_user("123456") { + /// Ok(user) => { + /// perform_user_task(&user); + /// user.device() + /// }, + /// Err((device, err)) => { + /// println!("Could not authenticate as user: {:?}", err); + /// device + /// }, + /// }; + /// perform_other_task(&device); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`RngError`]: enum.CommandError.html#variant.RngError + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + pub fn authenticate_user( + self, + password: &str, + ) -> Result { + return self.authenticate(password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_user_authenticate(password_ptr, temp_password_ptr) + }); + } + + /// Performs admin authentication. This method consumes the device. If + /// successful, an authenticated device is returned. Otherwise, the + /// current unauthenticated device and the error are returned. + /// + /// This method generates a random temporary password that is used for all + /// operations that require admin access. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided admin password contains a null byte + /// - [`RngError`][] if the generation of the temporary password failed + /// - [`WrongPassword`][] if the provided admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{AdminAuthenticatedDevice, UnauthenticatedDevice}; + /// # use nitrokey::CommandError; + /// + /// fn perform_admin_task(device: &AdminAuthenticatedDevice) {} + /// fn perform_other_task(device: &UnauthenticatedDevice) {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let device = match device.authenticate_admin("123456") { + /// Ok(admin) => { + /// perform_admin_task(&admin); + /// admin.device() + /// }, + /// Err((device, err)) => { + /// println!("Could not authenticate as admin: {:?}", err); + /// device + /// }, + /// }; + /// perform_other_task(&device); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`RngError`]: enum.CommandError.html#variant.RngError + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + pub fn authenticate_admin( + self, + password: &str, + ) -> Result { + return self.authenticate(password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr) + }); + } +} + +impl Device for UnauthenticatedDevice {} + +impl UserAuthenticatedDevice { + /// Forgets the user authentication and returns an unauthenticated + /// device. This method consumes the authenticated device. It does not + /// perform any actual commands on the Nitrokey. + pub fn device(self) -> UnauthenticatedDevice { + self.device + } +} + +impl Device for UserAuthenticatedDevice { + /// Generates an HOTP code on the given slot. This operation may not + /// require user authorization, depending on the device configuration (see + /// [`get_config`][]). + /// + /// # Errors + /// + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.authenticate_user("123456") { + /// Ok(user) => { + /// let code = user.get_hotp_code(1)?; + /// println!("Generated HOTP code on slot 1: {:?}", code); + /// }, + /// Err(err) => println!("Could not authenticate: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`get_config`]: #method.get_config + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + fn get_hotp_code(&self, slot: u8) -> Result { + unsafe { + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + return result_from_string(nitrokey_sys::NK_get_hotp_code_PIN(slot, temp_password_ptr)); + } + } + + /// Generates a TOTP code on the given slot. This operation may not + /// require user authorization, depending on the device configuration (see + /// [`get_config`][]). + /// + /// To make sure that the Nitrokey’s time is in sync, consider calling + /// [`set_time`][] before calling this method. + /// + /// # Errors + /// + /// - [`SlotNotProgrammed`][] if the given slot is not configured + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Device; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.authenticate_user("123456") { + /// Ok(user) => { + /// let code = user.get_totp_code(1)?; + /// println!("Generated TOTP code on slot 1: {:?}", code); + /// }, + /// Err(err) => println!("Could not authenticate: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`get_config`]: #method.get_config + /// [`set_time`]: trait.Device.html#method.set_time + /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + fn get_totp_code(&self, slot: u8) -> Result { + unsafe { + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + return result_from_string(nitrokey_sys::NK_get_totp_code_PIN( + slot, + 0, + 0, + 0, + temp_password_ptr, + )); + } + } +} + +impl AuthenticatedDevice for UserAuthenticatedDevice { + fn new(device: UnauthenticatedDevice, temp_password: Vec) -> Self { + UserAuthenticatedDevice { + device, + temp_password, + } + } +} + +impl AdminAuthenticatedDevice { + /// Forgets the user authentication and returns an unauthenticated + /// device. This method consumes the authenticated device. It does not + /// perform any actual commands on the Nitrokey. + pub fn device(self) -> UnauthenticatedDevice { + self.device + } + + /// Writes the given configuration to the Nitrokey device. + /// + /// # Errors + /// + /// - [`InvalidSlot`][] if the provided numlock, capslock or scrolllock + /// slot is larger than two + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::Config; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let config = Config::new(None, None, None, false); + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// admin.write_config(config); + /// () + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + pub fn write_config(&self, config: Config) -> CommandStatus { + let raw_config = match RawConfig::try_from(config) { + Ok(raw_config) => raw_config, + Err(err) => return CommandStatus::Error(err), + }; + unsafe { + let rv = nitrokey_sys::NK_write_config( + raw_config.numlock, + raw_config.capslock, + raw_config.scrollock, + raw_config.user_password, + false, + self.temp_password.as_ptr() as *const i8, + ); + return CommandStatus::from(rv); + } + } + + fn write_otp_slot(&self, data: OtpSlotData, callback: T) -> CommandStatus + where + T: Fn(RawOtpSlotData, *const i8) -> c_int, + { + let raw_data = match RawOtpSlotData::new(data) { + Ok(raw_data) => raw_data, + Err(err) => return CommandStatus::Error(err), + }; + let temp_password_ptr = self.temp_password.as_ptr() as *const i8; + let rv = callback(raw_data, temp_password_ptr); + return CommandStatus::from(rv); + } + + /// Configure an HOTP slot with the given data and set the HOTP counter to + /// the given value (default 0). + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided token ID contains a null byte + /// - [`NoName`][] if the provided name is empty + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), (CommandError)> { + /// let device = nitrokey::connect()?; + /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::SixDigits); + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// match admin.write_hotp_slot(slot_data, 0) { + /// CommandStatus::Success => println!("Successfully wrote slot."), + /// CommandStatus::Error(err) => println!("Could not write slot: {:?}", err), + /// } + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`NoName`]: enum.CommandError.html#variant.NoName + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + pub fn write_hotp_slot(&self, data: OtpSlotData, counter: u64) -> CommandStatus { + return self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| unsafe { + nitrokey_sys::NK_write_hotp_slot( + raw_data.number, + raw_data.name.as_ptr(), + raw_data.secret.as_ptr(), + counter, + raw_data.mode == OtpMode::EightDigits, + raw_data.use_enter, + raw_data.use_token_id, + raw_data.token_id.as_ptr(), + temp_password_ptr, + ) + }); + } + + /// Configure a TOTP slot with the given data and set the TOTP time window + /// to the given value (default 30). + /// + /// # Errors + /// + /// - [`InvalidString`][] if the provided token ID contains a null byte + /// - [`NoName`][] if the provided name is empty + /// - [`WrongSlot`][] if there is no slot with the given number + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::{CommandStatus, Device, OtpMode, OtpSlotData}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), (CommandError)> { + /// let device = nitrokey::connect()?; + /// let slot_data = OtpSlotData::new(1, "test", "01234567890123456689", OtpMode::EightDigits); + /// match device.authenticate_admin("12345678") { + /// Ok(admin) => { + /// match admin.write_totp_slot(slot_data, 30) { + /// CommandStatus::Success => println!("Successfully wrote slot."), + /// CommandStatus::Error(err) => println!("Could not write slot: {:?}", err), + /// } + /// }, + /// Err((_, err)) => println!("Could not authenticate as admin: {:?}", err), + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`NoName`]: enum.CommandError.html#variant.NoName + /// [`WrongSlot`]: enum.CommandError.html#variant.WrongSlot + pub fn write_totp_slot(&self, data: OtpSlotData, time_window: u16) -> CommandStatus { + return self.write_otp_slot(data, |raw_data: RawOtpSlotData, temp_password_ptr| unsafe { + nitrokey_sys::NK_write_totp_slot( + raw_data.number, + raw_data.name.as_ptr(), + raw_data.secret.as_ptr(), + time_window, + raw_data.mode == OtpMode::EightDigits, + raw_data.use_enter, + raw_data.use_token_id, + raw_data.token_id.as_ptr(), + temp_password_ptr, + ) + }); + } +} + +impl Device for AdminAuthenticatedDevice {} + +impl AuthenticatedDevice for AdminAuthenticatedDevice { + fn new(device: UnauthenticatedDevice, temp_password: Vec) -> Self { + AdminAuthenticatedDevice { + device, + temp_password, + } + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..5f01b67 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod no_device; +mod pro; diff --git a/src/tests/no_device.rs b/src/tests/no_device.rs new file mode 100644 index 0000000..04c5c8a --- /dev/null +++ b/src/tests/no_device.rs @@ -0,0 +1,9 @@ +use Model; + +#[test] +#[cfg_attr(not(feature = "test-no-device"), ignore)] +fn connect() { + assert!(::connect().is_err()); + assert!(::connect_model(Model::Storage).is_err()); + assert!(::connect_model(Model::Pro).is_err()); +} diff --git a/src/tests/pro.rs b/src/tests/pro.rs new file mode 100644 index 0000000..7fa8ec3 --- /dev/null +++ b/src/tests/pro.rs @@ -0,0 +1,268 @@ +use std::ffi::CStr; +use std::marker::Sized; +use {set_debug, AdminAuthenticatedDevice, CommandError, CommandStatus, Config, Device, Model, + OtpMode, OtpSlotData, UnauthenticatedDevice}; + +static ADMIN_PASSWORD: &str = "12345678"; +static USER_PASSWORD: &str = "123456"; + +// 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"), +]; + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn connect() { + set_debug(false); + assert!(::connect().is_ok()); + assert!(::connect_model(Model::Pro).is_ok()); + assert!(::connect_model(Model::Storage).is_err()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn disconnect() { + set_debug(false); + ::connect().unwrap().disconnect(); + 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(feature = "test-pro"), ignore)] +fn get_serial_number() { + set_debug(false); + let device = ::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())); +} + +fn configure_hotp(admin: &AdminAuthenticatedDevice) { + let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits); + assert_eq!(CommandStatus::Success, admin.write_hotp_slot(slot_data, 0)); +} + +fn check_hotp_codes(device: &T) +where + T: Sized, +{ + for code in HOTP_CODES { + let result = device.get_hotp_code(1); + assert_eq!(code, &result.unwrap()); + } +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn hotp() { + set_debug(false); + let device = ::connect().unwrap(); + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let config = Config::new(None, None, None, false); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + + configure_hotp(&admin); + check_hotp_codes(&admin); + + configure_hotp(&admin); + check_hotp_codes(&admin.device()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn hotp_pin() { + set_debug(false); + let device = ::connect().unwrap(); + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let config = Config::new(None, None, None, true); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + + configure_hotp(&admin); + let user = admin.device().authenticate_user(USER_PASSWORD).unwrap(); + check_hotp_codes(&user); + + // TODO: enable for newer libnitrokey + // assert!(user.device().get_hotp_code(1).is_err()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn hotp_slot_name() { + set_debug(false); + let device = ::connect().unwrap(); + + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let slot_data = OtpSlotData::new(1, "test-hotp", HOTP_SECRET, OtpMode::SixDigits); + assert_eq!(CommandStatus::Success, admin.write_hotp_slot(slot_data, 0)); + + let result = admin.device().get_hotp_slot_name(1); + assert_eq!("test-hotp", result.unwrap()); +} + +fn configure_totp(admin: &AdminAuthenticatedDevice) { + let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits); + assert_eq!(CommandStatus::Success, admin.write_totp_slot(slot_data, 30)); +} + +fn check_totp_codes(device: &T) +where + T: Sized, +{ + for (i, &(time, code)) in TOTP_CODES.iter().enumerate() { + assert_eq!(CommandStatus::Success, device.set_time(time)); + 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(feature = "test-pro"), ignore)] +fn totp() { + // TODO: this test may fail due to bad timing --> find solution + set_debug(false); + let device = ::connect().unwrap(); + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let config = Config::new(None, None, None, false); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + + configure_totp(&admin); + check_totp_codes(&admin); + + configure_totp(&admin); + check_totp_codes(&admin.device()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn totp_pin() { + // TODO: this test may fail due to bad timing --> find solution + set_debug(false); + let device = ::connect().unwrap(); + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let config = Config::new(None, None, None, true); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + + configure_totp(&admin); + let user = admin.device().authenticate_user(USER_PASSWORD).unwrap(); + check_totp_codes(&user); + + // TODO: enable for newer libnitrokey + // assert!(user.device().get_totp_code(1).is_err()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn totp_slot_name() { + set_debug(false); + let device = ::connect().unwrap(); + + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + let slot_data = OtpSlotData::new(1, "test-totp", TOTP_SECRET, OtpMode::EightDigits); + assert_eq!(CommandStatus::Success, admin.write_totp_slot(slot_data, 0)); + + let result = admin.device().get_totp_slot_name(1); + assert!(result.is_ok()); + assert_eq!("test-totp", result.unwrap()); +} + +#[test] +#[cfg_attr(not(feature = "test-pro"), ignore)] +fn get_major_firmware_version() { + set_debug(false); + // TODO fix for different libnitrokey versions + assert_eq!(8, ::connect().unwrap().get_major_firmware_version()); +} + +fn admin_retry(device: UnauthenticatedDevice, suffix: &str, count: u8) -> UnauthenticatedDevice { + 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(device: UnauthenticatedDevice, suffix: &str, count: u8) -> UnauthenticatedDevice { + 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(feature = "test-pro"), ignore)] +fn get_retry_count() { + set_debug(false); + let device = ::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(feature = "test-pro"), ignore)] +fn read_write_config() { + set_debug(false); + let device = ::connect().unwrap(); + + let admin = device.authenticate_admin(ADMIN_PASSWORD).unwrap(); + + let config = Config::new(None, None, None, true); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + let get_config = admin.get_config().unwrap(); + assert_eq!(config, get_config); + + let config = Config::new(None, Some(9), None, true); + assert_eq!( + CommandStatus::Error(CommandError::InvalidSlot), + admin.write_config(config) + ); + + let config = Config::new(Some(1), None, Some(0), false); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + let get_config = admin.get_config().unwrap(); + assert_eq!(config, get_config); + + let config = Config::new(None, None, None, false); + assert_eq!(CommandStatus::Success, admin.write_config(config)); + let get_config = admin.get_config().unwrap(); + assert_eq!(config, get_config); +} -- cgit v1.2.1