xkcd_password/
lib.rs

1extern crate rand;
2
3mod utils;
4
5use rand::Rng;
6
7#[cfg(feature="built_in_dicts")]
8/// The dictionary to be used for password generation.
9///
10/// This is also a list of supported dictionaries.
11#[allow(non_camel_case_types)]
12#[derive(Copy, Clone, Eq, PartialEq, Hash)]
13pub enum Dictionary {
14    en_US,
15    //pt_BR,
16}
17
18#[cfg(feature="built_in_dicts")]
19impl Dictionary {
20    fn to_raw_dict(self) -> &'static str {
21        match self {
22            Dictionary::en_US => include_str!(concat!(env!("OUT_DIR"), "/dictionary/en_US")),
23            //Dictionary::pt_BR => include_str!(concat!(env!("OUT_DIR"), "/dictionary/pt_BR")),
24        }
25    }
26}
27
28/// Attempts to generate an xkcd-password given a words count and a custom dict.
29///
30/// Warning: This function doesn't protect against side-channel attacks.
31///
32/// This function doesn't check for duplicate words in the dict.
33///
34/// # Panics
35///
36/// Panics if dict is in an invalid format.
37///
38/// Panics if you ask for too many words.
39///
40/// # Examples
41/// 
42/// ```
43/// let res = xkcd_password::generate_password_custom_dict(4, &["Ia", "weanmeheno", "theshehimheryes"]);
44/// // res is made up of the words "I" "a" "we" "an" "me" "he" "no" "the" "she" "him" "her" "yes"
45/// ```
46pub fn generate_password_custom_dict(word_count: usize, dict: &[&str]) -> String {
47    // avoid using unnecessary memory in the next steps.
48    let dict = utils::trim_right_by(dict, |e| e.is_empty());
49    // index 0 = words of length 1, index 1 = words of length 2, etc. give us capacity for
50    // word_count * max_word_length + spaces.
51    let capacity = word_count * dict.len() + word_count;
52    // calculate how many words for each length.
53    let n_of_words: Vec<usize> = dict.iter().enumerate().map(|x| x.1.len() / (x.0 + 1)).collect(); 
54    // and how many words in total
55    let total_n_of_words = n_of_words.iter().sum();
56    // make sure all lengths are correct.
57    if dict.iter().zip(n_of_words.iter()).enumerate().any(|(wlen, (words, count))| (wlen+1)*count != words.len()) {
58        panic!("Error in password generation: Dictionary invalid.");
59    }
60    // prepare target.
61    let mut s = String::with_capacity(capacity);
62    // select words.
63    for word_n in (0..word_count).map(|_| rand::thread_rng().gen_range(0, total_n_of_words)) {
64        let mut found: Option<&str> = None;
65        let mut real_pos = 0;
66        // iterate the length of the words, the amount of the words, and the words themselves.
67        for (wlen, (n_words, words)) in n_of_words.iter().zip(dict.iter()).enumerate() {
68            // fix up wlen
69            let wlen = wlen + 1;
70            // figure out where we should be in the full dict.
71            real_pos += n_words;
72            if word_n < real_pos {
73                // figure out where we should be in the dict of `wlen`-sized words.
74                let base_pos = real_pos - n_words;
75                let inner_pos = word_n - base_pos;
76                let inner_index = inner_pos * wlen;
77                // extract word from dict.
78                found = Some(&words[inner_index..inner_index+wlen]);
79                break;
80            }
81        }
82        s.push_str(found.expect("bug in xkcd-password"));
83        s.push(' ');
84    }
85    s
86}
87
88/// Attempts to generate an xkcd-password given a words count and a custom dict.
89///
90/// Warning: This function doesn't protect against side-channel attacks.
91///
92/// # Panics
93///
94/// Panics if any words contain space or newline characters, or if it appears more than once.
95///
96/// Panics if you ask for too many words.
97// TODO this function is slow if you need to call it many times.
98pub fn generate_password_from_words<'a, I: IntoIterator<Item=&'a str>>(word_count: usize, dict: I) -> String {
99    let iter = dict.into_iter();
100    let mut dict: Vec<String> = vec![];
101    for word in iter {
102        if word.contains(' ') || word.contains('\n') {
103            panic!("Word contains space or newline characters");
104        }
105        if dict.len() < word.len() {
106            dict.resize(word.len(), String::default());
107        }
108        if dict[word.len() - 1].as_bytes().chunks(word.len()).any(|chunk| chunk == word.as_bytes()) {
109            panic!("Duplicate word in dict");
110        }
111        dict[word.len() - 1].push_str(word);
112    }
113    // not much point in collecting it into a single String, the overhead here is rather small.
114    let dict: Vec<&str> = dict.iter().map(String::as_str).collect();
115    generate_password_custom_dict(word_count, &dict)
116}
117
118#[cfg(feature="built_in_dicts")]
119/// Generates an xkcd-password with `word_count` words from built-in dictionary `dict`.
120///
121/// Warning: This function doesn't protect against side-channel attacks.
122///
123/// # Panics
124///
125/// Panics if you ask for too many words.
126pub fn generate_password(word_count: usize, dict: Dictionary) -> String {
127    let v: Vec<&str> = dict.to_raw_dict().lines().collect();
128    generate_password_custom_dict(word_count, &v)
129}