OXYGENHUB

Coverage / crates/hub/src/throttle.rs 97.4%

111 of 114 lines run, 28 of 30 functions · run #538 on main · run missed

1
//! Slowing down guessing (oxygenhub#221): password sign-in, the 2FA code
2
//! step, password reset requests and device codes.
3
//!
4
//! Each key (an account or an address, within one action) counts its
5
//! failures. A few are free; after that each one doubles the wait before the
6
//! next try, up to a short lockout. A quiet hour forgets the count, and a
7
//! success clears the account's. Kept in memory: a restart forgets it, which
8
//! costs an attacker a restart they cannot cause.
9
10
use std::collections::HashMap;
11
12
/// Failures an account may have before waits start.
13
const ACCOUNT_FREE: u32 = 5;
14
/// Failures an address may have before waits start: higher, since many
15
/// people can share one address.
16
const ADDRESS_FREE: u32 = 20;
17
/// The first wait.
18
const FIRST_WAIT_MS: i64 = 2_000;
19
/// The longest wait: the lockout.
20
pub const LOCKOUT_MS: i64 = 15 * 60_000;
21
/// A key with no failure for this long starts again.
22
const FORGET_MS: i64 = 60 * 60_000;
23
/// Keys kept before stale ones are swept.
24
const SWEEP_AT: usize = 50_000;
25
26
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
27
pub enum Kind {
28
    Account,
29
    Address,
30
}
31
32
/// What is counted: an account or an address, within one action.
33
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
34
pub struct Key {
35
    action: &'static str,
36
    kind: Kind,
37
    id: String,
38
}
39
40
impl Key {
41
    /// An account, by name; names compare without case.
4257×
    pub fn account(action: &'static str, name: &str) -> Key {
4357×
        Key { action, kind: Kind::Account, id: name.trim().to_lowercase() }
4457×
    }
45
46
    /// An address. An empty one (unknown) is never counted.
4757×
    pub fn address(action: &'static str, addr: &str) -> Key {
4857×
        Key { action, kind: Kind::Address, id: addr.to_string() }
4957×
    }
50
51162×
    fn free(&self) -> u32 {
52162×
        match self.kind {
53112×
            Kind::Account => ACCOUNT_FREE,
5450×
            Kind::Address => ADDRESS_FREE,
55
        }
56162×
    }
57
}
58
59
#[derive(Default)]
60
struct Count {
61
    failures: u32,
62
    last_ms: i64,
63
    until_ms: i64,
64
}
65
66
#[derive(Default)]
67
pub struct Throttle {
68
    counts: HashMap<Key, Count>,
69
}
70
71
impl Throttle {
72
    /// How long, in milliseconds, before any of these keys may try again;
73
    /// `None` when all may try now.
74279×
    pub fn wait(&self, keys: &[Key], now_ms: i64) -> Option<i64> {
75279×
        keys.iter()
76360×
            .filter_map(|k| self.counts.get(k))
77279×
            .filter(|c| now_ms - c.last_ms < FORGET_MS)
78279×
            .map(|c| c.until_ms - now_ms)
79279×
            .filter(|w| *w > 0)
80279×
            .max()
81279×
    }
82
83
    /// Count a failure (or, for an action with no failure, an attempt)
84
    /// against each key; the wait it leaves, if any.
85204×
    pub fn failed(&mut self, keys: &[Key], now_ms: i64) -> Option<i64> {
86204×
        if self.counts.len() >= SWEEP_AT {
870×
            self.counts.retain(|_, c| now_ms - c.last_ms < FORGET_MS);
88204×
        }
89234×
        for key in keys.iter().filter(|k| !k.id.is_empty()) {
90127×
            let count = self.counts.entry(key.clone()).or_default();
91127×
            if now_ms - count.last_ms >= FORGET_MS {
9233×
                *count = Count::default();
9394×
            }
94127×
            count.failures += 1;
95127×
            count.last_ms = now_ms;
96127×
            if count.failures > key.free() {
9735×
                let doublings = (count.failures - key.free() - 1).min(20);
9835×
                count.until_ms = now_ms + (FIRST_WAIT_MS << doublings).min(LOCKOUT_MS);
9992×
            }
100
        }
101204×
        self.wait(keys, now_ms)
102204×
    }
103
104
    /// A success: the key starts again.
10511×
    pub fn cleared(&mut self, key: &Key) {
10611×
        self.counts.remove(key);
10711×
    }
108
}
109
110
/// "Too many tries: try again in 2 minutes." for a wait.
1116×
pub fn message(wait_ms: i64) -> String {
1126×
    let secs = (wait_ms + 999) / 1000;
1136×
    let when = match secs {
1146×
        ..=1 => "a second".to_string(),
1155×
        2..=59 => format!("{secs} seconds"),
116
        _ => {
1172×
            let mins = (secs + 59) / 60;
1182×
            if mins == 1 {
1190×
                "a minute".to_string()
120
            } else {
1212×
                format!("{mins} minutes")
122
            }
123
        }
124
    };
1256×
    format!("Too many tries: try again in {when}.")
1266×
}
127
128
#[cfg(test)]
129
mod tests {
130
    use super::*;
131
132
    const T0: i64 = 1_000_000_000;
133
134
    #[test]
1351×
    fn an_account_gets_a_few_free_failures_then_doubling_waits_up_to_the_lockout() {
1361×
        let mut t = Throttle::default();
1371×
        let k = [Key::account("login", "Alice")];
1381×
        for _ in 0..ACCOUNT_FREE {
1395×
            assert_eq!(t.wait(&k, T0), None);
1405×
            t.failed(&k, T0);
141
        }
1421×
        assert_eq!(t.wait(&k, T0), None, "the free failures cost nothing");
1431×
        assert_eq!(t.failed(&k, T0), Some(2_000), "the failure says the wait it starts");
1441×
        t.failed(&k, T0);
1451×
        assert_eq!(t.wait(&k, T0), Some(4_000));
14620×
        for _ in 0..20 {
14720×
            t.failed(&k, T0);
14820×
        }
1491×
        assert_eq!(t.wait(&k, T0), Some(LOCKOUT_MS), "capped at the lockout");
1501×
        assert_eq!(t.wait(&k, T0 + LOCKOUT_MS), None, "and it ends");
1511×
        assert_eq!(t.wait(&[Key::account("login", " alice ")], T0), Some(LOCKOUT_MS), "names compare without case");
1521×
    }
153
154
    #[test]
1551×
    fn a_success_clears_the_account_and_a_quiet_hour_forgets() {
1561×
        let mut t = Throttle::default();
1571×
        let k = [Key::account("login", "bob")];
15810×
        for _ in 0..10 {
15910×
            t.failed(&k, T0);
16010×
        }
1611×
        assert!(t.wait(&k, T0).is_some());
1621×
        t.cleared(&k[0]);
1631×
        assert_eq!(t.wait(&k, T0), None);
164
16510×
        for _ in 0..10 {
16610×
            t.failed(&k, T0);
16710×
        }
1681×
        let later = T0 + FORGET_MS;
1691×
        assert_eq!(t.wait(&k, later), None);
1701×
        t.failed(&k, later);
1711×
        assert_eq!(t.wait(&k, later), None, "the count started again");
1721×
    }
173
174
    #[test]
1751×
    fn addresses_get_more_room_and_actions_and_unknown_addresses_are_apart() {
1761×
        let mut t = Throttle::default();
1771×
        let addr = [Key::address("login", "203.0.113.9")];
17820×
        for _ in 0..ADDRESS_FREE {
17920×
            t.failed(&addr, T0);
18020×
        }
1811×
        assert_eq!(t.wait(&addr, T0), None);
1821×
        t.failed(&addr, T0);
1831×
        assert_eq!(t.wait(&addr, T0), Some(2_000));
1841×
        assert_eq!(t.wait(&[Key::address("reset", "203.0.113.9")], T0), None, "another action counts apart");
185
1861×
        let unknown = [Key::address("login", "")];
187100×
        for _ in 0..100 {
188100×
            t.failed(&unknown, T0);
189100×
        }
1901×
        assert_eq!(t.wait(&unknown, T0), None, "an unknown address is never counted");
191
192
        // The longest wait of the keys given is the one that counts.
1931×
        let both = [Key::account("login", "carol"), addr[0].clone()];
1941×
        assert_eq!(t.wait(&both, T0), Some(2_000));
1951×
    }
196
197
    #[test]
1981×
    fn the_message_says_when() {
1991×
        assert_eq!(message(900), "Too many tries: try again in a second.");
2001×
        assert_eq!(message(4_000), "Too many tries: try again in 4 seconds.");
2011×
        assert_eq!(message(61_000), "Too many tries: try again in 2 minutes.");
2021×
        assert_eq!(message(LOCKOUT_MS), "Too many tries: try again in 15 minutes.");
2031×
    }
204
}