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

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());
}
}