use newtypes for clear and hashed passwords
parent
27ccba5252
commit
ac6d756acb
@ -1,35 +1,101 @@
|
||||
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};
|
||||
|
||||
pub fn hash(clear: &str) -> String {
|
||||
#[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();
|
||||
argon2
|
||||
.hash_password(clear.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
HashedPassword(
|
||||
argon2
|
||||
.hash_password(clear.0.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify(clear: &str, hash: &str) -> Result<()> {
|
||||
let parsed_hash = PasswordHash::new(&hash).unwrap();
|
||||
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<String> for ClearPassword {
|
||||
fn from(val: String) -> ClearPassword {
|
||||
ClearPassword(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> 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("asdf");
|
||||
assert!(verify("asdf", h.as_str()).is_ok());
|
||||
assert!(verify("fdsa", h.as_str()).is_err());
|
||||
assert!(verify("asdf\0", h.as_str()).is_err());
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue