Initial commit - v0.1.0 - it works!
This commit is contained in:
commit
a83b936658
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
|
@ -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 = []
|
|
@ -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");
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
|
@ -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…
Reference in New Issue