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}