Files
libertas/libertas_api/src/security.rs
2025-11-02 09:31:01 +01:00

116 lines
3.4 KiB
Rust

use argon2::{
Argon2,
password_hash::{
PasswordHash, PasswordHasher as _, PasswordVerifier, SaltString, rand_core::OsRng,
},
};
use async_trait::async_trait;
use chrono::{Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use libertas_core::error::{CoreError, CoreResult};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[async_trait]
pub trait PasswordHasher: Send + Sync {
async fn hash_password(&self, password: &str) -> CoreResult<String>;
async fn verify_password(&self, password: &str, hash: &str) -> CoreResult<()>;
}
pub struct Argon2Hasher;
impl Default for Argon2Hasher {
fn default() -> Self {
Self {}
}
}
#[async_trait]
impl PasswordHasher for Argon2Hasher {
async fn hash_password(&self, password: &str) -> CoreResult<String> {
let salt = SaltString::generate(&mut OsRng);
let password_bytes = password.as_bytes().to_vec();
let hash_string = tokio::task::spawn_blocking(move || {
Argon2::default()
.hash_password(&password_bytes, &salt)
.map(|hash| hash.to_string())
.map_err(|e| CoreError::Auth(format!("Password hashing failed: {}", e)))
})
.await
.unwrap()?;
Ok(hash_string)
}
async fn verify_password(&self, password: &str, hash_str: &str) -> CoreResult<()> {
let hash = PasswordHash::new(hash_str)
.map_err(|e| CoreError::Auth(format!("Invalid password hash format: {}", e)))?;
let (password_bytes, hash_bytes) = (password.as_bytes().to_vec(), hash.to_string());
tokio::task::spawn_blocking(move || {
Argon2::default()
.verify_password(&password_bytes, &PasswordHash::new(&hash_bytes).unwrap())
.map_err(|_| CoreError::Auth("Invalid password".to_string()))
})
.await
.unwrap()
}
}
static JWT_HEADER: Lazy<Header> = Lazy::new(Header::default);
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Claims {
sub: Uuid,
exp: usize,
iat: usize,
}
#[async_trait]
pub trait TokenGenerator: Send + Sync {
fn generate_token(&self, user_id: Uuid) -> CoreResult<String>;
fn verify_token(&self, token: &str) -> CoreResult<Uuid>;
}
pub struct JwtGenerator {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}
impl JwtGenerator {
pub fn new(secret: String) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
}
}
}
#[async_trait]
impl TokenGenerator for JwtGenerator {
fn generate_token(&self, user_id: Uuid) -> CoreResult<String> {
let now = Utc::now();
let iat = now.timestamp() as usize;
let exp = (now + Duration::days(7)).timestamp() as usize;
let claims = Claims {
sub: user_id,
iat,
exp,
};
encode(&JWT_HEADER, &claims, &self.encoding_key)
.map_err(|e| CoreError::Auth(format!("Token generation failed: {}", e)))
}
fn verify_token(&self, token: &str) -> CoreResult<Uuid> {
decode::<Claims>(token, &self.decoding_key, &Validation::default())
.map(|data| data.claims.sub)
.map_err(|e| CoreError::Auth(format!("Token invalid: {}", e)))
}
}