Skip to content

Commit

Permalink
Password mutation feature (#44)
Browse files Browse the repository at this point in the history
* feat: Add password mutation function

* chore: Add password mutation to command line interface

* feat: Add lengthen mode to mutation
  • Loading branch information
vschwaberow authored Oct 1, 2024
1 parent a2e2f00 commit 019b974
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 12 deletions.
65 changes: 65 additions & 0 deletions src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use crate::config::PasswordGeneratorConfig;
use crate::config::Separator;
use rand::seq::SliceRandom;
use rand::Rng;

const DEFAULT_SEPARATORS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
Expand Down Expand Up @@ -121,3 +122,67 @@ pub async fn generate_pronounceable_passwords(config: &PasswordGeneratorConfig)
}
passwords
}

pub fn mutate_password(
password: &str,
config: &PasswordGeneratorConfig,
lengthen: usize,
) -> String {
let mut rng = rand::thread_rng();
let mut mutated = password.to_string();
let mutation_count = (password.len() as f64 * 0.2).ceil() as usize;

for _ in 0..mutation_count {
let index = rng.gen_range(0..mutated.len());
let mutation_type = rng.gen_range(0..4);

match mutation_type {
0 => {
if let Some(new_char) = config.allowed_chars.choose(&mut rng) {
mutated.replace_range(index..index + 1, &new_char.to_string());
}
}
1 => {
if let Some(new_char) = config.allowed_chars.choose(&mut rng) {
mutated.insert(index, *new_char);
}
}
2 => {
if mutated.len() > 1 {
mutated.remove(index);
}
}
3 => {
if index < mutated.len() - 1 {
let mut chars: Vec<char> = mutated.chars().collect();
chars.swap(index, index + 1);
mutated = chars.into_iter().collect();
}
}
_ => unreachable!(),
}
}

if lengthen > 0 {
mutated = lengthen_password(&mutated, lengthen);
}

mutated
}

fn lengthen_password(password: &str, increase: usize) -> String {
let mut lengthened = password.to_string();
for _ in 0..increase {
lengthened.push(random_char());
}
lengthened
}

fn random_char() -> char {
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()"
.chars()
.collect::<Vec<char>>()
.choose(&mut rand::thread_rng())
.copied()
.unwrap()
}
135 changes: 123 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ mod strength;
use std::process;

use crate::config::DEFINE;
use clap::{value_parser, Arg, ArgAction, Command, ArgGroup};
use clap::{value_parser, Arg, ArgAction, ArgGroup, Command};
use colored::*;
use config::{PasswordGeneratorConfig, PasswordGeneratorMode, Separator};
use console::Term;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use error::{PasswordGeneratorError, Result};
use generator::{
generate_diceware_passphrase, generate_passwords, generate_pronounceable_passwords,
mutate_password,
};
use stats::show_stats;
use strength::{evaluate_password_strength, get_strength_feedback, get_strength_bar};
use strength::{evaluate_password_strength, get_strength_bar, get_strength_feedback};
use zeroize::Zeroize;

#[tokio::main]
Expand Down Expand Up @@ -108,6 +109,31 @@ async fn main() -> Result<()> {
.help("Generate pronounceable passwords")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("mutate")
.long("mutate")
.help("Mutate the passwords")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("mutation_type")
.long("mutation-type")
.help("Type of mutation to apply")
.default_value("default"),
)
.arg(
Arg::new("mutation_strength")
.long("mutation-strength")
.help("Strength of mutation")
.default_value("1"),
)
.arg(
Arg::new("lengthen")
.long("lengthen")
.value_name("INCREASE")
.help("Increase the length of passwords during mutation")
.value_parser(value_parser!(usize)),
)
.get_matches();

if matches.get_flag("interactive") {
Expand All @@ -116,13 +142,17 @@ async fn main() -> Result<()> {

let config = build_config(&matches)?;

match config.mode {
PasswordGeneratorMode::Diceware => handle_diceware(&config, &matches).await,
PasswordGeneratorMode::Password => {
if config.pronounceable {
handle_pronounceable(&config, &matches).await
} else {
handle_password(&config, &matches).await
if matches.get_flag("mutate") {
handle_mutation(&config, &matches).await
} else {
match config.mode {
PasswordGeneratorMode::Diceware => handle_diceware(&config, &matches).await,
PasswordGeneratorMode::Password => {
if config.pronounceable {
handle_pronounceable(&config, &matches).await
} else {
handle_password(&config, &matches).await
}
}
}
}
Expand Down Expand Up @@ -244,6 +274,43 @@ async fn handle_pronounceable(
Ok(())
}

async fn handle_mutation(
config: &PasswordGeneratorConfig,
matches: &clap::ArgMatches,
) -> Result<()> {
let passwords: Vec<String> = Input::<String>::new()
.with_prompt("Enter passwords to mutate (comma-separated)")
.interact_text()?
.split(',')
.map(|s| s.trim().to_string())
.collect();

let lengthen = matches.get_one::<usize>("lengthen").unwrap_or(&0);
let _mutation_type = matches
.get_one::<String>("mutation_type")
.unwrap_or(&"default".to_string());

let passwords_clone = passwords.clone();

println!("\n{}", "Mutated Passwords:".bold().green());
for password in passwords {
let mutated = mutate_password(&password, config, *lengthen);
println!("Original: {}", password.yellow());
println!("Mutated: {}", mutated.green());
println!();
}

if matches.get_flag("strength") {
print_strength_meter(&passwords_clone);
}

if matches.get_flag("stats") {
print_stats(&passwords_clone);
}

Ok(())
}

fn print_strength_meter(data: &[String]) {
println!("\n{}", "Password Strength:".blue().bold());
for (i, password) in data.iter().enumerate() {
Expand Down Expand Up @@ -285,7 +352,12 @@ async fn interactive_mode() -> Result<()> {
term.clear_screen()?;
println!("{}", "Welcome to NPWG Interactive Mode!".bold().cyan());

let options = vec!["Generate Password", "Generate Passphrase", "Exit"];
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)
Expand All @@ -296,7 +368,8 @@ async fn interactive_mode() -> Result<()> {
match selection {
0 => generate_interactive_password(&term, &theme).await?,
1 => generate_interactive_passphrase(&term, &theme).await?,
2 => break,
2 => mutate_interactive_password(&term, &theme).await?,
3 => break,
_ => unreachable!(),
}

Expand Down Expand Up @@ -431,4 +504,42 @@ async fn generate_interactive_passphrase(term: &Term, theme: &ColorfulTheme) ->
}

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 mutated = mutate_password(&password, &config, lengthen);

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

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

0 comments on commit 019b974

Please sign in to comment.