From 7197f19f38b06fe2953cfba1fe755d4562f5786e Mon Sep 17 00:00:00 2001
From: Robin Krahl <robin.krahl@ireas.org>
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<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));
+}
-- 
cgit v1.2.3