aboutsummaryrefslogblamecommitdiffstats
path: root/rust/src/main.rs
blob: 38738505d9470fc4030974ca8ec3befa0605c3d4 (plain) (tree)



































































































































































































































































                                                                                                               
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<char>,
    alnum: &'a Vec<char>,
    printlen: u8,
    col_width: u8,
    col_num: u16,
}

fn main() {
    let lower = LOWER.chars().collect::<Vec<_>>();
    let upper = UPPER.chars().collect::<Vec<_>>();
    let digit = DIGIT.chars().collect::<Vec<_>>();
    let other = OTHER.chars().collect::<Vec<_>>();
    let extra = EXTRA.chars().collect::<Vec<_>>();
    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::<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);
            }
        }
    }
    if !env_wordlist.is_empty() {
        wordlist = env_wordlist;
    }
    if !env_normal.is_empty() {
        normal = env_number.chars().collect::<Vec<_>>();
    }
    if !env_special.is_empty() {
        special = env_special.chars().collect::<Vec<_>>();
    }

    //
    // 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::<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
    };

    //
    // 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<char>, data: &CommonPrintData) {
    let mut strings: Vec<String> = 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<char>, alpha: &Vec<char>, alnum: &Vec<char>) -> 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<String> {
    let file = File::open(filename).unwrap();
    let reader = BufReader::new(file);

    reader.lines().map(|l| l.unwrap()).collect()
}

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("-")
}