aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--go/makepass.go8
-rw-r--r--rust/Cargo.toml1
-rw-r--r--rust/src/main.rs351
4 files changed, 252 insertions, 109 deletions
diff --git a/.gitignore b/.gitignore
index 8976dda..391cbbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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>")
}