use newtypes for clear and hashed passwords

main
anna 1 year ago
parent 27ccba5252
commit ac6d756acb
Signed by: fef
GPG Key ID: EC22E476DC2D3D84

@ -4,6 +4,7 @@ use sqlx::FromRow;
use crate::core::*;
use crate::model::AccountId;
use crate::util::keys::PrivateKeyDer;
use crate::util::password::HashedPassword;
use crate::util::validate::{ResultBuilder, Validate};
pub type UserId = Id<User>;
@ -13,7 +14,7 @@ pub struct User {
pub id: UserId,
pub account_id: AccountId,
pub email: String,
pub password: String,
pub password: HashedPassword,
pub reason: Option<String>,
pub locale: String,
pub activated: bool,
@ -23,7 +24,7 @@ pub struct User {
pub struct NewUser {
pub account_id: AccountId,
pub email: String,
pub password: String,
pub password: HashedPassword,
pub reason: Option<String>,
pub locale: String,
pub private_key: PrivateKeyDer,

@ -4,12 +4,13 @@ use serde::{Deserialize, Serialize};
use crate::core::*;
use crate::model::Account;
use crate::state::AppState;
use crate::util::password::ClearPassword;
use crate::util::{password, token};
#[derive(Deserialize)]
struct AuthRequest {
email: String,
password: String,
password: ClearPassword,
}
#[derive(Serialize)]
@ -31,7 +32,7 @@ async fn auth(body: web::Json<AuthRequest>, state: AppState) -> Result<HttpRespo
Error::NotFound => Error::BadCredentials,
e => e,
})?;
password::verify(body.password.as_str(), user.password.as_str())?;
password::verify(&body.password, &user.password)?;
let account = state.repo.accounts.by_id(user.account_id).await?;
let token = token::issue(&state, &account)?;
info!(target: "auth", "Successful login for user {}", &account.name);

@ -7,13 +7,14 @@ use crate::middle::AuthData;
use crate::model::{AccountId, NewAccount, NewUser};
use crate::state::AppState;
use crate::util::keys::generate_keypair;
use crate::util::password::{self, ClearPassword};
use crate::util::validate::{ResultBuilder, Validate};
#[derive(Deserialize)]
struct SignupData {
username: String,
email: String,
password: String,
password: ClearPassword,
agreement: bool,
locale: String,
reason: Option<String>,
@ -60,9 +61,7 @@ impl Validate for SignupData {
"You must accept the Terms of Service",
|v| *v,
)
.field(&self.password, "password", |f| {
f.check("Must be at least 8 characters long", |v| v.len() >= 8)
})
.field_auto(&self.password, "password")
}
}

@ -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…
Cancel
Save