npwg 0.5.1

Securely generate random passwords
Documentation
// SPDX-License-Identifier: MIT
// Project: npwg
// File: src/interactive.rs
// Author: Volker Schwaberow <volker@schwaberow.de>
// Copyright (c) 2022 Volker Schwaberow

use crate::config::{PasswordGeneratorConfig, Separator};
use crate::diceware;
use crate::error::{PasswordGeneratorError, Result};
use crate::generator::{
    generate_diceware_passphrase, generate_passwords, generate_pronounceable_passwords,
    mutate_password, MutationType,
};
use crate::stats::show_stats;
use crate::strength::{
    evaluate_password_strength, get_improvement_suggestions, get_strength_bar,
    get_strength_feedback,
};
use colored::Colorize;
use console::Term;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use zeroize::Zeroize;

pub async fn interactive_mode() -> Result<()> {
    let term = Term::stdout();
    let theme = ColorfulTheme::default();

    loop {
        term.clear_screen()?;
        println!("{}", "Welcome to NPWG Interactive Mode!".bold().cyan());

        let options = vec![
            "Generate Password",
            "Generate Passphrase",
            "Mutate Password",
            "Exit",
        ];
        let selection = Select::with_theme(&theme)
            .with_prompt("What would you like to do?")
            .items(&options)
            .default(0)
            .interact_on(&term)
            .map_err(|e| PasswordGeneratorError::DialoguerError(e))?;

        match selection {
            0 => generate_interactive_password(&term, &theme).await?,
            1 => generate_interactive_passphrase(&term, &theme).await?,
            2 => mutate_interactive_password(&term, &theme).await?,
            3 => break,
            _ => unreachable!(),
        }

        if !Confirm::with_theme(&theme)
            .with_prompt("Do you want to perform another action?")
            .default(true)
            .interact_on(&term)
            .map_err(|e| PasswordGeneratorError::DialoguerError(e))?
        {
            break;
        }
    }

    println!("{}", "Thank you for using NPWG!".bold().green());
    Ok(())
}

async fn generate_interactive_password(term: &Term, theme: &ColorfulTheme) -> Result<()> {
    let length: u8 = Input::with_theme(theme)
        .with_prompt("Password length")
        .default(16)
        .interact_on(term)?;

    let count: u32 = Input::with_theme(theme)
        .with_prompt("Number of passwords")
        .default(1)
        .interact_on(term)?;

    let avoid_repeating = Confirm::with_theme(theme)
        .with_prompt("Avoid repeating characters?")
        .default(false)
        .interact_on(term)?;

    let pronounceable = Confirm::with_theme(theme)
        .with_prompt("Generate pronounceable passwords?")
        .default(false)
        .interact_on(term)?;

    let mut config = PasswordGeneratorConfig::new();
    config.length = length as usize;
    config.num_passwords = count as usize;
    config.set_avoid_repeating(avoid_repeating);
    config.pronounceable = pronounceable;
    config.validate()?;

    let pattern = Input::with_theme(theme)
        .with_prompt("Enter desired pattern or leave empty for no pattern")
        .default("".to_string())
        .interact_text()?;

    if !pattern.is_empty() {
        config.pattern = Some(pattern);
    }

    let passwords = if pronounceable {
        generate_pronounceable_passwords(&config).await?
    } else {
        generate_passwords(&config).await?
    };

    println!("\n{}", "Generated Passwords:".bold().green());
    passwords.iter().for_each(|p| println!("{}", p.yellow()));

    if Confirm::with_theme(theme)
        .with_prompt("Show strength meter?")
        .default(true)
        .interact_on(term)?
    {
        print_strength_meter(&passwords);
    }

    if Confirm::with_theme(theme)
        .with_prompt("Show statistics?")
        .default(false)
        .interact_on(term)?
    {
        print_stats(&passwords);
    }

    passwords.into_iter().for_each(|mut p| p.zeroize());
    Ok(())
}

async fn generate_interactive_passphrase(term: &Term, theme: &ColorfulTheme) -> Result<()> {
    let count: u32 = Input::with_theme(theme)
        .with_prompt("Number of passphrases")
        .default(1)
        .interact_on(term)?;

    let separator: String = Input::with_theme(theme)
        .with_prompt("Separator (single character, 'random', or press Enter for space)")
        .allow_empty(true)
        .interact_on(term)?;

    let wordlist = match diceware::get_wordlist().await {
        Ok(list) => list,
        Err(PasswordGeneratorError::WordlistDownloaded) => {
            println!("Wordlist downloaded. Please run the program again.");
            return Ok(());
        }
        Err(e) => return Err(e),
    };

    let mut config = PasswordGeneratorConfig::new();
    config.num_passwords = count as usize;
    config.set_use_words(true);

    config.separator = if separator.is_empty() {
        Some(Separator::Fixed(' '))
    } else {
        match separator.as_str() {
            "random" => Some(Separator::Random(('a'..='z').chain('0'..='9').collect())),
            s if s.len() == 1 => Some(Separator::Fixed(s.chars().next().unwrap())),
            _ => {
                println!("Invalid separator. Using default (space).");
                Some(Separator::Fixed(' '))
            }
        }
    };

    config.validate()?;

    let passphrases = generate_diceware_passphrase(&wordlist, &config).await?;
    println!("\n{}", "Generated Passphrases:".bold().green());
    passphrases.iter().for_each(|p| println!("{}", p.yellow()));

    if Confirm::with_theme(theme)
        .with_prompt("Show strength meter?")
        .default(true)
        .interact_on(term)?
    {
        print_strength_meter(&passphrases);
    }

    if Confirm::with_theme(theme)
        .with_prompt("Show statistics?")
        .default(false)
        .interact_on(term)?
    {
        print_stats(&passphrases);
    }

    Ok(())
}

async fn mutate_interactive_password(term: &Term, theme: &ColorfulTheme) -> Result<()> {
    let password: String = Input::with_theme(theme)
        .with_prompt("Enter the password to mutate")
        .interact_on(term)?;

    let config = PasswordGeneratorConfig::new();
    config.validate()?;

    let lengthen: usize = Input::with_theme(theme)
        .with_prompt("Increase the length of the password")
        .default(0)
        .interact_on(term)?;

    let mutation_strength: u32 = Input::with_theme(theme)
        .with_prompt("Enter mutation strength (1-10)")
        .validate_with(|input: &u32| {
            if *input >= 1 && *input <= 10 {
                Ok(())
            } else {
                Err("Please enter a number between 1 and 10")
            }
        })
        .default(1)
        .interact_on(term)?;

    let mutation_types = vec![
        MutationType::Replace,
        MutationType::Insert,
        MutationType::Remove,
        MutationType::Swap,
        MutationType::Shift,
    ];
    let mutation_type_index = Select::with_theme(theme)
        .with_prompt("Select mutation type")
        .items(&mutation_types)
        .default(0)
        .interact_on(term)?;
    let mutation_type = &mutation_types[mutation_type_index];

    let mutated = mutate_password(
        &password,
        &config,
        lengthen,
        mutation_strength,
        Some(mutation_type),
    );

    println!("\n{}", "Mutated Password:".bold().green());
    println!("Original: {}", password.yellow());
    println!("Mutated:  {} (using {:?})", mutated.green(), mutation_type);

    if Confirm::with_theme(theme)
        .with_prompt("Show strength meter?")
        .default(true)
        .interact_on(term)?
    {
        print_strength_meter(&vec![password.clone(), mutated.clone()]);
    }

    if Confirm::with_theme(theme)
        .with_prompt("Show statistics?")
        .default(false)
        .interact_on(term)?
    {
        print_stats(&vec![password, mutated]);
    }

    Ok(())
}

fn print_strength_meter(data: &[String]) {
    println!("\n{}", "Password Strength:".blue().bold());
    for (i, password) in data.iter().enumerate() {
        let strength = evaluate_password_strength(password);
        let feedback = get_strength_feedback(strength);
        let strength_bar = get_strength_bar(strength);
        println!(
            "Password {}: {} {:.2} {} {}",
            i + 1,
            strength_bar,
            strength,
            feedback.color(match &*feedback {
                "Very Weak" => "red",
                "Weak" => "yellow",
                "Moderate" => "blue",
                "Strong" => "green",
                "Very Strong" => "bright green",
                _ => "white",
            }),
            password.yellow()
        );

        if strength < 0.6 {
            let suggestions = get_improvement_suggestions(password);
            if !suggestions.is_empty() {
                println!("  {}:", "Improvement suggestions".cyan());
                for suggestion in suggestions {
                    println!("{}", suggestion);
                }
            }
        }
    }
}

fn print_stats(data: &[String]) {
    let pq = show_stats(data);
    println!("\n{}", "Statistics:".blue().bold());
    println!("Mean: {:.6}", pq.mean.to_string().yellow());
    println!("Variance: {:.6}", pq.variance.to_string().yellow());
    println!("Skewness: {:.6}", pq.skewness.to_string().yellow());
    println!("Kurtosis: {:.6}", pq.kurtosis.to_string().yellow());
}