use std::convert::{TryFrom, TryInto};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use crate::errors::{PasswordError, ResultPwd};
use password_hash::{PasswordHash, SaltString};
use rand_core::OsRng;
pub mod hash_argon2 {
use crate::password::Password;
use argon2::Argon2;
use password_hash::{Ident, PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
pub(crate) const ARGON_IDENT: &[Ident<'_>] = &[
argon2::ARGON2D_IDENT,
argon2::ARGON2I_IDENT,
argon2::ARGON2ID_IDENT,
];
pub(crate) fn hash_password<'a>(
raw: &'a str,
salt: &'a SaltString,
) -> password_hash::Result<PasswordHash<'a>> {
Argon2::default().hash_password(raw.as_ref(), salt.as_ref())
}
pub(crate) fn validate_password(of: &Password, against: &str) -> password_hash::Result<()> {
let p = of.get_hash();
Argon2::default().verify_password(against.as_bytes(), &p)
}
}
#[cfg(feature = "use_scrypt")]
pub mod hash_scrypt {
use crate::password::Password;
use password_hash::{Ident, PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use scrypt;
use scrypt::Scrypt;
pub(crate) const SCRYPT_IDENT: &[Ident<'_>] = &[scrypt::ALG_ID];
pub(crate) fn hash_password<'a>(
raw: &'a str,
salt: &'a SaltString,
) -> password_hash::Result<PasswordHash<'a>> {
Scrypt.hash_password(raw.as_ref(), salt.as_ref())
}
pub(crate) fn validate_password(of: &Password, against: &str) -> password_hash::Result<()> {
let p = of.get_hash();
Scrypt.verify_password(against.as_bytes(), &p)
}
}
pub trait PasswordIsSafe {
fn is_safe(&self, raw: &str) -> ResultPwd<()>;
}
pub const MINIMUM_PASSWORD_LENGTH: usize = 8;
pub const CHECKER_MIN_SIZE: CheckPasswordMinSize = CheckPasswordMinSize {};
pub struct CheckPasswordMinSize {}
impl PasswordIsSafe for CheckPasswordMinSize {
fn is_safe(&self, raw: &str) -> ResultPwd<()> {
let provided = raw.trim().chars().count();
if provided < MINIMUM_PASSWORD_LENGTH {
return Err(PasswordError::MinimumPasswordLength { provided });
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum PasswordAlgo {
Argon2,
Scrypt,
}
impl TryFrom<&PasswordHash<'_>> for PasswordAlgo {
type Error = PasswordError;
fn try_from(value: &PasswordHash<'_>) -> Result<Self, Self::Error> {
if hash_argon2::ARGON_IDENT.contains(&value.algorithm) {
return Ok(PasswordAlgo::Argon2);
}
#[cfg(feature = "use_scrypt")]
if hash_scrypt::SCRYPT_IDENT.contains(&value.algorithm) {
return Ok(PasswordAlgo::Scrypt);
}
Err(PasswordError::InvalidPasswordAlgo {
provided: value.algorithm.as_str().to_string(),
})
}
}
impl PasswordAlgo {
fn max_length(&self) -> usize {
match self {
PasswordAlgo::Argon2 => argon2::MAX_PWD_LEN,
PasswordAlgo::Scrypt => 72,
}
}
}
impl PasswordIsSafe for PasswordAlgo {
fn is_safe(&self, raw: &str) -> ResultPwd<()> {
let provided = raw.trim().chars().count();
if provided > self.max_length() {
Err(PasswordError::MaximumPasswordLength { provided })
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct Password {
pub phc: String,
algo: PasswordAlgo,
}
impl Password {
fn _new(hash: PasswordHash, algo: PasswordAlgo) -> Self {
Password {
phc: hash.to_string(),
algo,
}
}
pub fn new(phc: &str) -> ResultPwd<Self> {
let hash = PasswordHash::new(phc)?;
let algo: PasswordAlgo = (&hash).try_into()?;
algo.is_safe(phc)?;
Ok(Self::_new(hash, algo))
}
pub fn hash(raw: &str, check: impl PasswordIsSafe) -> ResultPwd<Self> {
Self::hash_argon(raw, check)
}
pub unsafe fn hash_unsafe(raw: &str) -> ResultPwd<Self> {
Self::hash_argon_unsafe(raw)
}
pub fn hash_argon(raw: &str, check: impl PasswordIsSafe) -> ResultPwd<Self> {
check.is_safe(raw)?;
unsafe { Self::hash_argon_unsafe(raw) }
}
pub unsafe fn hash_argon_unsafe(raw: &str) -> ResultPwd<Self> {
let salt = Password::salt();
let hash = hash_argon2::hash_password(raw, &salt)?;
Ok(Self::_new(hash, PasswordAlgo::Argon2))
}
#[cfg(feature = "use_scrypt")]
pub fn hash_scrypt(raw: &str, check: impl PasswordIsSafe) -> ResultPwd<Self> {
check.is_safe(raw)?;
unsafe { Self::hash_scrypt_unsafe(raw) }
}
#[cfg(feature = "use_scrypt")]
pub unsafe fn hash_scrypt_unsafe(raw: &str) -> ResultPwd<Self> {
let salt = Password::salt();
let hash = hash_scrypt::hash_password(raw, &salt)?;
Ok(Self::_new(hash, PasswordAlgo::Scrypt))
}
pub fn get_hash(&self) -> PasswordHash {
PasswordHash::new(&self.phc).unwrap()
}
pub fn validate_password(&self, against: &str) -> Result<(), PasswordError> {
match self.algo {
PasswordAlgo::Argon2 => {
hash_argon2::validate_password(self, against)?;
}
PasswordAlgo::Scrypt => {
#[cfg(feature = "use_scrypt")]
hash_scrypt::validate_password(self, against)?;
#[cfg(not(feature = "use_scrypt"))]
return Err(PasswordError::InvalidPasswordAlgo {
provided: "Scrypt".to_string(),
});
}
}
Ok(())
}
pub fn salt() -> SaltString {
SaltString::generate(&mut OsRng)
}
}
impl FromStr for Password {
type Err = PasswordError;
fn from_str(phc: &str) -> Result<Self, Self::Err> {
Self::new(phc)
}
}
impl PartialEq<&str> for Password {
fn eq(&self, other: &&str) -> bool {
self.validate_password(other).is_ok()
}
}
impl Display for Password {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "****")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn make_passwords() {
assert!(Password::new("hi").is_err());
let p = Password::new("$argon2id$v=19$m=4096,t=3,p=1$B+wShXe3YjVd5C8oh4x3pw$XxZJ3BnZMGnBNwPnXrvVM4MMAeFzxf9yxkbXAPcvBzQ").unwrap();
p.validate_password("hi").unwrap();
assert_eq!(p, "hi")
}
}