mod symbol;
use std::fmt::{self, Display};
use clap::{Parser, ValueEnum};
use pwhash::sha512_crypt::hash;
use anyhow::{bail, Result};
use symbol::Symbols;
const DEFAULT_MAX_LEN: &str = "72";
const ENTROPY_CRITICAL_WARN: f64 = 45.0;
const ENTROPY_WARN: f64 = 55.0;
fn main() {
let args = Args::parse();
if let Err(e) = args.run() {
eprintln!("Error: {}", e);
}
}
#[derive(Debug, Parser)]
struct Args {
#[clap(short = 'm', long, default_value = "chars")]
mode: Mode,
#[clap(short = 'a', long)]
lower: bool,
#[clap(short = 'A', long)]
capital: bool,
#[clap(short = '0', long)]
digit: bool,
#[clap(short = '!', long)]
all_symbols: bool,
#[clap(short = 's', long)]
symbols: Option<String>,
#[clap(long)]
non_appear: bool,
#[clap(short = 'S', long, default_value = " ")]
sep: String,
#[clap(short = 'L', long)]
length: Option<usize>,
#[clap(short = 'E', long, default_value = "71.45")]
entropy: f64,
#[clap(short = 'M', long, default_value = DEFAULT_MAX_LEN)]
max_len: usize,
#[clap(short = 'H', long)]
hash: bool,
#[clap(short = 'N', long, default_value = "1")]
count: usize,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Mode {
Chars,
BasicWords,
Diceware,
DicewareAlnum,
}
impl Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Mode::Chars => write!(f, "chars"),
Mode::BasicWords => write!(f, "basic-words"),
Mode::Diceware => write!(f, "diceware"),
Mode::DicewareAlnum => write!(f, "diceware-alnum"),
}
}
}
impl Args {
fn run(&self) -> Result<()> {
warn_entropy(self.entropy);
for _ in 0..self.count {
let password = match self.mode {
Mode::Chars => self.char_mode()?,
_ => self.words_mode()?,
};
print!("{}", password);
if self.hash {
let hash = hash(password.as_bytes())?;
print!("\t{}", hash);
}
println!();
}
Ok(())
}
fn char_mode(&self) -> Result<String> {
let mut chars = String::new();
let mut groups = Vec::<&str>::new();
if self.lower {
let s = "abcdefghijklmnopqrstuvwxyz";
groups.push(s);
chars.push_str(s);
}
if self.capital {
let s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
groups.push(s);
chars.push_str(s);
}
if self.digit {
let s = "0123456789";
groups.push(s);
chars.push_str(s);
}
if self.all_symbols {
let s = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
groups.push(s);
chars.push_str(s);
} else if let Some(ref s) = self.symbols {
groups.push(s);
chars.push_str(s);
}
if chars.is_empty() {
bail!("No characters.");
}
let symbols = Symbols::from_chars(chars.chars());
let validate =
|s: &str| self.non_appear || groups.iter().all(|g| s.chars().any(|c| g.contains(c)));
self.generate(symbols, "", validate)
}
fn words_mode(&self) -> Result<String> {
let words = match self.mode {
Mode::BasicWords => &include_bytes!("../resources/basic-words.txt")[..],
Mode::Diceware => &include_bytes!("../resources/diceware.txt")[..],
Mode::DicewareAlnum => &include_bytes!("../resources/diceware-alnum.txt")[..],
m => panic!("Invalid mode: {}", m),
};
let symbols = Symbols::from_bufread(words)?;
let sep = self.sep.as_str();
self.generate(symbols, sep, |s| s.len() <= self.max_len)
}
fn generate(
&self,
symbols: Symbols,
sep: &str,
validate: impl Fn(&str) -> bool,
) -> Result<String> {
match self.length {
Some(length) => {
let ee = symbols.estimate_entropy(length, sep, &validate)?;
if ee == 0.0 {
bail!("It is impossible to meet the conditions.");
}
let password = symbols.generate(length, sep, &validate);
Ok(password)
}
None => {
let base = symbols.base_entropy(1);
let minimum_len = (self.entropy / base).ceil() as usize;
for length in minimum_len.. {
let ee = symbols.estimate_entropy(length, sep, &validate)?;
if ee == 0.0 {
bail!("Never met entropy requirement.");
}
if ee >= self.entropy {
let password = symbols.generate(length, sep, &validate);
return Ok(password);
}
}
unreachable!()
}
}
}
}
fn warn_entropy(ee: f64) {
if ee < ENTROPY_CRITICAL_WARN {
eprintln!("CRITICAL WARNING: This setting is too weak ({:.2} bits < {} bits). May be cracked by personal attackers.", ee, ENTROPY_CRITICAL_WARN);
return;
}
if ee < ENTROPY_WARN {
eprintln!(
"WARNING: This setting is weak ({:.2} bits < {} bits).",
ee, ENTROPY_WARN
);
return;
}
}