diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | go/makepass.go | 8 | ||||
-rw-r--r-- | rust/Cargo.toml | 1 | ||||
-rw-r--r-- | rust/src/main.rs | 351 |
4 files changed, 252 insertions, 109 deletions
@@ -3,3 +3,4 @@ /makepass.go /makepass.rs rust/target +rust/debug diff --git a/go/makepass.go b/go/makepass.go index 34d7d73..a5241a3 100644 --- a/go/makepass.go +++ b/go/makepass.go @@ -1,3 +1,11 @@ +// +// Author : Dennis Eriksen <d@ennis.no> +// File : makepass.go +// Created : 2023-09-05 +// Licence : BSD-3-Clause +// +// Copyright (c) 2018-2023 Dennis Eriksen <d@ennis.no> + package main import ( diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 99ede9d..181b7a7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "makepass" +authors = ["Dennis Eriksen <d@ennis.no>"] version = "0.1.0" edition = "2021" rust-version = "1.68" diff --git a/rust/src/main.rs b/rust/src/main.rs index 3873850..083a99f 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,38 +1,60 @@ -use clap::{value_parser, Arg, ArgAction, Command}; +// +// Author : Dennis Eriksen <d@ennis.no> +// File : main.rs +// Bin : makepass.rs +// Created : 2023-09-05 +// Licence : BSD-3-Clause +// +// Copyright (c) 2018-2023 Dennis Eriksen <d@ennis.no> + +// +// Imports +// +use clap::{Arg, ArgAction, Command, crate_authors, value_parser}; use rand::{seq::SliceRandom, Rng}; use std::env; -use std::fs::File; +use std::fs; +use std::io; use std::io::prelude::*; -use std::io::BufReader; -use std::path::Path; -use std::process; +use std::process::exit; use terminal_size::{terminal_size, Width}; +// +// Constants +// const MAX: u8 = 255; const RANGE_MAX: u8 = 42; const RANGE_MIN: u8 = 8; const PASS_WORDS: u8 = 8; +// Character sets 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"; +// Defaults +const DEFAULT_LENGTH: u8 = 0; +const DEFAULT_NUMBER: u8 = 10; +const DEFAULT_WORDLIST: &str = "/usr/share/dict/words"; +// Structure used to pass common data to the print-function struct CommonPrintData<'a> { length: u8, - alpha: &'a Vec<char>, - alnum: &'a Vec<char>, + alpha: &'a [char], + alnum: &'a [char], + printbool: bool, printlen: u8, col_width: u8, col_num: u16, } +// +// Main function. This is where the magic happens +// fn main() { + // Construct arrays of the character sets let lower = LOWER.chars().collect::<Vec<_>>(); let upper = UPPER.chars().collect::<Vec<_>>(); let digit = DIGIT.chars().collect::<Vec<_>>(); @@ -42,133 +64,111 @@ fn main() { let alnum = [&alpha[..], &digit[..]].concat(); let every = [&alnum[..], &other[..]].concat(); + // These are the sets we actually use 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(); + // Set the defaults + let mut length = DEFAULT_LENGTH; + let mut number = DEFAULT_NUMBER; + let mut wordlist = DEFAULT_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(|_| { + let env = ( + env::var("MAKEPASS_LENGTH").unwrap_or_default(), + env::var("MAKEPASS_NUMBER").unwrap_or_default(), + env::var("MAKEPASS_WORDLIST").unwrap_or_default(), + env::var("MAKEPASS_NORMAL").unwrap_or_default(), + env::var("MAKEPASS_SPECIAL").unwrap_or_default(), + ); + + // length + if !env.0.is_empty() { + length = env.0.parse().unwrap_or_else(|_| { + // length is a <u8>, which is 0-255, so no need for further error-checking eprintln!( "Error: MAKEPASS_LENGTH is not a valid number. Valid numbers are between 0 and 255" ); - process::exit(1); + exit(1); }); } - if !env_number.is_empty() { - match env_number.parse::<u8>() { + // number + if !env.1.is_empty() { + // number is a <u8>, but takes a minimum value of 1 + match env.1.parse::<u8>() { 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); + exit(1); } } } - if !env_wordlist.is_empty() { - wordlist = env_wordlist; + if !env.2.is_empty() { // wordlist + wordlist = env.2; } - if !env_normal.is_empty() { - normal = env_number.chars().collect::<Vec<_>>(); + if !env.3.is_empty() { // Overwrite normal character array if MAKEPASS_NORMAL is present + normal = env.3.chars().collect::<Vec<_>>(); } - if !env_special.is_empty() { - special = env_special.chars().collect::<Vec<_>>(); + if !env.4.is_empty() { // Overwrite special character array if MAKEPASS_SPECIAL is present + special = env.4.chars().collect::<Vec<_>>(); } // // Args // + let opts = cli().get_matches(); - 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::<u8>("length_flag") { - Some(n) => length = *n, - _ => (), - } - match opts.get_one::<u8>("number") { - Some(n) => number = *n, - _ => (), - } - match opts.get_one::<u8>("length") { - Some(n) => length = *n, - _ => (), - } - let printbool = if opts.get_flag("printlen") { - true - } else { - false - }; + // Set to new value if exists, or just set to old value + length = *opts.get_one::<u8>("length_flag").unwrap_or(&length); + number = *opts.get_one::<u8>("number").unwrap_or(&number); + length = *opts.get_one::<u8>("length_arg").unwrap_or(&length); + let printbool = opts.get_flag("printlen"); // // Other logic // let printlen: u8 = if printbool && length < 100 { - 3 + 2 } else if printbool { - 4 + 3 } else { 0 }; + // Get the width of the terminal let size = terminal_size(); let term_width: u16 = match size { Some((Width(w), _)) => w, None => 1, }; + // Calculate how wide each column has to be 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, - }; + // Number of columns to print + // If printbool is true, col_num is term_width / (col_width + printlen + 1) + // Else, col_num is term_width / col_width + // col_num is never 0. Use std::cmp::max to use the largest value of 1 and the above. + let col_num: u16 = std::cmp::max( + 1, + match printbool { + true => term_width / (col_width + printlen + 1) as u16, + false => term_width / col_width as u16, + }, + ); + // Fill in common print data let data = CommonPrintData { length: length, alpha: &alpha, alnum: &alnum, + printbool: printbool, printlen: printlen, col_width: col_width, col_num: col_num, @@ -178,10 +178,12 @@ fn main() { // Do the dirty! // + // Normal passwords print_columns("Normal passwords", number, &normal, &data); println!(""); + // Special passwords print_columns( "Passwords with special characters", number / 3 * 2 + 1, @@ -189,10 +191,20 @@ fn main() { &data, ); - if Path::new(&wordlist).exists() { + // Passphrases + if fs::metadata(&wordlist).is_ok() { + // Open the file + let file = fs::File::open(&wordlist).unwrap(); + + // Create a buffered reader to handle file contents + let reader = io::BufReader::new(file); + + // Read words into a Vector + let words = reader.lines().map(|l| l.unwrap()).collect(); + + // Print println!(""); println!("Passphrases:"); - let words = read_file(&wordlist); for _ in 0..number / 2 { println!("{}", passphrase(&words)); @@ -200,6 +212,9 @@ fn main() { } } +// +// Print passwords in neat columns +// fn print_columns(title: &str, num: u8, chars: &Vec<char>, data: &CommonPrintData) { let mut strings: Vec<String> = Vec::new(); for _ in 0..num { @@ -210,51 +225,169 @@ fn print_columns(title: &str, num: u8, chars: &Vec<char>, data: &CommonPrintData let mut i: u16 = 0; for s in &strings { - i = i + 1; + i += 1; - if data.printlen > 0 { - print!("{1:00$} ", (data.printlen - 1) as usize, s.len()); + if data.printbool { + print!("{1:00$} ", data.printlen 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!(""); + if i % data.col_num == 0 || (i == num as u16 && i % data.col_num > 0) { + println!(); } } } -fn randstring(len: u8, chars: &Vec<char>, alpha: &Vec<char>, alnum: &Vec<char>) -> String { +// +// Generate random strings to use as passwords +// +fn randstring(len: u8, chars: &[char], alpha: &[char], alnum: &[char]) -> String { let mut rng = rand::thread_rng(); let length = if len == 0 { - rng.gen_range(RANGE_MIN..RANGE_MAX) + 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()); + let c = if i == 0 { + alpha.choose(&mut rng) } else if i == length - 1 { - s.push(*alnum.choose(&mut rng).unwrap()); + alnum.choose(&mut rng) } else { - s.push(*chars.choose(&mut rng).unwrap()); - } + chars.choose(&mut rng) + }; + s.push(*c.unwrap()); } s } -fn read_file(filename: &str) -> Vec<String> { - let file = File::open(filename).unwrap(); - let reader = BufReader::new(file); +// +// Generate passphrases +// +fn passphrase(wordlist: &Vec<String>) -> String { + let mut rng = rand::thread_rng(); + let mut passphrase = Vec::new(); + + for _ in 0..PASS_WORDS { + let word = wordlist.choose(&mut rng).unwrap().clone(); + passphrase.push(word); + } - reader.lines().map(|l| l.unwrap()).collect() + passphrase.join("-") } -fn passphrase(wordlist: &Vec<String>) -> String { - let mut rng = rand::thread_rng(); - (0..PASS_WORDS) - .map(|_| wordlist.choose(&mut rng).unwrap()) - .cloned() - .collect::<Vec<_>>() - .join("-") +// +// Handle command line arguments +// +fn cli() -> Command { + Command::new("makepass") + .about("makepass - create random passwords") + .author(crate_authors!()) + .arg( + Arg::new("length_flag") + .short('l') + .value_name("LENGTH") + .help(format!("Length of passwords (0..={0}). 0 = random.", MAX)) + .value_parser(value_parser!(u8).range(0..)), + ) + .arg( + Arg::new("number") + .short('n') + .value_name("NUM") + .help(format!("Number of passwords (1..={0}). [default: 10]", MAX)) + .value_parser(value_parser!(u8).range(1..)) + .long_help("this is the long help. What is it?"), + ) + .arg( + Arg::new("printlen") + .short('p') + .help("Print length") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("length_arg") + .value_name("LENGTH") + .help(format!( + "Length of passwords (0..={MAX}). 0 = random length." + )) + .value_parser(value_parser!(u8).range(0..)), + ) + .override_help("NAME + makepass - create several random passwords + +SYNOPSIS + makepass [OPTIONS] [NUM] + + If a NUM is provided, passwords will be NUM characters long. + + By default `makepass` will output passwords from the three following classes: + + - Normal passwords - random strings with letters (both lower and upper + case), numbers, and dashes and underscores. + + - Passwords with special characters - random strings generated from lower + and upper case letters, numbers, and the following characters: + !#$%&/()=?+-_,.;:<>[]{}|@* + + - Passphrases - if we find a dictionary, a series of eight random words + from the dictionary, separated by dashes. The number of words can not be + changed, but you do not have to use all of them. Use as mane as you want. + + The first character will always be alphabetic, and the last will always be + alphanumeric. + +DESCRIPTION + makepass has the following options: + + -h + output this help-text + -l + length of passwords. See MAKEPASS_LENGTH below + -n + number of passwords. See MAKEPASS_NUMBER below + -p + print length of number + +ENVIRONMENT + makepass examines the following environmental variables. + + MAKEPASS_LENGTH + Specifies the length of passwords. Valid values are 0-255. If 0, a + random value between 8 and 42 will be used for each password. -l + overrides this environmental variable, and the argument NUM overrides + that again. So `MAKEPASS_LENGTH=10 makepass -l 12 14` will give + passwords that are 14 characters long, even though both -l and + MAKEPASS_LENGTH also specifies a length. + + MAKEPASS_NUMBER + The number of passwords to generate. This formula is used to determine + how many passwords from each group should be generated: + - (n) normal passwords + - (n / 3 * 2 + 1) special passwords + - (n / 2) passphrases + Where n is 10 by default. Valid values for n are 1-255. Floating-poing + math is not used, so results may vary. + + MAKEPASS_PRINTLEN + If 1, print length of all passwords. If 0, don't. + + MAKEPASS_NORMAL + String of characters from which to generate \"normal\" passwords. + Defaults to: + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_ + + MAKEPASS_SPECIAL + String of characters from which to generate passwords with special + characters. Defaults to the same characters as in MAKEPASS_NORMAL, plus + these: + !#$%&/()=?+-_,.;:<>[]{}|@* + + MAKEPASS_WORDLIST + Specifies the dictionary we find words for passphrases in. If this is + unset or empty, we try \"/usr/share/dict/words\". If that file does not + exist, no passphrases will be provided. + +AUTHOR + Dennis Eriksen <https://dnns.no>") } |