use deunicode::deunicode;
use rand::{distributions::Uniform, seq::SliceRandom, thread_rng, Rng};
use regex::Regex;
use snafu::Snafu;
use std::{fs, fs::metadata, path::Path, str::FromStr};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PassConfig {
pub capitalise: bool,
pub replace: bool,
pub randomise: bool,
pub pass_amount: usize,
pub reset_amount: usize,
pub length: String,
pub number_amount: String,
pub special_chars_amount: String,
pub special_chars: String,
pub upper_amount: String,
pub lower_amount: String,
pub keep_numbers: bool,
pub force_upper: bool,
pub force_lower: bool,
pub dont_upper: bool,
pub dont_lower: bool,
words: Vec<String>,
}
impl Default for PassConfig {
fn default() -> Self {
Self {
capitalise: false,
replace: false,
randomise: false,
pass_amount: 1,
reset_amount: 10,
length: String::from("24-30"),
number_amount: String::from("1-2"),
special_chars_amount: String::from("1-2"),
special_chars: String::from("^!(-_=)$<[@.#]>%{~,+}&*"),
upper_amount: String::from("1-2"),
lower_amount: String::from("1-2"),
keep_numbers: false,
force_upper: false,
force_lower: false,
dont_upper: false,
dont_lower: false,
words: Vec::new(),
}
}
}
impl PassConfig {
pub fn new() -> Self {
PassConfig::default()
}
pub fn get_words_from_path(&mut self, path: impl AsRef<Path>) -> std::io::Result<()> {
let md = metadata(&path)?;
let mut text = String::new();
if md.is_file() {
text = fs::read_to_string(&path)?;
} else if md.is_dir() {
get_text_from_dir(&path, &mut text)?;
} else {
unreachable!("Unexpected metadata error");
}
if text.is_empty() {
return Ok(());
}
if !text.is_ascii() {
text = deunicode(&text);
}
let re;
if self.keep_numbers {
re = Regex::new(r"\w+").unwrap();
} else {
re = Regex::new(r"[^\d\W]+").unwrap();
}
for caps in re.captures_iter(&text) {
if let Some(cap) = caps.get(0) {
self.words.push(cap.as_str().to_owned());
}
}
if self.randomise {
self.words.shuffle(&mut thread_rng());
}
Ok(())
}
pub fn get_words_from_str(&mut self, text: &str) {
if text.is_empty() {
return;
}
let converted;
let ascii = match text {
ascii if ascii.is_ascii() => ascii,
utf8 => {
converted = deunicode(utf8);
&converted
}
};
let re;
if self.keep_numbers {
re = Regex::new(r"\w+").unwrap();
} else {
re = Regex::new(r"[^\d\W]+").unwrap();
}
for caps in re.captures_iter(ascii) {
if let Some(cap) = caps.get(0) {
self.words.push(cap.as_str().to_owned());
}
}
if self.randomise {
self.words.shuffle(&mut thread_rng());
}
}
pub fn get_words(&self) -> &[String] {
&self.words
}
pub fn validate(&self) -> Result<ValidatedConfig> {
let (_, _) = match process_range(&self.length) {
Ok(a) => a,
Err(e) => {
return Err(ValidationError::InvalidRange {
field: "length".to_string(),
message: e,
})
}
};
let (_, _) = match process_range(&self.number_amount) {
Ok(a) => a,
Err(e) => {
return Err(ValidationError::InvalidRange {
field: "number".to_string(),
message: e,
})
}
};
let (_, _) = match process_range(&self.special_chars_amount) {
Ok(a) => a,
Err(e) => {
return Err(ValidationError::InvalidRange {
field: "special chars".to_string(),
message: e,
})
}
};
let (_, _) = match process_range(&self.upper_amount) {
Ok(a) => a,
Err(e) => {
return Err(ValidationError::InvalidRange {
field: "uppercase".to_string(),
message: e,
})
}
};
let (_, _) = match process_range(&self.lower_amount) {
Ok(a) => a,
Err(e) => {
return Err(ValidationError::InvalidRange {
field: "lowercase".to_string(),
message: e,
})
}
};
if !self.special_chars.is_ascii() {
return Err(ValidationError::NonAsciiSpecialChars);
}
if self.words.is_empty() || self.words.len() == 1 {
return Err(ValidationError::NoWords);
}
Ok(ValidatedConfig {
capitalise: self.capitalise,
replace: self.replace,
randomise: self.randomise,
pass_amount: self.pass_amount,
reset_amount: self.reset_amount,
length: self.length.clone(),
number_amount: self.number_amount.clone(),
special_chars_amount: self.special_chars_amount.clone(),
special_chars: self.special_chars.clone(),
upper_amount: self.upper_amount.clone(),
lower_amount: self.lower_amount.clone(),
keep_numbers: self.keep_numbers,
force_upper: self.force_upper,
force_lower: self.force_lower,
dont_upper: self.dont_upper,
dont_lower: self.dont_lower,
words: self.words.clone(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidatedConfig {
capitalise: bool,
replace: bool,
randomise: bool,
pass_amount: usize,
reset_amount: usize,
length: String,
number_amount: String,
special_chars_amount: String,
special_chars: String,
upper_amount: String,
lower_amount: String,
keep_numbers: bool,
force_upper: bool,
force_lower: bool,
dont_upper: bool,
dont_lower: bool,
words: Vec<String>,
}
impl ValidatedConfig {
pub fn generate(&self) -> Vec<String> {
let mut passwords = Vec::new();
for _ in 0..self.pass_amount {
passwords.push(Password::new(&self));
}
passwords
}
}
#[derive(Debug, Snafu)]
pub enum ValidationError {
#[snafu(display("Invalid {} range: {}", field, message.message))]
InvalidRange { field: String, message: RangeError },
#[snafu(display("No words for password generation"))]
NoWords,
#[snafu(display("Non-ASCII special characters aren't allowed for insertables"))]
NonAsciiSpecialChars,
}
#[derive(Debug, Snafu)]
pub struct RangeError {
message: String,
}
type Result<T, E = ValidationError> = std::result::Result<T, E>;
type RangeResult<T, E = RangeError> = std::result::Result<T, E>;
struct Password {
password: String,
reset_count: usize,
min_len: usize,
max_len: usize,
total_inserts: usize,
upper: usize,
lower: usize,
force_upper: bool,
force_lower: bool,
insertables: Vec<char>,
}
impl Password {
fn new(config: &ValidatedConfig) -> String {
let mut pass = Password::init(&config);
pass.get_pass_string(&config);
if config.replace {
pass.replace_chars();
} else {
pass.insert_chars();
}
pass.ensure_case(&config);
pass.password
}
fn init(config: &ValidatedConfig) -> Password {
let mut rng = thread_rng();
let (mut min_len, mut max_len) = process_range(&config.length).unwrap();
if max_len - min_len > 50 {
min_len = rng.gen_range(min_len, max_len - 49);
max_len = min_len + 50;
}
let (min_num, max_num) = process_range(&config.number_amount).unwrap();
let num = rng.gen_range(min_num, max_num + 1);
let (min_special, max_special) = process_range(&config.special_chars_amount).unwrap();
let special = rng.gen_range(min_special, max_special + 1);
let (min_upper, max_upper) = process_range(&config.upper_amount).unwrap();
let upper = rng.gen_range(min_upper, max_upper + 1);
let (min_lower, max_lower) = process_range(&config.lower_amount).unwrap();
let lower = rng.gen_range(min_lower, max_lower + 1);
let mut total_inserts = num + special;
if total_inserts > max_len {
total_inserts = max_len;
}
if !config.replace {
if min_len < total_inserts {
total_inserts = min_len;
}
min_len -= total_inserts;
max_len -= total_inserts;
}
let insertables = {
let mut chars = Vec::with_capacity(total_inserts);
let num_range = Uniform::new(0, 10);
let char_range = Uniform::new(0, config.special_chars.len());
for _ in 0..num {
let num = rng.sample(&num_range).to_string().chars().next().unwrap();
chars.push(num);
}
for _ in 0..special {
let index = rng.sample(&char_range);
let c = config.special_chars.chars().nth(index);
if let Some(c) = c {
chars.push(c.clone())
}
}
chars.shuffle(&mut rng);
chars
};
Password {
password: String::with_capacity(max_len),
reset_count: 0,
min_len,
max_len,
total_inserts,
upper,
lower,
force_upper: {
if config.force_upper {
true
} else {
false
}
},
force_lower: {
if config.force_lower {
true
} else {
false
}
},
insertables,
}
}
fn get_pass_string(&mut self, config: &ValidatedConfig) {
let mut rng = thread_rng();
let start_index = rng.gen_range(0, config.words.len() - 1);
let mut text = config.words.clone();
let mut words = text.iter_mut().skip(start_index).peekable();
loop {
if let Some(mut w) = words.next() {
if config.capitalise {
capitalise(&mut w, 0);
}
self.password.push_str(w.as_str());
match words.peek() {
Some(p) => {
let mut allowance = 0;
if self.password.len() < self.max_len {
allowance = self.max_len - self.password.len();
}
if p.len() > allowance {
if self.password.len() >= self.min_len
&& self.password.len() <= self.max_len
{
break;
} else if self.reset_count >= config.reset_amount {
self.password.truncate(self.max_len);
break;
} else {
self.reset_count += 1;
self.password.clear();
continue;
}
} else if self.password.len() < self.min_len {
continue;
} else if p.len() <= allowance && rng.gen_bool(0.8) {
continue;
} else {
break;
}
}
None => {
words = text.iter_mut().skip(0).peekable();
}
}
}
}
}
fn replace_chars(&mut self) {
let mut rng = thread_rng();
let range = Uniform::new(0, self.password.len());
let mut new_pass = String::with_capacity(self.max_len);
let mut pos = Vec::with_capacity(self.total_inserts);
while pos.len() < self.total_inserts {
let num = rng.sample(&range);
if !pos.contains(&num) {
pos.push(num);
}
}
for (i, c) in self.password.char_indices() {
if pos.contains(&i) {
new_pass.push(self.insertables.pop().unwrap());
} else {
new_pass.push(c);
}
}
self.password = new_pass;
}
fn insert_chars(&mut self) {
let mut rng = thread_rng();
if self.password.len() == 0 {
self.password.push(self.insertables.pop().unwrap());
self.total_inserts -= 1;
}
for _ in 0..self.total_inserts {
let index = rng.gen_range(0, self.password.len());
let c = self.insertables.pop().unwrap();
self.password.insert(index, c);
}
}
fn ensure_case(&mut self, config: &ValidatedConfig) {
let mut rng = thread_rng();
let u_amount = self
.password
.matches(|c: char| c.is_ascii_uppercase())
.count();
let mut l_indeces: Vec<usize> = self
.password
.char_indices()
.filter(|(_, c)| c.is_ascii_lowercase())
.collect::<Vec<(usize, char)>>()
.into_iter()
.map(|(i, _)| i)
.collect();
if u_amount == 0 {
self.force_upper = true;
} else if u_amount >= self.upper {
self.force_upper = false;
} else {
self.upper -= u_amount;
}
if self.upper > l_indeces.len() {
self.upper = l_indeces.len();
}
if self.force_upper && !config.dont_upper {
for _ in 0..self.upper {
let i = l_indeces.remove(rng.gen_range(0, l_indeces.len()));
capitalise(&mut self.password.as_mut_str(), i)
}
}
let mut u_indeces: Vec<usize> = self
.password
.char_indices()
.filter(|(_, c)| c.is_ascii_uppercase())
.collect::<Vec<(usize, char)>>()
.into_iter()
.map(|(i, _)| i)
.collect();
if l_indeces.len() == 0 {
self.force_lower = true;
} else if l_indeces.len() >= self.lower {
self.force_lower = false;
} else {
self.lower -= l_indeces.len();
}
if self.lower > u_indeces.len() {
self.lower = u_indeces.len();
}
if self.force_lower && !config.dont_lower {
for _ in 0..self.lower {
let i = u_indeces.remove(rng.gen_range(0, u_indeces.len()));
decapitalise(&mut self.password.as_mut_str(), i)
}
}
}
}
fn get_text_from_dir(dir: impl AsRef<Path>, mut text: &mut String) -> Result<(), std::io::Error> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
get_text_from_dir(&path, &mut text)?;
} else {
text.push_str(fs::read_to_string(&path).unwrap_or_default().as_str());
}
}
Ok(())
}
fn capitalise(s: &mut str, i: usize) {
if let Some(c) = s.get_mut(i..i + 1) {
c.make_ascii_uppercase();
}
}
fn decapitalise(s: &mut str, i: usize) {
if let Some(c) = s.get_mut(i..i + 1) {
c.make_ascii_lowercase();
}
}
fn process_range(range: &str) -> RangeResult<(usize, usize)> {
let min;
let max;
let range = range.trim_start_matches('-').trim_end_matches('-');
let re = Regex::new(r"-+").unwrap();
let range = re.replace_all(range, "-");
if range.matches('-').count() > 1 {
return Err(RangeError {
message: "more than two sides".to_string(),
});
}
if !range.chars().all(|c| c.is_numeric() || c == '-') {
return Err(RangeError {
message: "contains something other than integers and a - (dash)".to_string(),
});
}
if range.contains("-") {
let r: Vec<&str> = range.split("-").collect();
min = usize::from_str(r[0]).unwrap();
max = usize::from_str(r[1]).unwrap();
if max < min {
return Err(RangeError {
message: "right side of range can't be smaller than left side".to_string(),
});
}
Ok((min, max))
} else {
min = usize::from_str(&range).unwrap();
max = min.clone();
Ok((min, max))
}
}