1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
extern crate rand;

mod utils;

use rand::Rng;

#[cfg(feature="built_in_dicts")]
/// The dictionary to be used for password generation.
///
/// This is also a list of supported dictionaries.
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Dictionary {
    en_US,
    //pt_BR,
}

#[cfg(feature="built_in_dicts")]
impl Dictionary {
    fn to_raw_dict(self) -> &'static str {
        match self {
            Dictionary::en_US => include_str!(concat!(env!("OUT_DIR"), "/dictionary/en_US")),
            //Dictionary::pt_BR => include_str!(concat!(env!("OUT_DIR"), "/dictionary/pt_BR")),
        }
    }
}

/// Attempts to generate an xkcd-password given a words count and a custom dict.
///
/// Warning: This function doesn't protect against side-channel attacks.
///
/// This function doesn't check for duplicate words in the dict.
///
/// # Panics
///
/// Panics if dict is in an invalid format.
///
/// Panics if you ask for too many words.
///
/// # Examples
/// 
/// ```
/// let res = xkcd_password::generate_password_custom_dict(4, &["Ia", "weanmeheno", "theshehimheryes"]);
/// // res is made up of the words "I" "a" "we" "an" "me" "he" "no" "the" "she" "him" "her" "yes"
/// ```
pub fn generate_password_custom_dict(word_count: usize, dict: &[&str]) -> String {
    // avoid using unnecessary memory in the next steps.
    let dict = utils::trim_right_by(dict, |e| e.is_empty());
    // index 0 = words of length 1, index 1 = words of length 2, etc. give us capacity for
    // word_count * max_word_length + spaces.
    let capacity = word_count * dict.len() + word_count;
    // calculate how many words for each length.
    let n_of_words: Vec<usize> = dict.iter().enumerate().map(|x| x.1.len() / (x.0 + 1)).collect(); 
    // and how many words in total
    let total_n_of_words = n_of_words.iter().sum();
    // make sure all lengths are correct.
    if dict.iter().zip(n_of_words.iter()).enumerate().any(|(wlen, (words, count))| (wlen+1)*count != words.len()) {
        panic!("Error in password generation: Dictionary invalid.");
    }
    // prepare target.
    let mut s = String::with_capacity(capacity);
    // select words.
    for word_n in (0..word_count).map(|_| rand::thread_rng().gen_range(0, total_n_of_words)) {
        let mut found: Option<&str> = None;
        let mut real_pos = 0;
        // iterate the length of the words, the amount of the words, and the words themselves.
        for (wlen, (n_words, words)) in n_of_words.iter().zip(dict.iter()).enumerate() {
            // fix up wlen
            let wlen = wlen + 1;
            // figure out where we should be in the full dict.
            real_pos += n_words;
            if word_n < real_pos {
                // figure out where we should be in the dict of `wlen`-sized words.
                let base_pos = real_pos - n_words;
                let inner_pos = word_n - base_pos;
                let inner_index = inner_pos * wlen;
                // extract word from dict.
                found = Some(&words[inner_index..inner_index+wlen]);
                break;
            }
        }
        s.push_str(found.expect("bug in xkcd-password"));
        s.push(' ');
    }
    s
}

/// Attempts to generate an xkcd-password given a words count and a custom dict.
///
/// Warning: This function doesn't protect against side-channel attacks.
///
/// # Panics
///
/// Panics if any words contain space or newline characters, or if it appears more than once.
///
/// Panics if you ask for too many words.
// TODO this function is slow if you need to call it many times.
pub fn generate_password_from_words<'a, I: IntoIterator<Item=&'a str>>(word_count: usize, dict: I) -> String {
    let iter = dict.into_iter();
    let mut dict: Vec<String> = vec![];
    for word in iter {
        if word.contains(' ') || word.contains('\n') {
            panic!("Word contains space or newline characters");
        }
        if dict.len() < word.len() {
            dict.resize(word.len(), String::default());
        }
        if dict[word.len() - 1].as_bytes().chunks(word.len()).any(|chunk| chunk == word.as_bytes()) {
            panic!("Duplicate word in dict");
        }
        dict[word.len() - 1].push_str(word);
    }
    // not much point in collecting it into a single String, the overhead here is rather small.
    let dict: Vec<&str> = dict.iter().map(String::as_str).collect();
    generate_password_custom_dict(word_count, &dict)
}

#[cfg(feature="built_in_dicts")]
/// Generates an xkcd-password with `word_count` words from built-in dictionary `dict`.
///
/// Warning: This function doesn't protect against side-channel attacks.
///
/// # Panics
///
/// Panics if you ask for too many words.
pub fn generate_password(word_count: usize, dict: Dictionary) -> String {
    let v: Vec<&str> = dict.to_raw_dict().lines().collect();
    generate_password_custom_dict(word_count, &v)
}