use newtypes for clear and hashed passwords
This commit is contained in:
parent
27ccba5252
commit
ac6d756acb
4 changed files with 86 additions and 19 deletions
src
|
@ -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…
Reference in a new issue