securerand/
lib.rs

1use anyhow::anyhow;
2use anyhow::Context;
3use anyhow::Error;
4use anyhow::Result;
5
6// getrandom is a wrapper that provides entropy to the crate. The current implementation uses
7// getrandom::getrandom on every call, but the plan is to update this in the future so that
8// randomness is fetched once at init and then securely mutated to improve both performance and
9// also protect against weak/compromised rng from the operating system.
10fn getrandom(dest: &mut [u8]) -> Result<()> {
11    getrandom::getrandom(dest).context("unable to get entropy from the OS")
12}
13
14// rand_u64 will return a u64 that is in the range [lower_bound, upper_bound].
15pub fn rand_u64(lower_bound: u64, upper_bound: u64) -> Result<u64, Error> {
16    // Perform bounds checking.
17    if lower_bound > upper_bound {
18        return Err(anyhow!("upper_bound should be >= lower_bound"));
19    }
20
21    // Grab some random numbers.
22    let mut rand_buf = [0u8; 32];
23    getrandom(&mut rand_buf).context("unable to call getrandom for seed entropy")?;
24
25    // Convert the numbers to a u64.
26    let mut full_range = 0u64;
27    for i in 0..rand_buf.len() - 1 {
28        if i > 0 {
29            full_range = full_range.wrapping_mul(255);
30        }
31        full_range = full_range.wrapping_add(rand_buf[i] as u64);
32    }
33
34    // Mod the u64 into range.
35    let range = upper_bound - lower_bound;
36    let ranged_rand = full_range % (range + 1);
37    return Ok(ranged_rand + lower_bound);
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    // Ensure the bounds checking is working.
45    #[test]
46    fn catch_bad_bounds() {
47        let t0 = match rand_u64(0, 0) {
48            Ok(rand) => rand,
49            Err(error) => panic!("{}", error),
50        };
51        assert_eq!(t0, 0);
52        match rand_u64(1, 0) {
53            Ok(rand) => panic!("{}", rand),
54            Err(error) => {
55                let err_str = format!("{}", error);
56                assert_eq!(err_str, "upper_bound should be >= lower_bound")
57            }
58        };
59    }
60
61    // Check for simple off-by-one errors.
62    #[test]
63    fn catch_off_by_one() {
64        // Try range [1,1]
65        let mut one = false;
66        for _ in 0..1000 {
67            let result = match rand_u64(1, 1) {
68                Ok(result) => result,
69                Err(error) => {
70                    panic!("{}", error);
71                }
72            };
73            if result == 1 {
74                one = true
75            } else {
76                panic!("{}", result);
77            }
78        }
79        if !one {
80            panic!("one was not hit");
81        }
82
83        // Try range [0,1]
84        let mut zero = false;
85        let mut one = false;
86        for _ in 0..1000 {
87            let result = match rand_u64(0, 1) {
88                Ok(result) => result,
89                Err(error) => {
90                    panic!("{}", error);
91                }
92            };
93            if result == 0 {
94                zero = true
95            } else if result == 1 {
96                one = true
97            } else {
98                panic!("{}", result);
99            }
100        }
101        if !zero {
102            panic!("zero was not hit");
103        }
104        if !one {
105            panic!("one was not hit");
106        }
107
108        // Try range [1,2]
109        let mut one = false;
110        let mut two = false;
111        for _ in 0..1000 {
112            let result = match rand_u64(1, 2) {
113                Ok(result) => result,
114                Err(error) => {
115                    panic!("{}", error);
116                }
117            };
118            if result == 1 {
119                one = true
120            } else if result == 2 {
121                two = true
122            } else {
123                panic!("{}", result);
124            }
125        }
126        if !one {
127            panic!("one was not hit");
128        }
129        if !two {
130            panic!("two was not hit");
131        }
132
133        // Try range [0,2]
134        let mut zero = false;
135        let mut one = false;
136        let mut two = false;
137        for _ in 0..1000 {
138            let result = match rand_u64(0, 2) {
139                Ok(result) => result,
140                Err(error) => {
141                    panic!("{}", error);
142                }
143            };
144            if result == 0 {
145                zero = true
146            } else if result == 1 {
147                one = true
148            } else if result == 2 {
149                two = true
150            } else {
151                panic!("{}", result);
152            }
153        }
154        if !zero {
155            panic!("zero was not hit");
156        }
157        if !one {
158            panic!("one was not hit");
159        }
160        if !two {
161            panic!("two was not hit");
162        }
163    }
164
165    // Perform a statistical test to make sure numbers are distributing evenly. This isn't a
166    // cryptographic test, it's a test  to look for more basic mistakes.
167    #[test]
168    fn check_rand_distribution() {
169        let mut one = 0u64;
170        let mut two = 0u64;
171        let mut three = 0u64;
172        for _ in 0..1_000_000 {
173            match rand_u64(1,3) {
174                Ok(result) => {
175                    if result == 1 {
176                        one += 1;
177                    } else if result == 2 {
178                        two += 1;
179                    } else if result == 3 {
180                        three += 1;
181                    } else {
182                        panic!("{}", result);
183                    }
184                },
185                Err(error) => {
186                    panic!("{}", error);
187                }
188            }
189        }
190
191        // Check that the distribution is good. A typical spread will put more than 332_000 into
192        // each bucket, it's extremely unlikely that you'd ever see a number get 320_000 or less by
193        // chance.
194        if one < 320_000 || two < 320_000 || three < 320_000 {
195            panic!("{} - {} - {}", one, two, three);
196        }
197    }
198}