1use std::collections::HashMap;
22use std::num::NonZeroUsize;
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use koan_core::auth::{self, Role};
27use koan_core::db::pool::Pool;
28use koan_core::db::queries::auth as auth_queries;
29use lru::LruCache;
30use parking_lot::{Condvar, Mutex};
31use sha2::{Digest, Sha256};
32
33const REMEMBER: Duration = Duration::from_secs(600);
34
35const WAIT_FOR_CHECK: Duration = Duration::from_secs(5);
37
38fn max_checks() -> usize {
40 std::thread::available_parallelism().map_or(2, |n| n.get().clamp(2, 8))
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Refused {
46 Wrong,
48 Busy,
50}
51
52#[derive(Default)]
54struct Check {
55 outcome: Mutex<Option<bool>>,
56 done: Condvar,
57}
58
59pub struct PasswordVerifier {
60 pool: Arc<Pool>,
61 verified: Mutex<LruCache<[u8; 32], Instant>>,
62 checking: Mutex<HashMap<[u8; 32], Arc<Check>>>,
64 max_checks: usize,
65 sealing: Option<[u8; 32]>,
68}
69
70impl PasswordVerifier {
71 pub fn new(pool: Arc<Pool>) -> Self {
72 let sealing = auth::subsonic_key()
73 .inspect_err(|e| log::warn!("Subsonic token auth for accounts is off: {e}"))
74 .ok();
75 Self::with_key(pool, sealing)
76 }
77
78 pub fn with_key(pool: Arc<Pool>, sealing: Option<[u8; 32]>) -> Self {
79 Self {
80 pool,
81 verified: Mutex::new(LruCache::new(NonZeroUsize::new(256).expect("non-zero"))),
82 checking: Mutex::new(HashMap::new()),
83 max_checks: max_checks(),
84 sealing,
85 }
86 }
87
88 pub fn verify_token(
92 &self,
93 username: &str,
94 token: &str,
95 salt: &str,
96 ) -> Result<(i64, Role), Refused> {
97 use subtle::ConstantTimeEq;
98 let password = (|| {
99 let key = self.sealing.as_ref()?;
100 let sealed =
101 auth_queries::sealed_password(&self.pool.get().ok()?.conn, username).ok()??;
102 auth::open_password(key, username, &sealed)
103 })()
104 .ok_or(Refused::Wrong)?;
105 let expected = format!("{:x}", md5::compute(format!("{password}{salt}")));
106 if !bool::from(
107 token
108 .to_ascii_lowercase()
109 .as_bytes()
110 .ct_eq(expected.as_bytes()),
111 ) {
112 return Err(Refused::Wrong);
113 }
114 self.verify(username, &password)
115 }
116
117 pub fn has_sealed(&self, username: &str) -> bool {
119 self.sealing.is_some()
120 && self
121 .pool
122 .get()
123 .ok()
124 .and_then(|db| auth_queries::sealed_password(&db.conn, username).ok())
125 .flatten()
126 .is_some()
127 }
128
129 pub fn verify(&self, username: &str, password: &str) -> Result<(i64, Role), Refused> {
131 let db = self.pool.get().map_err(|_| Refused::Wrong)?;
132 let user =
133 auth_queries::get_user_by_username(&db.conn, username).map_err(|_| Refused::Wrong)?;
134 let hash = user.as_ref().map_or_else(
137 || super::routes::dummy_password_hash(),
138 |u| u.password_hash.as_str(),
139 );
140 let key: [u8; 32] = Sha256::new()
141 .chain_update(username)
142 .chain_update([0])
143 .chain_update(password)
144 .chain_update([0])
145 .chain_update(hash)
146 .finalize()
147 .into();
148 let fresh = self
149 .verified
150 .lock()
151 .get(&key)
152 .is_some_and(|at| at.elapsed() < REMEMBER);
153 if !fresh {
154 if !self.check(key, password, hash)? {
155 return Err(Refused::Wrong);
156 }
157 if let Some(k) = &self.sealing
158 && user.is_some()
159 && let Ok(sealed) = auth::seal_password(k, username, password)
160 {
161 let _ = auth_queries::set_sealed_password(&db.conn, username, &sealed);
162 }
163 }
164 user.map(|u| (u.id, u.role)).ok_or(Refused::Wrong)
165 }
166
167 fn check(&self, key: [u8; 32], password: &str, hash: &str) -> Result<bool, Refused> {
169 let (check, running) = {
170 let mut checking = self.checking.lock();
171 match checking.get(&key) {
172 Some(check) => (check.clone(), true),
173 None if checking.len() >= self.max_checks => return Err(Refused::Busy),
174 None => {
175 let check = Arc::new(Check::default());
176 checking.insert(key, check.clone());
177 (check, false)
178 }
179 }
180 };
181 if running {
182 let deadline = Instant::now() + WAIT_FOR_CHECK;
183 let mut outcome = check.outcome.lock();
184 while outcome.is_none() && !check.done.wait_until(&mut outcome, deadline).timed_out() {}
185 return outcome.ok_or(Refused::Busy);
186 }
187 let ok = auth::verify_password(password, hash).is_ok();
188 if ok {
189 self.verified.lock().put(key, Instant::now());
190 }
191 *check.outcome.lock() = Some(ok);
192 check.done.notify_all();
193 self.checking.lock().remove(&key);
194 Ok(ok)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use koan_core::db::connection::Database;
202
203 fn verifier() -> (PasswordVerifier, tempfile::TempDir) {
204 let dir = tempfile::tempdir().unwrap();
205 let path = dir.path().join("test.db");
206 let db = Database::open(&path).unwrap();
207 koan_core::db::schema::create_tables(&db.conn).unwrap();
208 auth_queries::create_user(&db.conn, "mate", "hunter22", Role::Readonly).unwrap();
209 (
210 PasswordVerifier::with_key(Arc::new(Pool::new(path)), Some([7; 32])),
211 dir,
212 )
213 }
214
215 #[test]
216 fn right_password_gives_the_users_role() {
217 let (v, _dir) = verifier();
218 assert_eq!(v.verify("mate", "hunter22"), Ok((1, Role::Readonly)));
219 assert_eq!(v.verify("mate", "hunter22"), Ok((1, Role::Readonly)));
221 }
222
223 #[test]
224 fn token_auth_works_once_a_password_sign_in_sealed_it() {
225 let (v, _dir) = verifier();
226 let token = |pw: &str, salt: &str| format!("{:x}", md5::compute(format!("{pw}{salt}")));
227 assert_eq!(
228 v.verify_token("mate", &token("hunter22", "abc"), "abc"),
229 Err(Refused::Wrong)
230 );
231 assert!(!v.has_sealed("mate"));
232 v.verify("mate", "hunter22").unwrap();
233 assert!(v.has_sealed("mate"));
234 assert_eq!(
235 v.verify_token("mate", &token("hunter22", "abc"), "abc"),
236 Ok((1, Role::Readonly))
237 );
238 assert_eq!(
239 v.verify_token("mate", &token("hunter2", "abc"), "abc"),
240 Err(Refused::Wrong)
241 );
242 assert_eq!(
243 v.verify_token("nobody", &token("hunter22", "abc"), "abc"),
244 Err(Refused::Wrong)
245 );
246 }
247
248 #[test]
249 fn a_password_changed_elsewhere_makes_the_sealed_copy_fail() {
250 let (v, dir) = verifier();
251 v.verify("mate", "hunter22").unwrap();
252 let db = Database::open(&dir.path().join("test.db")).unwrap();
253 auth_queries::update_password(&db.conn, "mate", "correct horse").unwrap();
254 let token = format!("{:x}", md5::compute("hunter22salt"));
255 assert_eq!(v.verify_token("mate", &token, "salt"), Err(Refused::Wrong));
256 }
257
258 #[test]
259 fn a_sealed_password_opens_only_for_its_user_and_key() {
260 let sealed = auth::seal_password(&[1; 32], "mate", "hunter22").unwrap();
261 assert_eq!(
262 auth::open_password(&[1; 32], "mate", &sealed).as_deref(),
263 Some("hunter22")
264 );
265 assert_eq!(auth::open_password(&[1; 32], "owner", &sealed), None);
266 assert_eq!(auth::open_password(&[2; 32], "mate", &sealed), None);
267 }
268
269 #[test]
270 fn wrong_password_or_unknown_user_is_refused() {
271 let (v, _dir) = verifier();
272 assert_eq!(v.verify("mate", "hunter2"), Err(Refused::Wrong));
273 assert_eq!(v.verify("nobody", "hunter22"), Err(Refused::Wrong));
274 }
275
276 #[test]
277 fn checks_beyond_the_ceiling_are_refused_without_running() {
278 let (mut v, _dir) = verifier();
279 v.max_checks = 1;
280 assert!(v.verify("mate", "hunter22").is_ok());
281 v.checking.lock().insert([0; 32], Arc::default());
282 assert_eq!(v.verify("nobody", "guess"), Err(Refused::Busy));
283 assert_eq!(v.verify("mate", "hunter2"), Err(Refused::Busy));
284 assert_eq!(v.verify("mate", "hunter22"), Ok((1, Role::Readonly)));
286 }
287
288 #[test]
289 fn a_burst_of_one_sign_in_waits_for_a_single_check() {
290 let (mut v, _dir) = verifier();
291 v.max_checks = 1;
292 let v = Arc::new(v);
293 let burst: Vec<_> = (0..8)
294 .map(|_| {
295 let v = v.clone();
296 std::thread::spawn(move || v.verify("mate", "hunter22"))
297 })
298 .collect();
299 for t in burst {
300 assert_eq!(t.join().unwrap(), Ok((1, Role::Readonly)));
301 }
302 }
303
304 #[test]
305 fn a_changed_password_forgets_the_old_one() {
306 let (v, dir) = verifier();
307 assert!(v.verify("mate", "hunter22").is_ok());
308 let db = Database::open(&dir.path().join("test.db")).unwrap();
309 auth_queries::update_password(&db.conn, "mate", "correct horse").unwrap();
310 assert_eq!(v.verify("mate", "hunter22"), Err(Refused::Wrong));
311 assert_eq!(v.verify("mate", "correct horse"), Ok((1, Role::Readonly)));
312 }
313}