You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
102 lines
2.9 KiB
Rust
102 lines
2.9 KiB
Rust
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<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(&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());
|
|
}
|
|
}
|