diff options
| -rw-r--r-- | TODO.md | 9 | ||||
| -rw-r--r-- | src/device.rs | 3 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/pws.rs | 403 | ||||
| -rw-r--r-- | src/tests/mod.rs | 1 | ||||
| -rw-r--r-- | src/tests/otp.rs | 7 | ||||
| -rw-r--r-- | src/tests/pws.rs | 169 | 
7 files changed, 583 insertions, 11 deletions
| @@ -4,13 +4,6 @@    - `NK_factory_reset`    - `NK_build_aes_key`    - `NK_unlock_user_password` -  - `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` @@ -42,3 +35,5 @@  - Prevent construction of internal types.  - Add Storage-only examples to the `DeviceWrapper` documentation.  - Fix generic connection (`get_connected_device`). +- More specific error checking in the tests. +- Differentiate empty strings and errors (see `result_from_string`). 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.      /// @@ -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<T: 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<T: Device> { +    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<T: Device>(pws: &PasswordSafe<T>) {} +    /// fn use_device<T: 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<PasswordSafe<Self>, (Self, CommandError)> +    where +        Self: Device + Sized; +} + +fn get_password_safe<T: Device>( +    device: T, +    user_pin: &str, +) -> Result<PasswordSafe<T>, (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<T, C>( +    device: T, +    constructor: C, +    user_pin: &str, +) -> Result<PasswordSafe<DeviceWrapper>, (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<T: Device> PasswordSafe<T> { +    /// 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<String, CommandError> { +        unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_name(slot)) } +    } + +    /// Returns the login for the given slot (if it is programmed). +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if the given slot is out of range +    /// - [`Unknown`][] if the slot is not programmed +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::GetPasswordSafe; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), CommandError> { +    /// let device = nitrokey::connect()?; +    /// 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<String, CommandError> { +        unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_login(slot)) } +    } + +    /// Returns the password for the given slot (if it is programmed). +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if the given slot is out of range +    /// - [`Unknown`][] if the slot is not programmed +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::GetPasswordSafe; +    /// # use nitrokey::CommandError; +    /// +    /// # fn try_main() -> Result<(), CommandError> { +    /// let device = nitrokey::connect()?; +    /// 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<String, CommandError> { +        unsafe { result_from_string(nitrokey_sys::NK_get_password_safe_slot_password(slot)) } +    } + +    /// Writes the given slot with the given name, login and password. +    /// +    /// # Errors +    /// +    /// - [`InvalidSlot`][] if the given slot is out of range +    /// - [`InvalidString`][] if the provided token ID contains a null byte +    /// +    /// # Example +    /// +    /// ```no_run +    /// use nitrokey::{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<PasswordSafe<Self>, (Self, CommandError)> { +        get_password_safe(self, user_pin) +    } +} + +impl GetPasswordSafe for Storage { +    fn get_password_safe(self, user_pin: &str) -> Result<PasswordSafe<Self>, (Self, CommandError)> { +        get_password_safe(self, user_pin) +    } +} + +impl GetPasswordSafe for DeviceWrapper { +    fn get_password_safe(self, user_pin: &str) -> Result<PasswordSafe<Self>, (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> { -    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> { +    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)); +} | 
