1use argon2::{
16 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
17 Argon2,
18};
19use pasetors::keys::SymmetricKey;
20use pasetors::token::UntrustedToken;
21use pasetors::version4::V4;
22use pasetors::{claims::Claims, claims::ClaimsValidationRules, Local};
23use secrecy::{ExposeSecret, Secret};
24use serde::{Deserialize, Serialize};
25use time::{Duration, OffsetDateTime};
26use ferrox_errors::AppError;
27
28pub mod auth_middleware;
29pub mod dual_token;
30pub mod public_id;
31pub mod threats;
32pub mod mtd;
33pub mod fingerprint;
34
35pub fn hash_password(password: Secret<String>) -> Result<String, AppError> {
38 let salt = SaltString::generate(&mut OsRng);
39 let argon2 = Argon2::default();
40
41 let password_hash = argon2
42 .hash_password(password.expose_secret().as_bytes(), &salt)
43 .map_err(|e| AppError::InternalServerError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))))?;
44
45 Ok(password_hash.to_string())
46}
47
48pub fn verify_password(password: Secret<String>, hash: &str) -> Result<bool, AppError> {
50 let parsed_hash = PasswordHash::new(hash)
51 .map_err(|e| AppError::ValidationError(format!("Invalid hash format: {}", e)))?;
52
53 let argon2 = Argon2::default();
54 Ok(argon2
55 .verify_password(password.expose_secret().as_bytes(), &parsed_hash)
56 .is_ok())
57}
58
59pub struct PasetoAuth {
61 key: SymmetricKey<V4>,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct AuthPayload {
66 pub user_id: String,
67 pub role: String,
68}
69
70impl PasetoAuth {
71 pub fn new(secret: Secret<String>) -> Result<Self, AppError> {
72 let bytes = secret.expose_secret().as_bytes();
73 let mut key_bytes = [0u8; 32];
74 if bytes.len() >= 32 {
75 key_bytes.copy_from_slice(&bytes[..32]);
76 } else {
77 key_bytes[..bytes.len()].copy_from_slice(bytes);
78 }
79
80 let key = SymmetricKey::<V4>::from(&key_bytes)
81 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
82 Ok(Self { key })
83 }
84
85 pub fn generate_token(&self, payload: &AuthPayload, duration: Duration) -> Result<String, AppError> {
87 let mut claims = Claims::new().map_err(|e| AppError::InternalServerError(Box::new(e)))?;
88
89 let exp = OffsetDateTime::now_utc() + duration;
91 let exp_iso = exp.format(&time::format_description::well_known::Rfc3339)
92 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
93 claims.expiration(&exp_iso).map_err(|e| AppError::InternalServerError(Box::new(e)))?;
94
95 let user_id_val = serde_json::to_value(&payload.user_id)
97 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
98 claims.add_additional("user_id", user_id_val)
99 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
100
101 let role_val = serde_json::to_value(&payload.role)
102 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
103 claims.add_additional("role", role_val)
104 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
105
106 pasetors::local::encrypt(&self.key, &claims, None, Some(b"ferrox-auth-footer"))
108 .map_err(|e| AppError::InternalServerError(Box::new(e)))
109 }
110
111 pub fn validate_token(&self, token: &str) -> Result<AuthPayload, AppError> {
113 let validation_rules = ClaimsValidationRules::new();
114
115 let untrusted_token = pasetors::token::UntrustedToken::<pasetors::Local, pasetors::version4::V4>::try_from(token)
116 .map_err(|_| AppError::ValidationError("Invalid token format".to_string()))?;
117
118 let trusted_token = pasetors::local::decrypt(
119 &self.key,
120 &untrusted_token,
121 &validation_rules,
122 None,
123 Some(b"ferrox-auth-footer"),
124 ).map_err(|e| AppError::InternalServerError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))))?;
125
126 let payload_json = trusted_token.payload();
127
128 let mut claims: serde_json::Value = serde_json::from_slice(payload_json.as_bytes())
129 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
130
131 let user_id = claims.get("user_id")
132 .and_then(|v| v.as_str())
133 .map(|s| s.to_string())
134 .ok_or_else(|| AppError::ValidationError("Missing user_id claim".to_string()))?;
135
136 let role = claims.get("role")
137 .and_then(|v| v.as_str())
138 .map(|s| s.to_string())
139 .ok_or_else(|| AppError::ValidationError("Missing role claim".to_string()))?;
140
141 Ok(AuthPayload { user_id, role })
142 }
143}
144
145pub fn setup() {
146 println!("ferrox-security initialized: Argon2 Hashing and PASETO Authentication ready.");
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_argon2_hashing() {
155 let password = Secret::new("SuperSecureP@ssw0rd!".to_string());
156
157 let hash = hash_password(password.clone()).unwrap();
159 assert!(hash.starts_with("$argon2"));
160
161 let is_valid = verify_password(password, &hash).unwrap();
163 assert!(is_valid);
164
165 let wrong_password = Secret::new("WrongPassword!".to_string());
167 let is_valid_wrong = verify_password(wrong_password, &hash).unwrap();
168 assert!(!is_valid_wrong);
169 }
170
171 #[test]
172 fn test_paseto_token_lifecycle() {
173 let secret = Secret::new("12345678901234567890123456789012".to_string());
175 let auth = PasetoAuth::new(secret).unwrap();
176
177 let payload = AuthPayload {
178 user_id: "user-123".into(),
179 role: "admin".into(),
180 };
181
182 let token = auth.generate_token(&payload, Duration::hours(1)).unwrap();
184 assert!(token.starts_with("v4.local.")); let validated = auth.validate_token(&token).unwrap();
188 assert_eq!(validated.user_id, "user-123");
189 assert_eq!(validated.role, "admin");
190
191 let bad_token = "v4.local.bad_data_here";
193 assert!(auth.validate_token(bad_token).is_err());
194 }
195}