From 89b8a947e5c622272362e967847eb19337aa68da Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 28 May 2018 22:02:10 +0000 Subject: Add rudimentary support for the Nitrokey Storage This patch adds the Storage struct and the test-storage feature. It also enables all currently supported Pro commands for the Storage. --- Cargo.toml | 1 + TODO.md | 3 ++- src/auth.rs | 16 ++++++++++++- src/device.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 5 ++-- src/tests/device.rs | 51 +++++++++++++++++++++++----------------- src/tests/otp.rs | 29 +++++++++++------------ src/tests/util.rs | 9 +++++++ src/util.rs | 7 +++--- 9 files changed, 143 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 09f44ba..524733d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ license = "MIT" default = ["test-no-device"] test-no-device = [] test-pro = [] +test-storage = [] [dependencies] libc = "0.2" diff --git a/TODO.md b/TODO.md index d57260c..9b9bc72 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,3 @@ -- Add support and tests for the Nitrokey Storage. - Add support for the currently unsupported commands: - `NK_set_unencrypted_volume_rorw_pin_type_user` - `NK_lock_device` @@ -41,3 +40,5 @@ - Fix segmentation faults when freeing string literals with old Nitrokey versions (fixed in libnitrokey commit 7a8550d). - Prevent construction of internal types. +- Add Storage-only examples to the `DeviceWrapper` documentation. +- Fix generic connection (`get_connected_device`). diff --git a/src/auth.rs b/src/auth.rs index 80abbc0..c772faf 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,5 @@ use config::{Config, RawConfig}; -use device::{Device, DeviceWrapper, Pro}; +use device::{Device, DeviceWrapper, Pro, Storage}; use nitrokey_sys; use std::ffi::CString; use std::ops::Deref; @@ -387,3 +387,17 @@ impl Authenticate for Pro { }) } } + +impl Authenticate for Storage { + fn authenticate_user(self, password: &str) -> Result, (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, (Self, CommandError)> { + authenticate(self, password, |password_ptr, temp_password_ptr| unsafe { + nitrokey_sys::NK_first_authenticate(password_ptr, temp_password_ptr) + }) + } +} diff --git a/src/device.rs b/src/device.rs index 684f64a..1201540 100644 --- a/src/device.rs +++ b/src/device.rs @@ -54,7 +54,7 @@ enum Model { #[derive(Debug)] pub enum DeviceWrapper { /// A Nitrokey Storage device. - Storage(()), + Storage(Storage), /// A Nitrokey Pro device. Pro(Pro), } @@ -101,6 +101,48 @@ pub enum DeviceWrapper { #[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) {} +/// 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 {} + /// A Nitrokey device. /// /// This trait provides the commands that can be executed without authentication and that are @@ -368,7 +410,7 @@ fn connect_model(model: Model) -> bool { impl DeviceWrapper { fn device(&self) -> &Device { match *self { - DeviceWrapper::Storage(_) => panic!("..."), + DeviceWrapper::Storage(ref storage) => storage, DeviceWrapper::Pro(ref pro) => pro, } } @@ -415,3 +457,25 @@ impl Drop for Pro { impl Device for Pro {} impl GenerateOtp for Pro {} + +impl Storage { + pub fn connect() -> Result { + // TODO: maybe Option instead of Result? + match connect_model(Model::Storage) { + true => Ok(Storage {}), + false => Err(CommandError::Unknown), + } + } +} + +impl Drop for Storage { + fn drop(&mut self) { + unsafe { + nitrokey_sys::NK_logout(); + } + } +} + +impl Device for Storage {} + +impl GenerateOtp for Storage {} diff --git a/src/lib.rs b/src/lib.rs index 9dad9f9..4d5452d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! //! 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`][] to connect to a specific device. +//! [`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 @@ -76,6 +76,7 @@ //! [`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 @@ -96,7 +97,7 @@ mod util; mod tests; pub use config::Config; -pub use device::{connect, Device, DeviceWrapper, Pro}; +pub use device::{connect, Device, DeviceWrapper, Pro, Storage}; pub use auth::{Admin, Authenticate, User}; pub use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; pub use util::{CommandError, CommandStatus, LogLevel}; diff --git a/src/tests/device.rs b/src/tests/device.rs index 394861c..80541d4 100644 --- a/src/tests/device.rs +++ b/src/tests/device.rs @@ -1,6 +1,6 @@ use std::ffi::CStr; -use {Authenticate, CommandError, CommandStatus, Config, Device, Pro}; -use tests::util::{ADMIN_PASSWORD, USER_PASSWORD}; +use {Authenticate, CommandError, CommandStatus, Config, Device}; +use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; static ADMIN_NEW_PASSWORD: &str = "1234567890"; static USER_NEW_PASSWORD: &str = "abcdefghij"; @@ -10,17 +10,23 @@ static USER_NEW_PASSWORD: &str = "abcdefghij"; fn connect_no_device() { assert!(::connect().is_err()); assert!(::Pro::connect().is_err()); - // TODO: test storage - // assert!(::Storage::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()); - // TODO: test storage - // assert!(::Storage::connect().is_err()); + assert!(::Pro::connect().is_ok()); + assert!(::Storage::connect().is_err()); +} + +#[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()); } fn assert_empty_serial_number() { @@ -33,16 +39,16 @@ fn assert_empty_serial_number() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] fn disconnect() { - ::connect().unwrap(); + Target::connect().unwrap(); assert_empty_serial_number(); - ::connect() + Target::connect() .unwrap() .authenticate_admin(ADMIN_PASSWORD) .unwrap(); assert_empty_serial_number(); - ::connect() + Target::connect() .unwrap() .authenticate_user(USER_PASSWORD) .unwrap(); @@ -50,9 +56,9 @@ fn disconnect() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] fn get_serial_number() { - let device = ::connect().unwrap(); + let device = Target::connect().unwrap(); let result = device.get_serial_number(); assert!(result.is_ok()); let serial_number = result.unwrap(); @@ -61,8 +67,9 @@ fn get_serial_number() { } #[test] #[cfg_attr(not(feature = "test-pro"), ignore)] +// TODO: adapt for storage fn get_firmware_version() { - let device = ::connect().unwrap(); + let device = Target::connect().unwrap(); assert_eq!(0, device.get_major_firmware_version()); let minor = device.get_minor_firmware_version(); assert!(minor == 7 || minor == 8); @@ -89,9 +96,9 @@ fn user_retry(device: T, suffix: &str, count: u8) -> T } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] fn get_retry_count() { - let device = ::connect().unwrap(); + let device = Target::connect().unwrap(); let device = admin_retry(device, "", 3); let device = admin_retry(device, "123", 2); @@ -105,9 +112,9 @@ fn get_retry_count() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] fn config() { - let device = ::connect().unwrap(); + let device = Target::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)); @@ -132,9 +139,9 @@ fn config() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] fn change_user_pin() { - let device = ::connect().unwrap(); + 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; @@ -158,9 +165,9 @@ fn change_user_pin() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] fn change_admin_pin() { - let device = ::connect().unwrap(); + 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; diff --git a/src/tests/otp.rs b/src/tests/otp.rs index e96bbfe..10f569d 100644 --- a/src/tests/otp.rs +++ b/src/tests/otp.rs @@ -1,7 +1,7 @@ use std::ops::Deref; -use {Admin, Authenticate, CommandError, CommandStatus, Config, ConfigureOtp, DeviceWrapper, GenerateOtp, +use {Admin, Authenticate, CommandError, CommandStatus, Config, ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; -use tests::util::{ADMIN_PASSWORD, USER_PASSWORD}; +use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; // test suite according to RFC 4226, Appendix D static HOTP_SECRET: &str = "3132333435363738393031323334353637383930"; @@ -21,8 +21,8 @@ static TOTP_CODES: &[(u64, &str)] = &[ (20000000000, "65353130"), ]; -fn get_admin_test_device() -> Admin { - ::connect().expect("Could not connect to the Nitrokey Pro.") +fn get_admin_test_device() -> Admin { + Target::connect().expect("Could not connect to the Nitrokey Pro.") .authenticate_admin(ADMIN_PASSWORD) .expect("Could not login as admin.") } @@ -40,7 +40,7 @@ fn check_hotp_codes(device: &GenerateOtp) { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -54,7 +54,7 @@ fn hotp_no_pin() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -68,7 +68,7 @@ fn hotp_pin() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -82,7 +82,7 @@ fn hotp_slot_name() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -100,7 +100,7 @@ fn hotp_error() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -141,7 +141,7 @@ fn check_totp_codes(device: &GenerateOtp) { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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(); @@ -156,7 +156,7 @@ fn totp_no_pin() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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(); @@ -171,7 +171,7 @@ fn totp_pin() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -186,7 +186,7 @@ fn totp_slot_name() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -204,7 +204,7 @@ fn totp_error() { } #[test] -#[cfg_attr(not(feature = "test-pro"), ignore)] +#[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); @@ -224,4 +224,3 @@ fn totp_erase() { assert_eq!("test2", device.get_totp_slot_name(2).unwrap()); } - diff --git a/src/tests/util.rs b/src/tests/util.rs index cbf6b93..c6fbb8f 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -1,2 +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/src/util.rs b/src/util.rs index 22cccb3..364b0de 100644 --- a/src/util.rs +++ b/src/util.rs @@ -48,9 +48,10 @@ pub enum CommandStatus { Error(CommandError), } -/// 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. +/// 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. -- cgit v1.2.1