Skip to content

Commit

Permalink
feat: Added console interactive mode (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
vschwaberow authored Sep 3, 2024
1 parent b74ee15 commit 12e3f28
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 0 deletions.
46 changes: 46 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ npwg [OPTIONS]
- `--stats`: Show statistics about the generated passwords
- `-a, --allowed <CHARS>`: 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

Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Copyright (c) 2022 Volker Schwaberow

use thiserror::Error;
use dialoguer::Error as DialoguerError;

#[derive(Error, Debug)]
pub enum PasswordGeneratorError {
Expand All @@ -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<DialoguerError> for PasswordGeneratorError {
fn from(error: DialoguerError) -> Self {
PasswordGeneratorError::DialoguerError(error)
}
}

pub type Result<T> = std::result::Result<T, PasswordGeneratorError>;
123 changes: 123 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
}

0 comments on commit 12e3f28

Please sign in to comment.