1use std::collections::HashMap;
30use std::net::IpAddr;
31use std::num::NonZeroUsize;
32use std::sync::Arc;
33use std::time::{Duration, Instant};
34
35use koan_core::auth;
36use koan_core::db::pool::Pool;
37use koan_core::db::queries::auth::{self as auth_queries, UserRow};
38use lru::LruCache;
39use parking_lot::{Condvar, Mutex};
40use sha2::{Digest, Sha256};
41
42const REMEMBER: Duration = Duration::from_secs(600);
43
44const WAIT_FOR_CHECK: Duration = Duration::from_secs(5);
46
47pub(crate) const FAILURES_PER_USERNAME_PER_MINUTE: u32 = 60;
51pub(crate) const FAILURE_WINDOW: Duration = Duration::from_secs(60);
52
53const KNOWN_FOR: Duration = Duration::from_secs(7 * 24 * 3600);
57
58pub(crate) struct FailureLimiter<K> {
60 limit: u32,
61 windows: Mutex<HashMap<K, (Instant, u32)>>,
62}
63
64impl<K: std::hash::Hash + Eq> FailureLimiter<K> {
65 pub(crate) fn new(limit: u32) -> Self {
66 Self {
67 limit,
68 windows: Default::default(),
69 }
70 }
71
72 pub(crate) fn exhausted(&self, key: &K) -> bool {
73 let windows = self.windows.lock();
74 windows
75 .get(key)
76 .is_some_and(|(start, count)| start.elapsed() < FAILURE_WINDOW && *count >= self.limit)
77 }
78
79 pub(crate) fn record(&self, key: K) {
80 let mut windows = self.windows.lock();
81 if windows.len() > 4096 {
82 windows.retain(|_, (start, _)| start.elapsed() < FAILURE_WINDOW);
83 }
84 let entry = windows.entry(key).or_insert((Instant::now(), 0));
85 if entry.0.elapsed() >= FAILURE_WINDOW {
86 *entry = (Instant::now(), 0);
87 }
88 entry.1 += 1;
89 }
90}
91
92fn max_checks() -> usize {
94 std::thread::available_parallelism().map_or(2, |n| n.get().clamp(2, 8))
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Refused {
100 Wrong,
102 Busy,
104}
105
106#[derive(Default)]
108struct Check {
109 outcome: Mutex<Option<bool>>,
110 done: Condvar,
111}
112
113pub struct PasswordVerifier {
114 pool: Arc<Pool>,
115 verified: Mutex<LruCache<[u8; 32], Instant>>,
116 checking: Mutex<HashMap<[u8; 32], Arc<Check>>>,
118 max_checks: usize,
119 failures: FailureLimiter<String>,
124 known: Mutex<LruCache<(String, IpAddr), Instant>>,
126 passes: Mutex<LruCache<[u8; 32], ()>>,
129}
130
131impl PasswordVerifier {
132 pub fn new(pool: Arc<Pool>) -> Self {
133 Self {
134 pool,
135 verified: Mutex::new(LruCache::new(NonZeroUsize::new(256).expect("non-zero"))),
136 checking: Mutex::new(HashMap::new()),
137 max_checks: max_checks(),
138 failures: FailureLimiter::new(FAILURES_PER_USERNAME_PER_MINUTE),
139 known: Mutex::new(LruCache::new(NonZeroUsize::new(4096).expect("non-zero"))),
140 passes: Mutex::new(LruCache::new(NonZeroUsize::new(256).expect("non-zero"))),
141 }
142 }
143
144 pub(crate) fn spent(&self, username: &str, from: IpAddr) -> bool {
150 if crate::demo::enabled() && username == crate::demo::USERNAME {
151 return false;
152 }
153 self.failures.exhausted(&username.to_owned())
154 && !self
155 .known
156 .lock()
157 .get(&(username.to_owned(), super::routes::network(from)))
158 .is_some_and(|at| at.elapsed() < KNOWN_FOR)
159 }
160
161 pub(crate) fn exhausted(&self, username: &str) -> bool {
166 self.failures.exhausted(&username.to_owned())
167 }
168
169 pub(crate) fn failed(&self, username: &str) {
171 self.failures.record(username.to_owned());
172 }
173
174 pub(crate) fn signed_in(&self, username: &str, from: IpAddr) {
176 self.known.lock().put(
177 (username.to_owned(), super::routes::network(from)),
178 Instant::now(),
179 );
180 }
181
182 pub(crate) fn passed(&self, digest: [u8; 32]) {
186 self.passes.lock().put(digest, ());
187 }
188
189 pub(crate) fn took_pass(&self, digest: &[u8; 32]) -> bool {
191 self.passes.lock().pop(digest).is_some()
192 }
193
194 pub fn verify(&self, username: &str, password: &str) -> Result<UserRow, Refused> {
196 let db = self.pool.get().map_err(|_| Refused::Wrong)?;
197 let user =
198 auth_queries::get_user_by_username(&db.conn, username).map_err(|_| Refused::Wrong)?;
199 let hash = user.as_ref().map_or_else(
202 || super::routes::dummy_password_hash(),
203 |u| u.password_hash.as_str(),
204 );
205 let key: [u8; 32] = Sha256::new()
206 .chain_update(username)
207 .chain_update([0])
208 .chain_update(password)
209 .chain_update([0])
210 .chain_update(hash)
211 .finalize()
212 .into();
213 let fresh = self
214 .verified
215 .lock()
216 .get(&key)
217 .is_some_and(|at| at.elapsed() < REMEMBER);
218 if !fresh && !self.check(key, password, hash)? {
219 return Err(Refused::Wrong);
220 }
221 user.ok_or(Refused::Wrong)
222 }
223
224 fn check(&self, key: [u8; 32], password: &str, hash: &str) -> Result<bool, Refused> {
226 let (check, running) = {
227 let mut checking = self.checking.lock();
228 match checking.get(&key) {
229 Some(check) => (check.clone(), true),
230 None if checking.len() >= self.max_checks => return Err(Refused::Busy),
231 None => {
232 let check = Arc::new(Check::default());
233 checking.insert(key, check.clone());
234 (check, false)
235 }
236 }
237 };
238 if running {
239 let deadline = Instant::now() + WAIT_FOR_CHECK;
240 let mut outcome = check.outcome.lock();
241 while outcome.is_none() && !check.done.wait_until(&mut outcome, deadline).timed_out() {}
242 return outcome.ok_or(Refused::Busy);
243 }
244 let ok = auth::verify_password(password, hash).is_ok();
245 if ok {
246 self.verified.lock().put(key, Instant::now());
247 }
248 *check.outcome.lock() = Some(ok);
249 check.done.notify_all();
250 self.checking.lock().remove(&key);
251 Ok(ok)
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use koan_core::auth::Role;
259 use koan_core::db::connection::Database;
260
261 impl PasswordVerifier {
262 fn check_as(&self, username: &str, password: &str) -> Result<(i64, Role), Refused> {
263 self.verify(username, password).map(|u| (u.id, u.role))
264 }
265 }
266
267 fn verifier() -> (PasswordVerifier, tempfile::TempDir) {
268 let dir = tempfile::tempdir().unwrap();
269 let path = dir.path().join("test.db");
270 let db = Database::open(&path).unwrap();
271 koan_core::db::schema::create_tables(&db.conn).unwrap();
272 auth_queries::create_user(&db.conn, "mate", "hunter22", Role::Readonly).unwrap();
273 (PasswordVerifier::new(Arc::new(Pool::new(path))), dir)
274 }
275
276 #[test]
277 fn right_password_gives_the_users_role() {
278 let (v, _dir) = verifier();
279 assert_eq!(v.check_as("mate", "hunter22"), Ok((1, Role::Readonly)));
280 assert_eq!(v.check_as("mate", "hunter22"), Ok((1, Role::Readonly)));
282 }
283
284 #[test]
285 fn wrong_password_or_unknown_user_is_refused() {
286 let (v, _dir) = verifier();
287 assert_eq!(v.check_as("mate", "hunter2"), Err(Refused::Wrong));
288 assert_eq!(v.check_as("nobody", "hunter22"), Err(Refused::Wrong));
289 }
290
291 #[test]
292 fn checks_beyond_the_ceiling_are_refused_without_running() {
293 let (mut v, _dir) = verifier();
294 v.max_checks = 1;
295 assert!(v.check_as("mate", "hunter22").is_ok());
296 v.checking.lock().insert([0; 32], Arc::default());
297 assert_eq!(v.check_as("nobody", "guess"), Err(Refused::Busy));
298 assert_eq!(v.check_as("mate", "hunter2"), Err(Refused::Busy));
299 assert_eq!(v.check_as("mate", "hunter22"), Ok((1, Role::Readonly)));
301 }
302
303 #[test]
304 fn a_burst_of_one_sign_in_waits_for_a_single_check() {
305 let (mut v, _dir) = verifier();
306 v.max_checks = 1;
307 let v = Arc::new(v);
308 let burst: Vec<_> = (0..8)
309 .map(|_| {
310 let v = v.clone();
311 std::thread::spawn(move || v.check_as("mate", "hunter22"))
312 })
313 .collect();
314 for t in burst {
315 assert_eq!(t.join().unwrap(), Ok((1, Role::Readonly)));
316 }
317 }
318
319 #[test]
320 fn a_changed_password_forgets_the_old_one() {
321 let (v, dir) = verifier();
322 assert!(v.check_as("mate", "hunter22").is_ok());
323 let db = Database::open(&dir.path().join("test.db")).unwrap();
324 auth_queries::update_password(&db.conn, "mate", "correct horse").unwrap();
325 assert_eq!(v.check_as("mate", "hunter22"), Err(Refused::Wrong));
326 assert_eq!(v.check_as("mate", "correct horse"), Ok((1, Role::Readonly)));
327 }
328}