From 7197f19f38b06fe2953cfba1fe755d4562f5786e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 29 May 2018 20:49:40 +0000 Subject: Add support for password safes A password safe (PWS) stores names, logins and passwords in slots. PWS are supported both by the Nitrokey Pro and the Nitrokey Storage. They are implemented as a struct wrapping a device as the device may not be disconnected while the password safe is alive. The creation of a password safe is handled by the GetPasswordSafe trait, implemented by DeviceWrapper, Pro and Storage. --- src/device.rs | 3 +- src/lib.rs | 2 + src/pws.rs | 403 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tests/mod.rs | 1 + src/tests/otp.rs | 7 +- src/tests/pws.rs | 169 +++++++++++++++++++++++ 6 files changed, 581 insertions(+), 4 deletions(-) create mode 100644 src/pws.rs create mode 100644 src/tests/pws.rs (limited to 'src') diff --git a/src/device.rs b/src/device.rs index 1201540..7d69df8 100644 --- a/src/device.rs +++ b/src/device.rs @@ -3,6 +3,7 @@ use libc; use nitrokey_sys; use std::ffi::CString; use otp::GenerateOtp; +use pws::GetPasswordSafe; use util::{get_last_error, result_from_string, CommandError, CommandStatus}; /// Available Nitrokey models. @@ -147,7 +148,7 @@ pub struct Storage {} /// /// This trait provides the commands that can be executed without authentication and that are /// present on all supported Nitrokey devices. -pub trait Device: GenerateOtp { +pub trait Device: GetPasswordSafe + GenerateOtp { /// Returns the serial number of the Nitrokey device. The serial number is the string /// representation of a hex number. /// diff --git a/src/lib.rs b/src/lib.rs index 4d5452d..b30e9ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,6 +92,7 @@ mod auth; mod config; mod device; mod otp; +mod pws; mod util; #[cfg(test)] mod tests; @@ -100,6 +101,7 @@ pub use config::Config; pub use device::{connect, Device, DeviceWrapper, Pro, Storage}; pub use auth::{Admin, Authenticate, User}; pub use otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; +pub use pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; pub use util::{CommandError, CommandStatus, LogLevel}; /// Enables or disables debug output. Calling this method with `true` is equivalent to setting the diff --git a/src/pws.rs b/src/pws.rs new file mode 100644 index 0000000..fc4b516 --- /dev/null +++ b/src/pws.rs @@ -0,0 +1,403 @@ +use device::{Device, DeviceWrapper, Pro, Storage}; +use libc; +use nitrokey_sys; +use std::ffi::CString; +use util::{get_last_error, result_from_string, CommandError, CommandStatus}; + +/// 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. +/// +/// The password safe struct wraps a device. To get back to the original device, use the +/// [`device`][] method. To retrieve a password safe from a Nitrokey device, use the +/// [`get_password_safe`][] method from the [`GetPasswordSafe`][] trait. +/// +/// # Examples +/// +/// Open a password safe and access a password: +/// +/// ```no_run +/// use nitrokey::{Device, GetPasswordSafe}; +/// # use nitrokey::CommandError; +/// +/// fn use_device(device: &T) {} +/// +/// # fn try_main() -> Result<(), CommandError> { +/// let device = nitrokey::connect()?; +/// let device = 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); +/// pws.device() +/// }, +/// Err((device, err)) => { +/// println!("Could not open the password safe: {:?}", err); +/// device +/// }, +/// }; +/// use_device(&device); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`SLOT_COUNT`]: constant.SLOT_COUNT.html +/// [`device`]: #method.device +/// [`get_password_safe`]: trait.GetPasswordSafe.html#method.get_password_safe +/// [`GetPasswordSafe`]: trait.GetPasswordSafe.html +pub struct PasswordSafe { + device: T, +} + +/// Provides access to a [`PasswordSafe`][]. +/// +/// When retrieving a password safe, the underlying device is consumed. On success, the returned +/// password safe also containes the device. On error, the device is returned as part of the +/// error. +/// +/// [`PasswordSafe`]: struct.PasswordSafe.html +pub trait GetPasswordSafe { + /// Enables and returns the password safe. This method consumes the device. You can go back + /// to the device using the [`device`][] method of the returned password safe. If the method + /// fails, the current device will be returned as part of the error. + /// + /// # 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 use_device(device: &T) {} + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// let device = match device.get_password_safe("123456") { + /// Ok(pws) => { + /// use_password_safe(&pws); + /// pws.device() + /// }, + /// Err((device, err)) => { + /// println!("Could not open the password safe: {:?}", err); + /// device + /// }, + /// }; + /// use_device(&device); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`device`]: struct.PasswordSafe.html#method.device + /// [`InvalidString`]: enum.CommandError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + fn get_password_safe(self, user_pin: &str) -> Result, (Self, CommandError)> + where + Self: Device + Sized; +} + +fn get_password_safe( + device: T, + user_pin: &str, +) -> Result, (T, CommandError)> { + let user_pin_string = CString::new(user_pin); + if user_pin_string.is_err() { + return Err((device, CommandError::InvalidString)); + } + let user_pin_string = user_pin_string.unwrap(); + let status = unsafe { + CommandStatus::from(nitrokey_sys::NK_enable_password_safe( + user_pin_string.as_ptr(), + )) + }; + match status { + CommandStatus::Success => Ok(PasswordSafe { device }), + CommandStatus::Error(err) => Err((device, err)), + } +} + +fn get_password_safe_wrapper( + device: T, + constructor: C, + user_pin: &str, +) -> Result, (DeviceWrapper, CommandError)> +where + T: Device, + C: Fn(T) -> DeviceWrapper, +{ + let result = device.get_password_safe(user_pin); + match result { + Ok(pws) => Ok(PasswordSafe { + device: constructor(pws.device), + }), + Err((device, err)) => Err((constructor(device), err)), + } +} + +impl PasswordSafe { + /// Forgets the password safe access and returns an unauthenticated device. This method + /// consumes the password safe. It does not perform any actual commands on the Nitrokey. + pub fn device(self) -> T { + self.device + } + + /// 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()?; + /// match device.get_password_safe("123456") { + /// Ok(pws) => { + /// pws.get_slot_status()?.iter().enumerate().for_each(|(slot, programmed)| { + /// let status = match *programmed { + /// true => "programmed", + /// false => "not programmed", + /// }; + /// println!("Slot {}: {}", slot, status); + /// }); + /// }, + /// Err((_, err)) => println!("Could not open the password safe: {:?}", err), + /// }; + /// # 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 { + 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()?; + /// 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_login(&self, slot: u8) -> Result { + 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()?; + /// 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_password(&self, slot: u8) -> Result { + 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::{CommandStatus, GetPasswordSafe}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_password_safe("123456") { + /// Ok(pws) => match pws.write_slot(0, "GitHub", "johndoe", "passw0rd") { + /// CommandStatus::Success => println!("Successfully wrote slot 0."), + /// CommandStatus::Error(err) => println!("Could not write slot 0: {:?}", err), + /// }, + /// Err((_, err)) => println!("Could not open the password safe: {:?}", err), + /// }; + /// # 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) -> CommandStatus { + let name_string = CString::new(name); + let login_string = CString::new(login); + let password_string = CString::new(password); + if name_string.is_err() || login_string.is_err() || password_string.is_err() { + return CommandStatus::Error(CommandError::InvalidString); + } + + let name_string = name_string.unwrap(); + let login_string = login_string.unwrap(); + let password_string = password_string.unwrap(); + unsafe { + CommandStatus::from(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::{CommandStatus, GetPasswordSafe}; + /// # use nitrokey::CommandError; + /// + /// # fn try_main() -> Result<(), CommandError> { + /// let device = nitrokey::connect()?; + /// match device.get_password_safe("123456") { + /// Ok(pws) => match pws.erase_slot(0) { + /// CommandStatus::Success => println!("Successfully erased slot 0."), + /// CommandStatus::Error(err) => println!("Could not erase slot 0: {:?}", err), + /// }, + /// Err((_, err)) => println!("Could not open the password safe: {:?}", err), + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`InvalidSlot`]: enum.CommandError.html#variant.InvalidSlot + pub fn erase_slot(&self, slot: u8) -> CommandStatus { + unsafe { CommandStatus::from(nitrokey_sys::NK_erase_password_safe_slot(slot)) } + } +} + +impl GetPasswordSafe for Pro { + fn get_password_safe(self, user_pin: &str) -> Result, (Self, CommandError)> { + get_password_safe(self, user_pin) + } +} + +impl GetPasswordSafe for Storage { + fn get_password_safe(self, user_pin: &str) -> Result, (Self, CommandError)> { + get_password_safe(self, user_pin) + } +} + +impl GetPasswordSafe for DeviceWrapper { + fn get_password_safe(self, user_pin: &str) -> Result, (Self, CommandError)> { + match self { + DeviceWrapper::Storage(storage) => { + get_password_safe_wrapper(storage, DeviceWrapper::Storage, user_pin) + } + DeviceWrapper::Pro(pro) => get_password_safe_wrapper(pro, DeviceWrapper::Pro, user_pin), + } + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index c2c9f9d..34ca0aa 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,3 +1,4 @@ mod device; mod otp; +mod pws; mod util; diff --git a/src/tests/otp.rs b/src/tests/otp.rs index 10f569d..8c59341 100644 --- a/src/tests/otp.rs +++ b/src/tests/otp.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use {Admin, Authenticate, CommandError, CommandStatus, Config, ConfigureOtp, GenerateOtp, - OtpMode, OtpSlotData}; +use {Admin, Authenticate, CommandError, CommandStatus, Config, ConfigureOtp, GenerateOtp, OtpMode, + OtpSlotData}; use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; // test suite according to RFC 4226, Appendix D @@ -22,7 +22,8 @@ static TOTP_CODES: &[(u64, &str)] = &[ ]; fn get_admin_test_device() -> Admin { - Target::connect().expect("Could not connect to the Nitrokey Pro.") + Target::connect() + .expect("Could not connect to the Nitrokey Pro.") .authenticate_admin(ADMIN_PASSWORD) .expect("Could not login as admin.") } diff --git a/src/tests/pws.rs b/src/tests/pws.rs new file mode 100644 index 0000000..5f5a325 --- /dev/null +++ b/src/tests/pws.rs @@ -0,0 +1,169 @@ +use util::{CommandError, CommandStatus}; +use pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; +use tests::util::{Target, ADMIN_PASSWORD, USER_PASSWORD}; + +fn get_pws() -> PasswordSafe { + Target::connect() + .unwrap() + .get_password_safe(USER_PASSWORD) + .unwrap() +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn enable() { + assert!( + Target::connect() + .unwrap() + .get_password_safe(&(USER_PASSWORD.to_owned() + "123")) + .is_err() + ); + assert!( + Target::connect() + .unwrap() + .get_password_safe(USER_PASSWORD) + .is_ok() + ); + assert!( + Target::connect() + .unwrap() + .get_password_safe(ADMIN_PASSWORD) + .is_err() + ); + assert!( + Target::connect() + .unwrap() + .get_password_safe(USER_PASSWORD) + .is_ok() + ); +} + +#[test] +#[cfg_attr(not(any(feature = "test-pro", feature = "test-storage")), ignore)] +fn get_status() { + let pws = get_pws(); + for i in 0..SLOT_COUNT { + assert_eq!( + CommandStatus::Success, + pws.erase_slot(i), + "Could not erase slot {}", + i + ); + } + let status = pws.get_slot_status().unwrap(); + assert_eq!(status, [false; SLOT_COUNT as usize]); + + assert_eq!( + CommandStatus::Success, + pws.write_slot(1, "name", "login", "password") + ); + 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_eq!( + CommandStatus::Success, + pws.write_slot(i, "name", "login", "password") + ); + } + 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 pws = get_pws(); + assert_eq!( + CommandStatus::Success, + pws.write_slot(1, "name", "login", "password") + ); + 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!(CommandStatus::Success, pws.erase_slot(1)); + // 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_eq!( + CommandStatus::Success, + pws.write_slot(1, name, login, password) + ); + 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 pws = get_pws(); + + assert_eq!( + CommandStatus::Error(CommandError::InvalidSlot), + pws.write_slot(SLOT_COUNT, "name", "login", "password") + ); + + assert_eq!( + CommandStatus::Success, + pws.write_slot(0, "", "login", "password") + ); + 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_eq!( + CommandStatus::Success, + pws.write_slot(0, "name", "", "password") + ); + 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_eq!( + CommandStatus::Success, + pws.write_slot(0, "name", "login", "") + ); + 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 pws = get_pws(); + assert_eq!( + CommandStatus::Error(CommandError::InvalidSlot), + pws.erase_slot(SLOT_COUNT) + ); + + assert_eq!( + CommandStatus::Success, + pws.write_slot(0, "name", "login", "password") + ); + assert_eq!(CommandStatus::Success, pws.erase_slot(0)); + assert_eq!(CommandStatus::Success, pws.erase_slot(0)); + assert_eq!(Err(CommandError::Unknown), pws.get_slot_name(0)); +} -- cgit v1.2.1