use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::SaltString; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use serde::{Deserialize, Serialize}; use crate::core::*; use crate::util::validate::{FieldResultBuilder, ValidateField}; #[derive(sqlx::Type, Serialize, Deserialize, Clone)] #[sqlx(transparent)] pub struct ClearPassword(String); #[derive(sqlx::Type, Serialize, Deserialize, Clone)] #[sqlx(transparent)] pub struct HashedPassword(String); pub fn hash(clear: &ClearPassword) -> HashedPassword { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); HashedPassword( argon2 .hash_password(clear.0.as_bytes(), &salt) .unwrap() .to_string(), ) } pub fn verify(clear: &ClearPassword, hash: &HashedPassword) -> Result<()> { let (clear, hash) = (clear.0.as_str(), hash.0.as_str()); let parsed_hash = PasswordHash::new(hash).unwrap(); match Argon2::default().verify_password(clear.as_bytes(), &parsed_hash) { Ok(_) => Ok(()), Err(_) => Err(Error::BadCredentials), } } impl ClearPassword { pub fn len(&self) -> usize { self.0.len() } // these also match non ascii chars but whatever pub fn has_uppercase(&self) -> bool { self.0.chars().any(|c| c.is_uppercase()) } pub fn has_lowercase(&self) -> bool { self.0.chars().any(|c| c.is_lowercase()) } pub fn has_digit(&self) -> bool { self.0.chars().any(|c| c.is_numeric()) } pub fn has_special(&self) -> bool { self.0.chars().any(|c| !c.is_ascii_alphanumeric()) } } impl From for ClearPassword { fn from(val: String) -> ClearPassword { ClearPassword(val) } } impl From for HashedPassword { fn from(val: String) -> HashedPassword { HashedPassword(val) } } impl From<&str> for HashedPassword { fn from(val: &str) -> HashedPassword { HashedPassword(String::from(val)) } } impl<'a> ValidateField<'a> for ClearPassword { fn validate_field(builder: FieldResultBuilder<'a, Self>) -> FieldResultBuilder<'a, Self> { builder .check("Must be at least 8 characters long", |v| v.len() >= 8) .check("Must contain an uppercase character", |v| v.has_uppercase()) .check("Must contain a lowercase character", |v| v.has_lowercase()) .check("Must contain a digit", |v| v.has_digit()) .check("Must contain a special character", |v| v.has_special()) } } #[cfg(test)] mod tests { use crate::util::password::{hash, verify}; #[test] fn validate_hashes() { let h = hash(&String::from("asdf").into()); assert!(verify(&String::from("asdf").into(), &h).is_ok()); assert!(verify(&String::from("fdsa").into(), &h).is_err()); assert!(verify(&String::from("asdf\0").into(), &h).is_err()); } }