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, so the caller-facing copy
24/// never sits in ordinary heap memory. Note that any downstream code
25/// that clones the value into a plain `String` (e.g. the rmpv-encoded
26/// `Action::Encrypt` payload sent over the agent socket) reintroduces
27/// that exposure — this function only eliminates it in the immediate
28/// caller's scope.
29pub fn pwgen(ty: Type, len: usize) -> locked::Password {
30    let mut rng = rand::rng();
31
32    let alphabet = match ty {
33        Type::AllChars => {
34            let mut v = vec![];
35            v.extend(SYMBOLS.iter().copied());
36            v.extend(NUMBERS.iter().copied());
37            v.extend(LETTERS.iter().copied());
38            v
39        }
40        Type::NoSymbols => {
41            let mut v = vec![];
42            v.extend(NUMBERS.iter().copied());
43            v.extend(LETTERS.iter().copied());
44            v
45        }
46        Type::Numbers => {
47            let mut v = vec![];
48            v.extend(NUMBERS.iter().copied());
49            v
50        }
51        Type::NonConfusables => {
52            let mut v = vec![];
53            v.extend(NONCONFUSABLES.iter().copied());
54            v
55        }
56        Type::Diceware => {
57            return diceware(&mut rng, len);
58        }
59    };
60
61    let mut buf = locked::Vec::new();
62    buf.extend(
63        std::iter::repeat_with(|| *alphabet.iter().choose(&mut rng).unwrap())
64            .take(len),
65    );
66    locked::Password::new(buf)
67}
68
69fn diceware(rng: &mut impl rand::RngCore, len: usize) -> locked::Password {
70    let mut words = vec![];
71    for _ in 0..len {
72        // unwrap is safe because choose only returns None for an empty slice
73        words.push(*crate::wordlist::EFF_LONG.iter().choose(rng).unwrap());
74    }
75    let mut joined = words.join(" ");
76    let mut buf = locked::Vec::new();
77    buf.extend(joined.as_bytes().iter().copied());
78    // The intermediate `String` held the full passphrase in plain heap;
79    // scrub it before it drops.
80    joined.zeroize();
81    locked::Password::new(buf)
82}
83
84#[cfg(test)]
85mod test {
86    use super::*;
87
88    #[test]
89    fn test_pwgen() {
90        let pw = pwgen(Type::AllChars, 50);
91        assert_eq!(pw.password().len(), 50);
92        // technically this could fail, but the chances are incredibly low
93        // (around 0.000009%)
94        assert_duplicates(pw.password());
95
96        let pw = pwgen(Type::AllChars, 100);
97        assert_eq!(pw.password().len(), 100);
98        assert_duplicates(pw.password());
99
100        let pw = pwgen(Type::NoSymbols, 100);
101        assert_eq!(pw.password().len(), 100);
102        assert_duplicates(pw.password());
103
104        let pw = pwgen(Type::Numbers, 100);
105        assert_eq!(pw.password().len(), 100);
106        assert_duplicates(pw.password());
107
108        let pw = pwgen(Type::NonConfusables, 100);
109        assert_eq!(pw.password().len(), 100);
110        assert_duplicates(pw.password());
111    }
112
113    #[track_caller]
114    fn assert_duplicates(bytes: &[u8]) {
115        let s = std::str::from_utf8(bytes).unwrap();
116        let mut set = std::collections::HashSet::new();
117        for c in s.chars() {
118            set.insert(c);
119        }
120        assert!(set.len() < s.len());
121    }
122}