use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use sha2::{Sha256, Digest};
use rand::Rng;
use base64::{engine::general_purpose::STANDARD as BASE64_ENGINE, Engine};
pub struct PasswordHasher;
impl PasswordHasher {
const ITERATIONS: u32 = 100_000; const SALT_LENGTH: usize = 32;
pub fn hash_password(password: &str) -> Result<String, String> {
let salt = Self::generate_salt();
let hash = Self::pbkdf2_hash(password.as_bytes(), &salt, Self::ITERATIONS);
let mut result = Vec::new();
result.extend_from_slice(&salt);
result.extend_from_slice(&hash);
Ok(BASE64_ENGINE.encode(result))
}
pub fn verify_password(password: &str, stored_hash: &str) -> Result<bool, String> {
let decoded = BASE64_ENGINE.decode(stored_hash)
.map_err(|e| format!("Invalid hash format: {}", e))?;
if decoded.len() < Self::SALT_LENGTH + 32 {
return Err("Invalid hash length".to_string());
}
let salt = &decoded[0..Self::SALT_LENGTH];
let stored_hash_bytes = &decoded[Self::SALT_LENGTH..];
let computed_hash = Self::pbkdf2_hash(password.as_bytes(), salt, Self::ITERATIONS);
Ok(Self::constant_time_compare(&computed_hash, stored_hash_bytes))
}
fn generate_salt() -> Vec<u8> {
let mut rng = rand::thread_rng();
(0..Self::SALT_LENGTH).map(|_| rng.gen()).collect()
}
fn pbkdf2_hash(password: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
let mut result = vec![0u8; 32];
let mut u = Vec::new();
for block in 1..=1 {
let mut block_bytes = [0u8; 4];
block_bytes[3] = block as u8;
u.clear();
u.extend_from_slice(salt);
u.extend_from_slice(&block_bytes);
let mut current = Self::hmac_sha256(password, &u);
let mut t = current.clone();
for _ in 1..iterations {
current = Self::hmac_sha256(password, ¤t);
for (i, &byte) in current.iter().enumerate() {
t[i] ^= byte;
}
}
result[0..32].copy_from_slice(&t);
}
result
}
fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
const BLOCK_SIZE: usize = 64;
let mut key_padded = vec![0u8; BLOCK_SIZE];
if key.len() > BLOCK_SIZE {
let mut hasher = Sha256::new();
hasher.update(key);
key_padded[0..32].copy_from_slice(&hasher.finalize());
} else {
key_padded[0..key.len()].copy_from_slice(key);
}
let mut ipad = vec![0x36u8; BLOCK_SIZE];
let mut opad = vec![0x5cu8; BLOCK_SIZE];
for i in 0..BLOCK_SIZE {
ipad[i] ^= key_padded[i];
opad[i] ^= key_padded[i];
}
let mut inner_hasher = Sha256::new();
inner_hasher.update(&ipad);
inner_hasher.update(data);
let inner_hash = inner_hasher.finalize();
let mut outer_hasher = Sha256::new();
outer_hasher.update(&opad);
outer_hasher.update(&inner_hash);
outer_hasher.finalize().to_vec()
}
fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
}
#[derive(Debug, Clone)]
pub struct SecureSession {
pub id: String,
pub user_id: String,
pub roles: Vec<String>,
pub permissions: Vec<String>,
pub created_at: u64,
pub expires_at: u64,
pub last_activity: u64,
pub session_token: String,
pub csrf_token: String,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
impl SecureSession {
pub fn new(user_id: String, roles: Vec<String>, ip_address: Option<String>, user_agent: Option<String>) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let session_id = Self::generate_secure_id();
let session_token = Self::generate_secure_token();
let csrf_token = Self::generate_secure_token();
let permissions = Self::get_permissions_for_roles(&roles);
Self {
id: session_id,
user_id,
roles,
permissions,
created_at: now,
expires_at: now + (24 * 60 * 60), last_activity: now,
session_token,
csrf_token,
ip_address,
user_agent,
}
}
pub fn is_valid(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now < self.expires_at && (now - self.last_activity) < (30 * 60) }
pub fn refresh(&mut self) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.last_activity = now;
if self.expires_at - now < (2 * 60 * 60) { self.expires_at = now + (24 * 60 * 60); }
}
fn generate_secure_id() -> String {
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
hex::encode(bytes)
}
fn generate_secure_token() -> String {
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
BASE64_ENGINE.encode(bytes)
}
fn get_permissions_for_roles(roles: &[String]) -> Vec<String> {
let mut permissions = Vec::new();
for role in roles {
match role.as_str() {
"admin" => {
permissions.extend_from_slice(&[
"read".to_string(),
"write".to_string(),
"delete".to_string(),
"admin".to_string(),
"user_management".to_string(),
"system_config".to_string(),
]);
}
"moderator" => {
permissions.extend_from_slice(&[
"read".to_string(),
"write".to_string(),
"moderate".to_string(),
"user_management".to_string(),
]);
}
"user" => {
permissions.extend_from_slice(&[
"read".to_string(),
"write".to_string(),
]);
}
_ => {
permissions.push("read".to_string());
}
}
}
permissions.sort();
permissions.dedup();
permissions
}
}
pub struct SecureUserStore {
users: HashMap<String, SecureUser>,
sessions: HashMap<String, SecureSession>,
#[allow(dead_code)]
failed_login_attempts: HashMap<String, (u32, u64)>, }
#[derive(Debug, Clone)]
pub struct SecureUser {
pub id: String,
pub username: String,
pub password_hash: String,
pub roles: Vec<String>,
pub email: String,
pub created_at: u64,
pub last_login: Option<u64>,
pub is_active: bool,
pub failed_login_attempts: u32,
pub locked_until: Option<u64>,
}
impl SecureUserStore {
const MAX_LOGIN_ATTEMPTS: u32 = 5;
const LOCKOUT_DURATION: u64 = 15 * 60;
pub fn new() -> Self {
Self {
users: HashMap::new(),
sessions: HashMap::new(),
failed_login_attempts: HashMap::new(),
}
}
pub fn create_user(&mut self, username: String, password: String, email: String, roles: Vec<String>) -> Result<String, String> {
if self.users.values().any(|u| u.username == username || u.email == email) {
return Err("User already exists".to_string());
}
if !Self::is_strong_password(&password) {
return Err("Password does not meet security requirements".to_string());
}
let password_hash = PasswordHasher::hash_password(&password)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let user_id = format!("user_{}", now);
let user = SecureUser {
id: user_id.clone(),
username,
password_hash,
roles,
email,
created_at: now,
last_login: None,
is_active: true,
failed_login_attempts: 0,
locked_until: None,
};
self.users.insert(user_id.clone(), user);
Ok(user_id)
}
pub fn authenticate(&mut self, username: String, password: String, ip_address: Option<String>, user_agent: Option<String>) -> Result<SecureSession, String> {
let user = self.users.values_mut()
.find(|u| u.username == username)
.ok_or("Invalid credentials")?;
if let Some(locked_until) = user.locked_until {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if now < locked_until {
return Err(format!("Account locked until {}", locked_until));
} else {
user.locked_until = None;
user.failed_login_attempts = 0;
}
}
if !user.is_active {
return Err("Account is disabled".to_string());
}
if !PasswordHasher::verify_password(&password, &user.password_hash)? {
user.failed_login_attempts += 1;
if user.failed_login_attempts >= Self::MAX_LOGIN_ATTEMPTS {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
user.locked_until = Some(now + Self::LOCKOUT_DURATION);
return Err("Account locked due to too many failed attempts".to_string());
}
return Err("Invalid credentials".to_string());
}
user.failed_login_attempts = 0;
user.locked_until = None;
user.last_login = Some(SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs());
let session = SecureSession::new(user.id.clone(), user.roles.clone(), ip_address, user_agent);
let session_id = session.id.clone();
self.sessions.insert(session_id, session.clone());
Ok(session)
}
pub fn validate_session(&mut self, session_id: &str) -> Option<SecureSession> {
let is_valid = if let Some(session) = self.sessions.get(session_id) {
session.is_valid()
} else {
return None;
};
if is_valid {
if let Some(session) = self.sessions.get_mut(session_id) {
session.refresh();
Some(session.clone())
} else {
None
}
} else {
self.sessions.remove(session_id);
None
}
}
pub fn logout(&mut self, session_id: &str) {
self.sessions.remove(session_id);
}
pub fn get_user(&self, user_id: &str) -> Option<&SecureUser> {
self.users.get(user_id)
}
pub fn get_user_by_username(&self, username: &str) -> Option<&SecureUser> {
self.users.values().find(|u| u.username == username)
}
pub fn create_session_for_user(&mut self, user_id: &str) -> Result<SecureSession, String> {
let user = self.users.get(user_id).ok_or("User not found")?;
if !user.is_active {
return Err("User is disabled".to_string());
}
let session = SecureSession::new(user_id.to_string(), user.roles.clone(), None, None);
let session_id = session.id.clone();
self.sessions.insert(session_id, session.clone());
Ok(session)
}
pub fn get_session_by_token(&self, token: &str) -> Option<SecureSession> {
self.sessions
.values()
.find(|s| s.session_token == token && s.is_valid())
.cloned()
}
pub fn get_session(&self, session_id: &str) -> Option<&SecureSession> {
self.sessions.get(session_id)
}
pub fn try_validate_credentials(&mut self, username: &str, password: &str) -> Option<String> {
let user_id = self.users.iter().find(|(_, u)| u.username == username).map(|(id, _)| id.clone())?;
let user = self.users.get_mut(&user_id)?;
if let Some(locked_until) = user.locked_until {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
if now < locked_until {
return None;
}
user.locked_until = None;
user.failed_login_attempts = 0;
}
if !user.is_active {
return None;
}
if !PasswordHasher::verify_password(password, &user.password_hash).unwrap_or(false) {
user.failed_login_attempts += 1;
if user.failed_login_attempts >= Self::MAX_LOGIN_ATTEMPTS {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
user.locked_until = Some(now + Self::LOCKOUT_DURATION);
}
return None;
}
user.failed_login_attempts = 0;
user.locked_until = None;
user.last_login = Some(SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs());
Some(user_id)
}
fn is_strong_password(password: &str) -> bool {
password.len() >= 8 &&
password.chars().any(|c| c.is_ascii_lowercase()) &&
password.chars().any(|c| c.is_ascii_uppercase()) &&
password.chars().any(|c| c.is_ascii_digit()) &&
password.chars().any(|c| !c.is_alphanumeric())
}
}
pub struct RateLimiter {
requests: HashMap<String, Vec<u64>>, max_requests: u32,
window_seconds: u64,
}
impl RateLimiter {
pub fn new(max_requests: u32, window_seconds: u64) -> Self {
Self {
requests: HashMap::new(),
max_requests,
window_seconds,
}
}
pub fn is_allowed(&mut self, key: &str) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let window_start = now - self.window_seconds;
let requests = self.requests.entry(key.to_string()).or_insert(Vec::new());
requests.retain(|×tamp| timestamp > window_start);
if requests.len() >= self.max_requests as usize {
false
} else {
requests.push(now);
true
}
}
pub fn remaining_requests(&self, key: &str) -> u32 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let window_start = now - self.window_seconds;
if let Some(requests) = self.requests.get(key) {
let current_requests = requests.iter()
.filter(|&×tamp| timestamp > window_start)
.count() as u32;
if current_requests >= self.max_requests {
0
} else {
self.max_requests - current_requests
}
} else {
self.max_requests
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_hashing() {
let password = format!("test_pwd_{}", std::process::id());
let hash = PasswordHasher::hash_password(&password).unwrap();
assert!(PasswordHasher::verify_password(&password, &hash).unwrap());
let wrong = format!("wrong_{}", std::process::id());
assert!(!PasswordHasher::verify_password(&wrong, &hash).unwrap());
}
#[test]
fn test_session_management() {
let session = SecureSession::new(
"user123".to_string(),
vec!["user".to_string()],
Some("192.168.1.1".to_string()),
Some("Mozilla/5.0".to_string())
);
assert!(session.is_valid());
assert!(session.permissions.contains(&"read".to_string()));
assert!(session.permissions.contains(&"write".to_string()));
assert!(!session.permissions.contains(&"admin".to_string()));
}
#[test]
fn test_rate_limiting() {
let mut limiter = RateLimiter::new(3, 60);
assert!(limiter.is_allowed("user1"));
assert!(limiter.is_allowed("user1"));
assert!(limiter.is_allowed("user1"));
assert!(!limiter.is_allowed("user1"));
assert!(limiter.is_allowed("user2"));
}
}