use clap::{value_parser, Arg, ArgAction, Command}; use rand::{seq::SliceRandom, Rng}; use std::env; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use std::path::Path; use std::process; use terminal_size::{terminal_size, Width}; const MAX: u8 = 255; const RANGE_MAX: u8 = 42; const RANGE_MIN: u8 = 8; const PASS_WORDS: u8 = 8; const LOWER: &str = "abcdefghijklmnopqrstuvwxyz"; const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DIGIT: &str = "0123456789"; const OTHER: &str = "!$%&#/()=?+-_,.;:<>[]{}|@*"; const EXTRA: &str = "-_"; const DEF_LENGTH: u8 = 0; const DEF_NUMBER: u8 = 10; const DEF_WORDLIST: &str = "/usr/share/dict/words"; struct CommonPrintData<'a> { length: u8, alpha: &'a Vec, alnum: &'a Vec, printlen: u8, col_width: u8, col_num: u16, } fn main() { let lower = LOWER.chars().collect::>(); let upper = UPPER.chars().collect::>(); let digit = DIGIT.chars().collect::>(); let other = OTHER.chars().collect::>(); let extra = EXTRA.chars().collect::>(); let alpha = [&lower[..], &upper[..]].concat(); let alnum = [&alpha[..], &digit[..]].concat(); let every = [&alnum[..], &other[..]].concat(); let mut normal = [&alnum[..], &extra[..]].concat(); let mut special = every; let mut length = DEF_LENGTH; let mut number = DEF_NUMBER; let mut wordlist = DEF_WORDLIST.to_string(); // // env // Deal with env before cli-args, because cli-args take precedence. // let env_length = env::var("MAKEPASS_LENGTH").unwrap_or_default(); let env_number = env::var("MAKEPASS_NUMBER").unwrap_or_default(); let env_wordlist = env::var("MAKEPASS_WORDLIST").unwrap_or_default(); let env_normal = env::var("MAKEPASS_NORMAL").unwrap_or_default(); let env_special = env::var("MAKEPASS_SPECIAL").unwrap_or_default(); if !env_length.is_empty() { length = env_length.parse().unwrap_or_else(|_| { eprintln!( "Error: MAKEPASS_LENGTH is not a valid number. Valid numbers are between 0 and 255" ); process::exit(1); }); } if !env_number.is_empty() { match env_number.parse::() { Ok(n) if n >= 1 => number = n, _ => { eprintln!("Error: MAKEPASS_NUMBER is not a valid number. Valid numbers are between 1 and 255"); process::exit(1); } } } if !env_wordlist.is_empty() { wordlist = env_wordlist; } if !env_normal.is_empty() { normal = env_number.chars().collect::>(); } if !env_special.is_empty() { special = env_special.chars().collect::>(); } // // Args // let opts = Command::new("makepass") .arg( Arg::new("length_flag") .short('l') .value_name("length") .help(format!("Length of passwords (0..={MAX}). 0 = random.")) .value_parser(value_parser!(u8).range(0..)), ) .arg( Arg::new("number") .short('n') .help(format!("Number of passwords (1..={MAX}). [default: 10]")) .value_parser(value_parser!(u8).range(1..)), ) .arg( Arg::new("printlen") .short('p') .help("Print length") .action(ArgAction::SetTrue), ) .arg( Arg::new("length") .help(format!( "Length of passwords (0..={MAX}). 0 = random length." )) .value_parser(value_parser!(u8).range(0..)), ) .get_matches(); match opts.get_one::("length_flag") { Some(n) => length = *n, _ => (), } match opts.get_one::("number") { Some(n) => number = *n, _ => (), } match opts.get_one::("length") { Some(n) => length = *n, _ => (), } let printbool = if opts.get_flag("printlen") { true } else { false }; // // Other logic // let printlen: u8 = if printbool && length < 100 { 3 } else if printbool { 4 } else { 0 }; let size = terminal_size(); let term_width: u16 = match size { Some((Width(w), _)) => w, None => 1, }; let col_width: u8 = match length { 0 => RANGE_MAX + 2, _ => length + 2, }; let col_num: u16 = match term_width / (col_width + printlen) as u16 { 0 => 1, n => n, }; let data = CommonPrintData { length: length, alpha: &alpha, alnum: &alnum, printlen: printlen, col_width: col_width, col_num: col_num, }; // // Do the dirty! // print_columns("Normal passwords", number, &normal, &data); println!(""); print_columns( "Passwords with special characters", number / 3 * 2 + 1, &special, &data, ); if Path::new(&wordlist).exists() { println!(""); println!("Passphrases:"); let words = read_file(&wordlist); for _ in 0..number / 2 { println!("{}", passphrase(&words)); } } } fn print_columns(title: &str, num: u8, chars: &Vec, data: &CommonPrintData) { let mut strings: Vec = Vec::new(); for _ in 0..num { strings.push(randstring(data.length, chars, data.alpha, data.alnum)); } println!("{}:", title); let mut i: u16 = 0; for s in &strings { i = i + 1; if data.printlen > 0 { print!("{1:00$} ", (data.printlen - 1) as usize, s.len()); } print!("{1:<0$}", data.col_width as usize, s); if (i % data.col_num == 0) || (i == num as u16 && i % data.col_num > 0) { println!(""); } } } fn randstring(len: u8, chars: &Vec, alpha: &Vec, alnum: &Vec) -> String { let mut rng = rand::thread_rng(); let length = if len == 0 { rng.gen_range(RANGE_MIN..RANGE_MAX) } else { len }; let mut s = String::with_capacity(length as usize); for i in 0..length { if i == 0 { s.push(*alpha.choose(&mut rng).unwrap()); } else if i == length - 1 { s.push(*alnum.choose(&mut rng).unwrap()); } else { s.push(*chars.choose(&mut rng).unwrap()); } } s } fn read_file(filename: &str) -> Vec { let file = File::open(filename).unwrap(); let reader = BufReader::new(file); reader.lines().map(|l| l.unwrap()).collect() } fn passphrase(wordlist: &Vec) -> String { let mut rng = rand::thread_rng(); (0..PASS_WORDS) .map(|_| wordlist.choose(&mut rng).unwrap()) .cloned() .collect::>() .join("-") }