use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use pasetors::keys::SymmetricKey;
use pasetors::token::UntrustedToken;
use pasetors::version4::V4;
use pasetors::{claims::Claims, claims::ClaimsValidationRules, Local};
use secrecy::{ExposeSecret, Secret};
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};
use ferrox_errors::AppError;
pub mod auth_middleware;
pub mod dual_token;
pub mod public_id;
pub mod threats;
pub mod mtd;
pub mod fingerprint;
pub fn hash_password(password: Secret<String>) -> Result<String, AppError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.expose_secret().as_bytes(), &salt)
.map_err(|e| AppError::InternalServerError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))))?;
Ok(password_hash.to_string())
}
pub fn verify_password(password: Secret<String>, hash: &str) -> Result<bool, AppError> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| AppError::ValidationError(format!("Invalid hash format: {}", e)))?;
let argon2 = Argon2::default();
Ok(argon2
.verify_password(password.expose_secret().as_bytes(), &parsed_hash)
.is_ok())
}
pub struct PasetoAuth {
key: SymmetricKey<V4>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthPayload {
pub user_id: String,
pub role: String,
}
impl PasetoAuth {
pub fn new(secret: Secret<String>) -> Result<Self, AppError> {
let bytes = secret.expose_secret().as_bytes();
let mut key_bytes = [0u8; 32];
if bytes.len() >= 32 {
key_bytes.copy_from_slice(&bytes[..32]);
} else {
key_bytes[..bytes.len()].copy_from_slice(bytes);
}
let key = SymmetricKey::<V4>::from(&key_bytes)
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
Ok(Self { key })
}
pub fn generate_token(&self, payload: &AuthPayload, duration: Duration) -> Result<String, AppError> {
let mut claims = Claims::new().map_err(|e| AppError::InternalServerError(Box::new(e)))?;
let exp = OffsetDateTime::now_utc() + duration;
let exp_iso = exp.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
claims.expiration(&exp_iso).map_err(|e| AppError::InternalServerError(Box::new(e)))?;
let user_id_val = serde_json::to_value(&payload.user_id)
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
claims.add_additional("user_id", user_id_val)
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
let role_val = serde_json::to_value(&payload.role)
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
claims.add_additional("role", role_val)
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
pasetors::local::encrypt(&self.key, &claims, None, Some(b"ferrox-auth-footer"))
.map_err(|e| AppError::InternalServerError(Box::new(e)))
}
pub fn validate_token(&self, token: &str) -> Result<AuthPayload, AppError> {
let validation_rules = ClaimsValidationRules::new();
let untrusted_token = pasetors::token::UntrustedToken::<pasetors::Local, pasetors::version4::V4>::try_from(token)
.map_err(|_| AppError::ValidationError("Invalid token format".to_string()))?;
let trusted_token = pasetors::local::decrypt(
&self.key,
&untrusted_token,
&validation_rules,
None,
Some(b"ferrox-auth-footer"),
).map_err(|e| AppError::InternalServerError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))))?;
let payload_json = trusted_token.payload();
let mut claims: serde_json::Value = serde_json::from_slice(payload_json.as_bytes())
.map_err(|e| AppError::InternalServerError(Box::new(e)))?;
let user_id = claims.get("user_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| AppError::ValidationError("Missing user_id claim".to_string()))?;
let role = claims.get("role")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| AppError::ValidationError("Missing role claim".to_string()))?;
Ok(AuthPayload { user_id, role })
}
}
pub fn setup() {
println!("ferrox-security initialized: Argon2 Hashing and PASETO Authentication ready.");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_argon2_hashing() {
let password = Secret::new("SuperSecureP@ssw0rd!".to_string());
let hash = hash_password(password.clone()).unwrap();
assert!(hash.starts_with("$argon2"));
let is_valid = verify_password(password, &hash).unwrap();
assert!(is_valid);
let wrong_password = Secret::new("WrongPassword!".to_string());
let is_valid_wrong = verify_password(wrong_password, &hash).unwrap();
assert!(!is_valid_wrong);
}
#[test]
fn test_paseto_token_lifecycle() {
let secret = Secret::new("12345678901234567890123456789012".to_string());
let auth = PasetoAuth::new(secret).unwrap();
let payload = AuthPayload {
user_id: "user-123".into(),
role: "admin".into(),
};
let token = auth.generate_token(&payload, Duration::hours(1)).unwrap();
assert!(token.starts_with("v4.local."));
let validated = auth.validate_token(&token).unwrap();
assert_eq!(validated.user_id, "user-123");
assert_eq!(validated.role, "admin");
let bad_token = "v4.local.bad_data_here";
assert!(auth.validate_token(bad_token).is_err());
}
}