Skip to main content

koan_server/auth/
password.rs

1//! Username and password checks for transports that send them with every
2//! request, such as Subsonic's `p=`.
3//!
4//! argon2 is deliberately slow, and a Subsonic client authenticates each call,
5//! so a successful check is remembered for a while. The key is a digest of the
6//! username, the password and the stored hash, so changing the password or
7//! deleting the user ends it; the role is read afresh every time.
8//!
9//! A check that misses the cache runs argon2, which costs ~19 MiB and a core
10//! for tens of milliseconds, and anyone may ask for one — an unknown username
11//! still pays, so response time does not say which exist. So only a few run at
12//! once, and a request that finds them all busy is refused rather than queued.
13//! Requests carrying the same credentials while one is being checked wait for
14//! that check instead, which is what a client's burst of requests on first
15//! contact looks like.
16//!
17//! Subsonic token auth (`t`/`s`) is not checked here: it needs the plaintext
18//! password, and koan keeps only the hash.
19
20use 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
34/// How long a request waits on another's check of the same credentials.
35const WAIT_FOR_CHECK: Duration = Duration::from_secs(5);
36
37/// argon2 checks allowed at once.
38fn max_checks() -> usize {
39    std::thread::available_parallelism().map_or(2, |n| n.get().clamp(2, 8))
40}
41
42/// Why a password was not accepted.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Refused {
45    /// Not this account's password, or no such account.
46    Wrong,
47    /// Every argon2 slot was taken; nothing was checked.
48    Busy,
49}
50
51/// A check in progress: its outcome once known, and a signal for those waiting.
52#[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    /// Checks running now, by the same key as `verified`.
62    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    /// The user's id and role when the password is theirs.
77    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        // An unknown username is checked against the dummy hash, so response
82        // time doesn't say which usernames exist.
83        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    /// Run argon2 for `key`, or wait on the check already running for it.
107    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        // Remembered, and still answered from the database's role.
156        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        // A remembered sign-in needs no check, so it still works.
175        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}