From 12e3f28f51fcf50a53f4ca396d7dd266bf5cd962 Mon Sep 17 00:00:00 2001 From: Volker Schwaberow Date: Tue, 3 Sep 2024 06:53:14 +0200 Subject: [PATCH] feat: Added console interactive mode (#34) --- Cargo.lock | 46 +++++++++++++++++++ Cargo.toml | 2 + README.md | 10 +++++ src/error.rs | 9 ++++ src/main.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e716464..d62cc2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,6 +234,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -289,6 +302,19 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "dirs" version = "5.0.1" @@ -310,6 +336,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -801,7 +833,9 @@ dependencies = [ "chacha20", "clap", "colored", + "console", "dashmap", + "dialoguer", "dirs", "futures", "rand", @@ -1278,6 +1312,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -1567,6 +1607,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 6b05a5c..8d6765f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ futures = "0.3.30" colored = "2.1.0" serde = { version = "1.0.209", features = ["derive"] } thiserror = "1.0.63" +dialoguer = "0.11.0" +console = "0.15.8" [profile.release] opt-level = 3 diff --git a/README.md b/README.md index b9fdb72..f5d17ec 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ npwg [OPTIONS] - `--stats`: Show statistics about the generated passwords - `-a, --allowed `: Sets the allowed characters (comma-separated list of predefined sets) [default: allprint] - `--use-words`: Use words instead of characters (generate diceware passphrases) +- `-i --interactive`: Interactive mode, use a small console based gui to lead through the process - `-h, --help`: Print help - `-V, --version`: Print version @@ -70,6 +71,15 @@ npwg [OPTIONS] ### Examples +Use the interactive mode +```sh +npwg -i +``` +or +```sh +npwg --interactive +``` + Generate a password with the default length (16 characters): ```sh npwg diff --git a/src/error.rs b/src/error.rs index 30dad42..b91817f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,7 @@ // Copyright (c) 2022 Volker Schwaberow use thiserror::Error; +use dialoguer::Error as DialoguerError; #[derive(Error, Debug)] pub enum PasswordGeneratorError { @@ -16,6 +17,14 @@ pub enum PasswordGeneratorError { Network(#[from] reqwest::Error), #[error("Worldlist downloaded, restart the program to use it.")] WordlistDownloaded, + #[error("Dialoguer error: {0}")] + DialoguerError(DialoguerError), +} + +impl From for PasswordGeneratorError { + fn from(error: DialoguerError) -> Self { + PasswordGeneratorError::DialoguerError(error) + } } pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index a723eec..444d787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,8 @@ use error::{PasswordGeneratorError, Result}; use generator::{generate_diceware_passphrase, generate_passwords}; use stats::show_stats; use zeroize::Zeroize; +use dialoguer::{theme::ColorfulTheme, Select, Input, Confirm}; +use console::Term; #[tokio::main] async fn main() -> Result<()> { @@ -71,8 +73,19 @@ async fn main() -> Result<()> { .help("Use words instead of characters") .action(ArgAction::SetTrue), ) + .arg( + Arg::new("interactive") + .short('i') + .long("interactive") + .help("Start interactive console mode") + .action(ArgAction::SetTrue), + ) .get_matches(); + if matches.get_flag("interactive") { + return interactive_mode().await; + } + let config = build_config(&matches)?; match config.mode { @@ -157,3 +170,113 @@ fn print_stats(data: &[String]) { println!("Skewness: {:.6}", pq.skewness.to_string().yellow()); println!("Kurtosis: {:.6}", pq.kurtosis.to_string().yellow()); } + +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", "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 => 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 mut config = PasswordGeneratorConfig::new(); + config.length = length as usize; + config.num_passwords = count as usize; + config.set_avoid_repeating(avoid_repeating); + config.validate()?; + + let passwords = generate_passwords(&config).await; + println!("\n{}", "Generated Passwords:".bold().green()); + passwords.iter().for_each(|password| println!("{}", password.yellow())); + + 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 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.validate()?; + + let passphrases = generate_diceware_passphrase(&wordlist, &config).await; + println!("\n{}", "Generated Passphrases:".bold().green()); + passphrases.iter().for_each(|passphrase| println!("{}", passphrase.yellow())); + + if Confirm::with_theme(theme) + .with_prompt("Show statistics?") + .default(false) + .interact_on(term)? + { + print_stats(&passphrases); + } + + Ok(()) +}