#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![doc(
    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg",
    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg"
)]
#![forbid(unsafe_code)]
#![warn(
    missing_docs,
    rust_2018_idioms,
    unused_lifetimes,
    missing_debug_implementations
)]

//!
//! # Usage
//!
//! This crate represents password hashes using the [`PasswordHash`] type, which
//! represents a parsed "PHC string" with the following format:
//!
//! ```text
//! $<id>[$v=<version>][$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
//! ```
//!
//! For more information, please see the documentation for [`PasswordHash`].

#[cfg(feature = "alloc")]
#[allow(unused_extern_crates)]
extern crate alloc;

mod error;

pub use crate::error::{Error, Result};

#[cfg(feature = "phc")]
pub use phc;

#[cfg(feature = "rand_core")]
pub use rand_core;

/// DEPRECATED: import this as `password_hash::phc::PasswordHash`.
#[cfg(feature = "phc")]
#[deprecated(
    since = "0.6.0",
    note = "import as `password_hash::phc::PasswordHash` instead"
)]
pub type PasswordHash = phc::PasswordHash;

/// DEPRECATED: use `password_hash::phc::PasswordHash` or `String`
#[cfg(all(feature = "alloc", feature = "phc"))]
#[deprecated(
    since = "0.6.0",
    note = "use `password_hash::phc::PasswordHash` or `String`"
)]
#[allow(deprecated)]
pub type PasswordHashString = phc::PasswordHashString;

use core::fmt::Debug;

#[cfg(feature = "rand_core")]
use rand_core::TryCryptoRng;

/// Numeric version identifier for password hashing algorithms.
pub type Version = u32;

/// Recommended length of a salt: 16-bytes.
///
/// This recommendation comes from the [PHC string format specification]:
///
/// > The role of salts is to achieve uniqueness. A *random* salt is fine
/// > for that as long as its length is sufficient; a 16-byte salt would
/// > work well (by definition, UUID are very good salts, and they encode
/// > over exactly 16 bytes). 16 bytes encode as 22 characters in B64.
///
/// [PHC string format specification]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties
#[cfg(any(feature = "getrandom", feature = "rand_core"))]
const RECOMMENDED_SALT_LEN: usize = 16;

/// High-level trait for password hashing functions.
///
/// Generic around a password hash to be returned (typically [`phc::PasswordHash`])
pub trait PasswordHasher<H> {
    /// Compute the hash `H` from the given password and salt, potentially using configuration
    /// stored in `&self` for the parameters, or otherwise the recommended defaults.
    ///
    /// The salt should be unique per password. When in doubt, use [`PasswordHasher::hash_password`]
    /// which will choose the salt for you.
    ///
    /// # Errors
    /// These will vary by algorithm/implementation of this trait, but may be due to:
    /// - length restrictions on the password and/or salt
    /// - algorithm-specific internal error
    fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result<H>;

    /// Compute the hash `H` from the given password, potentially using configuration stored in
    /// `&self` for the parameters, or otherwise the recommended defaults.
    ///
    /// A large random salt will be generated automatically.
    ///
    /// # Errors
    /// These will vary by algorithm/implementation of this trait, but may be due to:
    /// - length restrictions on the password
    /// - algorithm-specific internal error
    /// - RNG internal error
    #[cfg(feature = "getrandom")]
    fn hash_password(&self, password: &[u8]) -> Result<H> {
        let salt = try_generate_salt()?;
        self.hash_password_with_salt(password, &salt)
    }

    /// Compute the hash `H` from the given password, potentially using configuration stored in
    /// `&self` for the parameters, or otherwise the recommended defaults.
    ///
    /// A large random salt will be generated automatically from the provided RNG.
    ///
    /// # Errors
    /// These will vary by algorithm/implementation of this trait, but may be due to:
    /// - length restrictions on the password
    /// - algorithm-specific internal error
    /// - RNG internal error
    #[cfg(feature = "rand_core")]
    fn hash_password_with_rng<R: TryCryptoRng + ?Sized>(
        &self,
        rng: &mut R,
        password: &[u8],
    ) -> Result<H> {
        let mut salt = [0u8; RECOMMENDED_SALT_LEN];
        rng.try_fill_bytes(&mut salt).map_err(|_| Error::Crypto)?;
        self.hash_password_with_salt(password, &salt)
    }
}

/// Trait for password hashing functions which support customization.
///
/// Generic around a password hash to be returned (typically [`phc::PasswordHash`]).
pub trait CustomizedPasswordHasher<H> {
    /// Algorithm-specific parameters.
    type Params: Clone + Debug + Default;

    /// Compute a password hash from the provided password using an explicit set of customized
    /// algorithm parameters as opposed to the defaults.
    ///
    /// When in doubt, use [`PasswordHasher::hash_password`] instead.
    ///
    /// # Errors
    /// These will vary by algorithm/implementation of this trait, but may be due to:
    /// - length restrictions on the password and/or salt
    /// - algorithm-specific params or internal error
    fn hash_password_customized(
        &self,
        password: &[u8],
        salt: &[u8],
        algorithm: Option<&str>,
        version: Option<Version>,
        params: Self::Params,
    ) -> Result<H>;

    /// Compute a password hash using customized parameters only, using the default algorithm and
    /// version.
    ///
    /// # Errors
    /// These will vary by algorithm/implementation of this trait, but may be due to:
    /// - length restrictions on the password and/or salt
    /// - algorithm-specific params or internal error
    fn hash_password_with_params(
        &self,
        password: &[u8],
        salt: &[u8],
        params: Self::Params,
    ) -> Result<H> {
        self.hash_password_customized(password, salt, None, None, params)
    }
}

/// Trait for password verification.
///
/// Generic around a password hash to be returned (typically [`phc::PasswordHash`]).
///
/// Automatically impl'd for type that impl [`PasswordHasher`] with [`phc::PasswordHash`] as `H`.
///
/// This trait is object safe and can be used to implement abstractions over multiple password
/// hashing algorithms.
pub trait PasswordVerifier<H: ?Sized> {
    /// Compute this password hashing function against the provided password using the parameters
    /// from the provided password hash and see if the computed output matches.
    ///
    /// # Errors
    /// - Returns [`Error::Algorithm`] if the algorithm requested by the hash `H` is unsupported
    /// - Returns [`Error::PasswordInvalid`] if the computed hash for the supplied password does not
    ///   match the expected hash
    /// - May return other algorithm-specific errors
    fn verify_password(&self, password: &[u8], hash: &H) -> Result<()>;
}

#[cfg(feature = "phc")]
impl<T: CustomizedPasswordHasher<phc::PasswordHash>> PasswordVerifier<phc::PasswordHash> for T
where
    T::Params: for<'a> TryFrom<&'a phc::PasswordHash, Error = Error>,
{
    fn verify_password(&self, password: &[u8], hash: &phc::PasswordHash) -> Result<()> {
        #[allow(clippy::single_match)]
        match (&hash.salt, &hash.hash) {
            (Some(salt), Some(expected_output)) => {
                let computed_hash = self.hash_password_customized(
                    password,
                    salt,
                    Some(hash.algorithm.as_str()),
                    hash.version,
                    T::Params::try_from(hash)?,
                )?;

                if let Some(computed_output) = &computed_hash.hash {
                    // See notes on `Output` about the use of a constant-time comparison
                    if expected_output == computed_output {
                        return Ok(());
                    }
                }
            }
            _ => (),
        }

        Err(Error::PasswordInvalid)
    }
}

/// Generate a random salt value of the recommended length using the system's secure RNG.
///
/// # Panics
/// If the system's secure RNG experiences an internal failure.
#[cfg(feature = "getrandom")]
#[must_use]
pub fn generate_salt() -> [u8; RECOMMENDED_SALT_LEN] {
    try_generate_salt().expect("RNG failure")
}

/// Try generating a random salt value of the recommended length using the system's secure RNG,
/// returning errors if they occur.
///
/// # Errors
/// If the system's secure RNG experiences an internal failure.
#[cfg(feature = "getrandom")]
pub fn try_generate_salt() -> core::result::Result<[u8; RECOMMENDED_SALT_LEN], getrandom::Error> {
    let mut salt = [0u8; RECOMMENDED_SALT_LEN];
    getrandom::fill(&mut salt)?;
    Ok(salt)
}
