1use std::collections::HashMap;
21use std::num::NonZeroUsize;
22use std::sync::Arc;
23use std::time::{Duration, Instant};
24
25use koan_core::auth::{self, Role};
26use koan_core::db::pool::Pool;
27use koan_core::db::queries::auth as auth_queries;
28use lru::LruCache;
29use parking_lot::{Condvar, Mutex};
30use sha2::{Digest, Sha256};
31
32const REMEMBER: Duration = Duration::from_secs(600);
33
34const WAIT_FOR_CHECK: Duration = Duration::from_secs(5);
36
37fn max_checks() -> usize {
39 std::thread::available_parallelism().map_or(2, |n| n.get().clamp(2, 8))
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Refused {
45 Wrong,
47 Busy,
49}
50
51#[derive(Default)]
53struct Check {
54 outcome: Mutex<Option<bool>>,
55 done: Condvar,
56}
57
58pub struct PasswordVerifier {
59 pool: Arc<Pool>,
60 verified: Mutex<LruCache<[u8; 32], Instant>>,
61 checking: Mutex<HashMap<[u8; 32], Arc<Check>>>,
63 max_checks: usize,
64}
65
66impl PasswordVerifier {
67 pub fn new(pool: Arc<Pool>) -> Self {
68 Self {
69 pool,
70 verified: Mutex::new(LruCache::new(NonZeroUsize::new(256).expect("non-zero"))),
71 checking: Mutex::new(HashMap::new()),
72 max_checks: max_checks(),
73 }
74 }
75
76 pub fn verify(&self, username: &str, password: &str) -> Result<(i64, Role), Refused> {
78 let db = self.pool.get().map_err(|_| Refused::Wrong)?;
79 let user =
80 auth_queries::get_user_by_username(&db.conn, username).map_err(|_| Refused::Wrong)?;
81 let hash = user.as_ref().map_or_else(
84 || super::routes::dummy_password_hash(),
85 |u| u.password_hash.as_str(),
86 );
87 let key: [u8; 32] = Sha256::new()
88 .chain_update(username)
89 .chain_update([0])
90 .chain_update(password)
91 .chain_update([0])
92 .chain_update(hash)
93 .finalize()
94 .into();
95 let fresh = self
96 .verified
97 .lock()
98 .get(&key)
99 .is_some_and(|at| at.elapsed() < REMEMBER);
100 if !fresh && !self.check(key, password, hash)? {
101 return Err(Refused::Wrong);
102 }
103 user.map(|u| (u.id, u.role)).ok_or(Refused::Wrong)
104 }
105
106 fn check(&self, key: [u8; 32], password: &str, hash: &str) -> Result<bool, Refused> {
108 let (check, running) = {
109 let mut checking = self.checking.lock();
110 match checking.get(&key) {
111 Some(check) => (check.clone(), true),
112 None if checking.len() >= self.max_checks => return Err(Refused::Busy),
113 None => {
114 let check = Arc::new(Check::default());
115 checking.insert(key, check.clone());
116 (check, false)
117 }
118 }
119 };
120 if running {
121 let deadline = Instant::now() + WAIT_FOR_CHECK;
122 let mut outcome = check.outcome.lock();
123 while outcome.is_none() && !check.done.wait_until(&mut outcome, deadline).timed_out() {}
124 return outcome.ok_or(Refused::Busy);
125 }
126 let ok = auth::verify_password(password, hash).is_ok();
127 if ok {
128 self.verified.lock().put(key, Instant::now());
129 }
130 *check.outcome.lock() = Some(ok);
131 check.done.notify_all();
132 self.checking.lock().remove(&key);
133 Ok(ok)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use koan_core::db::connection::Database;
141
142 fn verifier() -> (PasswordVerifier, tempfile::TempDir) {
143 let dir = tempfile::tempdir().unwrap();
144 let path = dir.path().join("test.db");
145 let db = Database::open(&path).unwrap();
146 koan_core::db::schema::create_tables(&db.conn).unwrap();
147 auth_queries::create_user(&db.conn, "mate", "hunter22", Role::Readonly).unwrap();
148 (PasswordVerifier::new(Arc::new(Pool::new(path))), dir)
149 }
150
151 #[test]
152 fn right_password_gives_the_users_role() {
153 let (v, _dir) = verifier();
154 assert_eq!(v.verify("mate", "hunter22"), Ok((1, Role::Readonly)));
155 assert_eq!(v.verify("mate", "hunter22"), Ok((1, Role::Readonly)));
157 }
158
159 #[test]
160 fn wrong_password_or_unknown_user_is_refused() {
161 let (v, _dir) = verifier();
162 assert_eq!(v.verify("mate", "hunter2"), Err(Refused::Wrong));
163 assert_eq!(v.verify("nobody", "hunter22"), Err(Refused::Wrong));
164 }
165
166 #[test]
167 fn checks_beyond_the_ceiling_are_refused_without_running() {
168 let (mut v, _dir) = verifier();
169 v.max_checks = 1;
170 assert!(v.verify("mate", "hunter22").is_ok());
171 v.checking.lock().insert([0; 32], Arc::default());
172 assert_eq!(v.verify("nobody", "guess"), Err(Refused::Busy));
173 assert_eq!(v.verify("mate", "hunter2"), Err(Refused::Busy));
174 assert_eq!(v.verify("mate", "hunter22"), Ok((1, Role::Readonly)));
176 }
177
178 #[test]
179 fn a_burst_of_one_sign_in_waits_for_a_single_check() {
180 let (mut v, _dir) = verifier();
181 v.max_checks = 1;
182 let v = Arc::new(v);
183 let burst: Vec<_> = (0..8)
184 .map(|_| {
185 let v = v.clone();
186 std::thread::spawn(move || v.verify("mate", "hunter22"))
187 })
188 .collect();
189 for t in burst {
190 assert_eq!(t.join().unwrap(), Ok((1, Role::Readonly)));
191 }
192 }
193
194 #[test]
195 fn a_changed_password_forgets_the_old_one() {
196 let (v, dir) = verifier();
197 assert!(v.verify("mate", "hunter22").is_ok());
198 let db = Database::open(&dir.path().join("test.db")).unwrap();
199 auth_queries::update_password(&db.conn, "mate", "correct horse").unwrap();
200 assert_eq!(v.verify("mate", "hunter22"), Err(Refused::Wrong));
201 assert_eq!(v.verify("mate", "correct horse"), Ok((1, Role::Readonly)));
202 }
203}