Initial commit - v0.1.0 - it works!

This commit is contained in:
SoniEx2 2018-09-26 00:31:03 -03:00
commit a83b936658
6 changed files with 7971 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

13
Cargo.toml Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

129
src/lib.rs Normal file
View 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
View 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::*;