1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Credential cache for bcrypt password verification.
//!
//! Bcrypt verification is intentionally slow (~250ms at cost 12) to resist brute-force attacks.
//! This cache stores successful authentication results for a configurable TTL to avoid
//! repeated bcrypt verification for the same credentials.
//!
//! Security considerations:
//! - Passwords are never stored in plaintext
//! - Cache keys are SHA-256 hashes of username:password
//! - Cache entries expire after the configured TTL
//! - Failed authentications are never cached
use parking_lot::RwLock;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
/// A cache entry with expiration time
struct CacheEntry {
/// When this entry expires
expires_at: Instant,
}
/// Thread-safe credential cache with TTL expiration.
///
/// This cache stores SHA-256 hashes of successfully verified credentials
/// to avoid repeated bcrypt verification within the TTL window.
#[derive(Clone)]
pub struct CredentialCache {
/// Cache of credential hashes to expiration times
cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
/// Time-to-live for cache entries
ttl: Duration,
}
impl CredentialCache {
/// Create a new credential cache with the specified TTL.
///
/// # Arguments
/// * `ttl` - How long successful authentications should be cached
pub fn new(ttl: Duration) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
/// Generate a cache key from username and password.
/// Uses SHA-256 to avoid storing plaintext passwords in memory.
fn cache_key(username: &str, password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(username.as_bytes());
hasher.update(b":");
hasher.update(password.as_bytes());
format!("{:x}", hasher.finalize())
}
/// Check if credentials are cached and still valid.
///
/// Returns `true` if the credentials are in the cache and haven't expired.
pub fn is_cached(&self, username: &str, password: &str) -> bool {
let key = Self::cache_key(username, password);
let cache = self.cache.read();
if let Some(entry) = cache.get(&key)
&& entry.expires_at > Instant::now()
{
return true;
}
false
}
/// Cache a successful authentication.
///
/// Only call this after bcrypt verification succeeds.
pub fn cache_success(&self, username: &str, password: &str) {
let key = Self::cache_key(username, password);
let entry = CacheEntry {
expires_at: Instant::now() + self.ttl,
};
let mut cache = self.cache.write();
cache.insert(key, entry);
// Opportunistically clean up expired entries if cache is getting large
if cache.len() > 1000 {
self.cleanup_expired_internal(&mut cache);
}
}
/// Remove expired entries from the cache.
fn cleanup_expired_internal(&self, cache: &mut HashMap<String, CacheEntry>) {
let now = Instant::now();
cache.retain(|_, entry| entry.expires_at > now);
}
/// Clear all cached entries.
///
/// Used when the htpasswd file is reloaded to invalidate stale credentials.
pub fn clear(&self) {
self.cache.write().clear();
}
/// Get the number of entries in the cache (for debugging/monitoring).
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.cache.read().len()
}
/// Check if the cache is empty.
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.cache.read().is_empty()
}
}
impl std::fmt::Debug for CredentialCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CredentialCache")
.field("ttl", &self.ttl)
.field("entries", &self.cache.read().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_cache_hit() {
let cache = CredentialCache::new(Duration::from_secs(60));
// Initially not cached
assert!(!cache.is_cached("user", "password"));
// Cache the credentials
cache.cache_success("user", "password");
// Now should be cached
assert!(cache.is_cached("user", "password"));
// Different password should not be cached
assert!(!cache.is_cached("user", "wrong_password"));
// Different user should not be cached
assert!(!cache.is_cached("other_user", "password"));
}
#[test]
fn test_cache_expiry() {
let cache = CredentialCache::new(Duration::from_millis(50));
cache.cache_success("user", "password");
assert!(cache.is_cached("user", "password"));
// Wait for expiry
sleep(Duration::from_millis(100));
// Should no longer be cached
assert!(!cache.is_cached("user", "password"));
}
#[test]
fn test_cache_key_uniqueness() {
// Ensure different username:password combinations produce different keys
let key1 = CredentialCache::cache_key("user", "pass");
let key2 = CredentialCache::cache_key("user", "pass2");
let key3 = CredentialCache::cache_key("user2", "pass");
assert_ne!(key1, key2);
assert_ne!(key1, key3);
assert_ne!(key2, key3);
}
}