Skip to main content

rustlavel_auth/
random.rs

1//! Random bytes, read straight from the operating system.
2//!
3//! Session ids, CSRF tokens, encryption nonces and application keys all come
4//! from here. Unlike a protocol nonce, most of these values are secrets: an
5//! attacker who can predict a session id is logged in as somebody else. So the
6//! only source we trust is the OS CSPRNG, and a system that cannot provide one
7//! gets a loud error rather than a quiet downgrade.
8
9use std::io::Read;
10
11/// Fill a buffer with `length` bytes from the operating system's CSPRNG.
12///
13/// Reading `/dev/urandom` directly keeps the dependency list honest — it is
14/// the same syscall a `rand` crate would make. If it is unreachable (a
15/// chroot without `/dev`, a platform without it) the caller still gets bytes,
16/// but they are only unique, not unpredictable, and the process says so.
17pub fn bytes(length: usize) -> Vec<u8> {
18    let mut buffer = vec![0u8; length];
19
20    if let Ok(mut source) = std::fs::File::open("/dev/urandom")
21        && source.read_exact(&mut buffer).is_ok()
22    {
23        return buffer;
24    }
25
26    rustlavel_core::error!(
27        "could not read /dev/urandom: session ids and tokens are being generated from a \
28         clock-seeded fallback. Do not run this configuration in production."
29    );
30    fallback(&mut buffer);
31    buffer
32}
33
34/// A clock- and address-seeded mixer, used only when the OS source is gone.
35fn fallback(buffer: &mut [u8]) {
36    let now = std::time::SystemTime::now()
37        .duration_since(std::time::UNIX_EPOCH)
38        .map(|d| d.as_nanos() as u64)
39        .unwrap_or(0);
40    let mut state = now ^ (Box::into_raw(Box::new(0u8)) as u64) | 1;
41
42    for slot in buffer.iter_mut() {
43        state ^= state << 13;
44        state ^= state >> 7;
45        state ^= state << 17;
46        *slot = (state >> 24) as u8;
47    }
48}
49
50/// `length` random bytes rendered as lowercase hex.
51///
52/// Hex rather than an alphabet lookup because folding random bytes into a
53/// 62-character alphabet with `%` biases the result; hex is a bijection, so a
54/// 32-byte id really carries 256 bits.
55pub fn hex(length: usize) -> String {
56    let mut out = String::with_capacity(length * 2);
57    for byte in bytes(length) {
58        out.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
59        out.push(char::from_digit((byte & 0x0f) as u32, 16).unwrap_or('0'));
60    }
61    out
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn returns_the_requested_number_of_bytes() {
70        assert_eq!(bytes(32).len(), 32);
71        assert_eq!(bytes(0).len(), 0);
72    }
73
74    #[test]
75    fn hex_output_is_twice_as_long_and_lowercase_hexadecimal() {
76        let value = hex(32);
77
78        assert_eq!(value.len(), 64);
79        assert!(value.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
80    }
81
82    #[test]
83    fn two_draws_of_the_same_length_differ() {
84        assert_ne!(hex(32), hex(32));
85    }
86
87    #[test]
88    fn the_fallback_mixer_still_fills_every_byte() {
89        // Even degraded, the buffer must not be left as zeroes: a session id of
90        // all zeroes would be shared by every visitor.
91        let mut buffer = vec![0u8; 64];
92        fallback(&mut buffer);
93        assert!(buffer.iter().any(|byte| *byte != 0));
94    }
95}