// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use libc;
use nitrokey_sys;

use crate::device::{Device, DeviceWrapper, Pro, Storage};
use crate::error::{CommandError, Error};
use crate::util::{get_command_result, get_cstring, get_last_error, result_from_string};

/// 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.  To
/// retrieve a password safe from a Nitrokey device, use the [`get_password_safe`][] method from
/// the [`GetPasswordSafe`][] trait.  Note that the device must live at least as long as the
/// password safe.
///
/// Once the password safe has been unlocked, it can be accessed without a password.  Therefore it
/// is mandatory to call [`lock`][] on the corresponding device after the password store is used.
/// As this command may have side effects on the Nitrokey Storage, it cannot be called
/// automatically once the password safe is destroyed.
///
/// # Examples
///
/// Open a password safe and access a password:
///
/// ```no_run
/// use nitrokey::{Device, GetPasswordSafe, PasswordSafe};
/// # use nitrokey::Error;
///
/// fn use_password_safe(pws: &PasswordSafe) -> Result<(), Error> {
///     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);
///     Ok(())
/// }
///
/// # fn try_main() -> Result<(), Error> {
/// let mut manager = nitrokey::take()?;
/// let mut device = manager.connect()?;
/// let pws = device.get_password_safe("123456")?;
/// use_password_safe(&pws);
/// drop(pws);
/// device.lock()?;
/// #     Ok(())
/// # }
/// ```
///
/// [`SLOT_COUNT`]: constant.SLOT_COUNT.html
/// [`get_password_safe`]: trait.GetPasswordSafe.html#method.get_password_safe
/// [`lock`]: trait.Device.html#method.lock
/// [`GetPasswordSafe`]: trait.GetPasswordSafe.html
#[derive(Debug)]
pub struct PasswordSafe<'a, 'b> {
    _device: &'a dyn Device<'b>,
}

/// Provides access to a [`PasswordSafe`][].
///
/// The device that implements this trait must always live at least as long as a password safe
/// retrieved from it.
///
/// [`PasswordSafe`]: struct.PasswordSafe.html
pub trait GetPasswordSafe<'a> {
    /// Enables and returns the password safe.
    ///
    /// The underlying device must always live at least as long as a password safe retrieved from
    /// it.  It is mandatory to lock the underlying device using [`lock`][] after the password safe
    /// has been used.  Otherwise, other applications can access the password store without
    /// authentication.
    ///
    /// If this method returns an `AesDecryptionFailed` (Nitrokey Pro) or `Unknown` (Nitrokey
    /// Storage) error, the AES data object on the smart card could not be accessed.  This problem
    /// occurs after a factory reset using `gpg --card-edit` and can be fixed using the
    /// [`Device::build_aes_key`][] command.
    ///
    /// # Errors
    ///
    /// - [`AesDecryptionFailed`][] if the secret for the password safe could not be decrypted
    ///   (Nitrokey Pro only)
    /// - [`InvalidString`][] if one of the provided passwords contains a null byte
    /// - [`Unknown`][] if the secret for the password safe could not be decrypted (Nitrokey
    ///   Storage only)
    /// - [`WrongPassword`][] if the current user password is wrong
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::{Device, GetPasswordSafe, PasswordSafe};
    /// # use nitrokey::Error;
    ///
    /// fn use_password_safe(pws: &PasswordSafe) {}
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// match device.get_password_safe("123456") {
    ///     Ok(pws) => {
    ///         use_password_safe(&pws);
    ///     },
    ///     Err(err) => eprintln!("Could not open the password safe: {}", err),
    /// };
    /// device.lock()?;
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`device`]: struct.PasswordSafe.html#method.device
    /// [`lock`]: trait.Device.html#method.lock
    /// [`AesDecryptionFailed`]: enum.CommandError.html#variant.AesDecryptionFailed
    /// [`Device::build_aes_key`]: trait.Device.html#method.build_aes_key
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    /// [`Unknown`]: enum.CommandError.html#variant.Unknown
    /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword
    fn get_password_safe(&mut self, user_pin: &str) -> Result<PasswordSafe<'_, 'a>, Error>;
}

fn get_password_safe<'a, 'b>(
    device: &'a dyn Device<'b>,
    user_pin: &str,
) -> Result<PasswordSafe<'a, 'b>, Error> {
    let user_pin_string = get_cstring(user_pin)?;
    get_command_result(unsafe { nitrokey_sys::NK_enable_password_safe(user_pin_string.as_ptr()) })
        .map(|_| PasswordSafe { _device: device })
}

fn get_pws_result(s: String) -> Result<String, Error> {
    if s.is_empty() {
        Err(CommandError::SlotNotProgrammed.into())
    } else {
        Ok(s)
    }
}

impl<'a, 'b> PasswordSafe<'a, 'b> {
    /// 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::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// let pws = device.get_password_safe("123456")?;
    /// pws.get_slot_status()?.iter().enumerate().for_each(|(slot, programmed)| {
    ///     let status = match *programmed {
    ///         true => "programmed",
    ///         false => "not programmed",
    ///     };
    ///     println!("Slot {}: {}", slot, status);
    /// });
    /// #     Ok(())
    /// # }
    /// ```
    pub fn get_slot_status(&self) -> Result<[bool; SLOT_COUNT as usize], Error> {
        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).
    ///
    /// This method also returns a `SlotNotProgrammed` error if the name is empty.
    ///
    /// # Errors
    ///
    /// - [`InvalidSlot`][] if the given slot is out of range
    /// - [`SlotNotProgrammed`][] if the slot is not programmed
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::GetPasswordSafe;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.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) => eprintln!("Could not open the password safe: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    pub fn get_slot_name(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_name(slot) })
            .and_then(get_pws_result)
    }

    /// Returns the login for the given slot (if it is programmed).
    ///
    /// This method also returns a `SlotNotProgrammed` error if the login is empty.
    ///
    /// # Errors
    ///
    /// - [`InvalidSlot`][] if the given slot is out of range
    /// - [`SlotNotProgrammed`][] if the slot is not programmed
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::GetPasswordSafe;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// let pws = device.get_password_safe("123456")?;
    /// 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);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    pub fn get_slot_login(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_login(slot) })
            .and_then(get_pws_result)
    }

    /// Returns the password for the given slot (if it is programmed).
    ///
    /// This method also returns a `SlotNotProgrammed` error if the password is empty.
    ///
    /// # Errors
    ///
    /// - [`InvalidSlot`][] if the given slot is out of range
    /// - [`SlotNotProgrammed`][] if the slot is not programmed
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nitrokey::GetPasswordSafe;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// let pws = device.get_password_safe("123456")?;
    /// 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);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`SlotNotProgrammed`]: enum.CommandError.html#variant.SlotNotProgrammed
    pub fn get_slot_password(&self, slot: u8) -> Result<String, Error> {
        result_from_string(unsafe { nitrokey_sys::NK_get_password_safe_slot_password(slot) })
            .and_then(get_pws_result)
    }

    /// 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::GetPasswordSafe;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// let pws = device.get_password_safe("123456")?;
    /// 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);
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString
    pub fn write_slot(
        &mut self,
        slot: u8,
        name: &str,
        login: &str,
        password: &str,
    ) -> Result<(), Error> {
        let name_string = get_cstring(name)?;
        let login_string = get_cstring(login)?;
        let password_string = get_cstring(password)?;
        get_command_result(unsafe {
            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::GetPasswordSafe;
    /// # use nitrokey::Error;
    ///
    /// # fn try_main() -> Result<(), Error> {
    /// let mut manager = nitrokey::take()?;
    /// let mut device = manager.connect()?;
    /// let mut pws = device.get_password_safe("123456")?;
    /// match pws.erase_slot(0) {
    ///     Ok(()) => println!("Erased slot 0."),
    ///     Err(err) => eprintln!("Could not erase slot 0: {}", err),
    /// };
    /// #     Ok(())
    /// # }
    /// ```
    ///
    /// [`InvalidSlot`]: enum.LibraryError.html#variant.InvalidSlot
    pub fn erase_slot(&mut self, slot: u8) -> Result<(), Error> {
        get_command_result(unsafe { nitrokey_sys::NK_erase_password_safe_slot(slot) })
    }
}

impl<'a, 'b> Drop for PasswordSafe<'a, 'b> {
    fn drop(&mut self) {
        // TODO: disable the password safe -- NK_lock_device has side effects on the Nitrokey
        // Storage, see https://github.com/Nitrokey/nitrokey-storage-firmware/issues/65
    }
}

impl<'a> GetPasswordSafe<'a> for Pro<'a> {
    fn get_password_safe(&mut self, user_pin: &str) -> Result<PasswordSafe<'_, 'a>, Error> {
        get_password_safe(self, user_pin)
    }
}

impl<'a> GetPasswordSafe<'a> for Storage<'a> {
    fn get_password_safe(&mut self, user_pin: &str) -> Result<PasswordSafe<'_, 'a>, Error> {
        get_password_safe(self, user_pin)
    }
}

impl<'a> GetPasswordSafe<'a> for DeviceWrapper<'a> {
    fn get_password_safe(&mut self, user_pin: &str) -> Result<PasswordSafe<'_, 'a>, Error> {
        get_password_safe(self, user_pin)
    }
}
