Skip to main content

bwx/
pwgen.rs

1use rand::seq::IteratorRandom as _;
2use zeroize::Zeroize as _;
3
4use crate::locked;
5
6const SYMBOLS: &[u8] = b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
7const NUMBERS: &[u8] = b"0123456789";
8const LETTERS: &[u8] =
9    b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
10const NONCONFUSABLES: &[u8] = b"34678abcdefhjkmnpqrtuwxy";
11
12#[derive(Debug, Eq, PartialEq, Copy, Clone)]
13pub enum Type {
14    AllChars,
15    NoSymbols,
16    Numbers,
17    NonConfusables,
18    Diceware,
19}
20
21/// Generate a password into a `locked::Password`.
22///
23/// The result is mlocked + zeroized on drop. Downstream code that clones
24/// the value into a plain `String` (e.g. the rmpv-encoded `Action::Encrypt`
25/// payload sent over the agent socket) reintroduces heap exposure; this
26/// function only eliminates it in the immediate caller's scope.
27pub fn pwgen(ty: Type, len: usize) -> locked::Password {
28    let mut rng = rand::rng();
29
30    let alphabet = match ty {
31        Type::AllChars => {
32            let mut v = vec![];
33            v.extend(SYMBOLS.iter().copied());
34            v.extend(NUMBERS.iter().copied());
35            v.extend(LETTERS.iter().copied());
36            v
37        }
38        Type::NoSymbols => {
39            let mut v = vec![];
40            v.extend(NUMBERS.iter().copied());
41            v.extend(LETTERS.iter().copied());
42            v
43        }
44        Type::Numbers => {
45            let mut v = vec![];
46            v.extend(NUMBERS.iter().copied());
47            v
48        }
49        Type::NonConfusables => {
50            let mut v = vec![];
51            v.extend(NONCONFUSABLES.iter().copied());
52            v
53        }
54        Type::Diceware => {
55            return diceware(&mut rng, len);
56        }
57    };
58
59    let mut buf = locked::Vec::new();
60    buf.extend(
61        std::iter::repeat_with(|| *alphabet.iter().choose(&mut rng).unwrap())
62            .take(len),
63    );
64    locked::Password::new(buf)
65}
66
67fn diceware(rng: &mut impl rand::RngCore, len: usize) -> locked::Password {
68    let mut words = vec![];
69    for _ in 0..len {
70        // unwrap is safe because choose only returns None for an empty slice
71        words.push(*crate::wordlist::EFF_LONG.iter().choose(rng).unwrap());
72    }
73    let mut joined = words.join(" ");
74    let mut buf = locked::Vec::new();
75    buf.extend(joined.as_bytes().iter().copied());
76    // The intermediate `String` held the full passphrase in plain heap;
77    // scrub before drop.
78    joined.zeroize();
79    locked::Password::new(buf)
80}
81
82#[cfg(test)]
83mod test {
84    use super::*;
85
86    #[test]
87    fn test_pwgen() {
88        let pw = pwgen(Type::AllChars, 50);
89        assert_eq!(pw.password().len(), 50);
90        // technically this could fail, but the chances are incredibly low
91        // (around 0.000009%)
92        assert_duplicates(pw.password());
93
94        let pw = pwgen(Type::AllChars, 100);
95        assert_eq!(pw.password().len(), 100);
96        assert_duplicates(pw.password());
97
98        let pw = pwgen(Type::NoSymbols, 100);
99        assert_eq!(pw.password().len(), 100);
100        assert_duplicates(pw.password());
101
102        let pw = pwgen(Type::Numbers, 100);
103        assert_eq!(pw.password().len(), 100);
104        assert_duplicates(pw.password());
105
106        let pw = pwgen(Type::NonConfusables, 100);
107        assert_eq!(pw.password().len(), 100);
108        assert_duplicates(pw.password());
109    }
110
111    #[track_caller]
112    fn assert_duplicates(bytes: &[u8]) {
113        let s = std::str::from_utf8(bytes).unwrap();
114        let mut set = std::collections::HashSet::new();
115        for c in s.chars() {
116            set.insert(c);
117        }
118        assert!(set.len() < s.len());
119    }
120}