Initial commit - v0.1.0 - it works!
This commit is contained in:
		
						commit
						a83b936658
					
				
					 6 changed files with 7971 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
/target
 | 
			
		||||
**/*.rs.bk
 | 
			
		||||
Cargo.lock
 | 
			
		||||
							
								
								
									
										13
									
								
								Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "xkcd-password"
 | 
			
		||||
description = "Generate passwords"
 | 
			
		||||
license = "AGPL-3.0-or-later"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
authors = ["SoniEx2 <endermoneymod@gmail.com>"]
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
rand = "0.5.5"
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
default = ["built_in_dicts"]
 | 
			
		||||
built_in_dicts = []
 | 
			
		||||
							
								
								
									
										38
									
								
								build.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								build.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
use std::fs;
 | 
			
		||||
use std::env;
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
 | 
			
		||||
fn convert(filename: &str) {
 | 
			
		||||
    println!("Processing: dictionary/{}", filename);
 | 
			
		||||
    let out_dir = env::var("OUT_DIR").unwrap();
 | 
			
		||||
    let in_data = fs::read_to_string(format!("dictionary/{}", filename)).expect("Failed to read dictionary");
 | 
			
		||||
    let mut out_dict: Vec<HashSet<&str>> = Vec::new();
 | 
			
		||||
    for line in in_data.lines() {
 | 
			
		||||
        for (pos, line_part) in line.split_whitespace().enumerate() {
 | 
			
		||||
            if line_part.is_empty() || line_part.starts_with('#') {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            if pos != 0 {
 | 
			
		||||
                panic!("More than one word per line");
 | 
			
		||||
            }
 | 
			
		||||
            if out_dict.len() < line_part.len() {
 | 
			
		||||
                let count = line_part.len() - out_dict.len();
 | 
			
		||||
                out_dict.reserve(count);
 | 
			
		||||
                while out_dict.len() < line_part.len() {
 | 
			
		||||
                    out_dict.push(HashSet::default());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if !out_dict[line_part.len() - 1].insert(line_part) {
 | 
			
		||||
                panic!("Duplicate word {}", line_part);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let out_data: String = out_dict.into_iter().flat_map(|x| x.into_iter().chain(std::iter::once("\n"))).collect();
 | 
			
		||||
    fs::write(format!("{}/dictionary/{}", out_dir, filename), out_data).expect("Failed to write converted dictionary");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    let out_dir = env::var("OUT_DIR").unwrap();
 | 
			
		||||
    fs::create_dir_all(format!("{}/dictionary/", out_dir)).expect("failed to create dictionary output dir");
 | 
			
		||||
    convert("en_US");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7778
									
								
								dictionary/en_US
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7778
									
								
								dictionary/en_US
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										129
									
								
								src/lib.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/lib.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/utils.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/utils.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
pub mod slices {
 | 
			
		||||
    pub fn trim_right_by<T, F: Fn(&T) -> bool>(slice: &[T], f: F) -> &[T] {
 | 
			
		||||
        let mut res = slice;
 | 
			
		||||
        while res.len() > 0 && f(res.last().unwrap()) {
 | 
			
		||||
            res = res.split_last().unwrap().1;
 | 
			
		||||
        }
 | 
			
		||||
        res
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
pub use self::slices::*;
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue