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/pws.rs | 403 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 src/pws.rs (limited to 'src/pws.rs') 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), + } + } +} -- cgit v1.2.1