securepass 0.1.0

A password generator and balancer library in Rust
Documentation
use rand::{thread_rng, Rng};

pub struct PasswordOptions {
    pub length:Option<u32>,
    pub include_special_chars:Option<bool>,
    pub include_uppercase:Option<bool>,
    pub include_numbers:Option<bool>,
    pub with_balancing:Option<bool>,
    pub phrase:Option<String>
}
    
    const UPPERCASE_CHARSET: &str = "QWERTYUIOPASDFGHJKLZXCVBNM";
    const LOWERCASE_CHARSET: &str = "qwertyuiopasdfghjklzxcvbnm";
    const NUMBERS: &str = "1234567890";
    const SPECIAL_CHARSET:&str = "!@#$%^&*()-=_+{}[]<>?";

    pub fn generate_random_password(charset: &str, length: u32) -> String {
        let mut rng_thread = thread_rng();
        (0..length)
            .map(|_| {
                let index = rng_thread.gen_range(0..charset.len());
                charset.chars().nth(index).unwrap()
            })
            .collect()
    }
    
    pub fn generate_password(options:Option<PasswordOptions>) -> String {
        let default_options = PasswordOptions {
            length: Some(12),
            include_special_chars: Some(true),
            include_uppercase: Some(true),
            include_numbers: Some(true),
            with_balancing: Some(true),
            phrase: None,
        };

        let options = options.unwrap_or(default_options);
        let length = options.length.unwrap_or(10);
        if length < 10 {
           return String::from("Password length less than 10 is considered weak.")
        }

        let charset = generate_charset(&options);

        let mut password:String = generate_random_password(&charset, length);

        if options.phrase.is_none() && options.with_balancing.unwrap_or(true) {
            balance_password(&mut password, &options);
        }


        password
    }

    pub fn balance_password(password:&mut String, options:&PasswordOptions) {
        let mut rng_thread = thread_rng();
        let password_length:usize = options.length.unwrap_or(12) as usize;

        if password.len() < password_length {
            let charset = format!("{}{}{}{}", UPPERCASE_CHARSET, LOWERCASE_CHARSET, NUMBERS, SPECIAL_CHARSET);
            let num_chars_to_add = (password_length - password.len()) as u32;
            let str_to_add = generate_random_password(&charset, num_chars_to_add);
            password.push_str(&str_to_add);
        }

        loop {
            let has_lowercase = password.chars().any(|c| c.is_lowercase());
            let mut has_uppercase = password.chars().any(|c| c.is_uppercase());
            let mut has_number = password.chars().any(|c| c.is_digit(10));
            let mut has_special = password.chars().any(|c| SPECIAL_CHARSET.contains(c));
    
            if !options.include_uppercase.unwrap_or(false) {
                has_uppercase = true;
            }
            if !options.include_numbers.unwrap_or(false) {
                has_number = true;
            }
            if !options.include_special_chars.unwrap_or(false) {
                has_special = true;
            }

    
            if  has_uppercase && has_number && has_special && has_lowercase {
                break;
            }

            if !has_lowercase  {
                replace_char(password, LOWERCASE_CHARSET, &mut rng_thread);
            }
            if !has_uppercase && options.include_uppercase.unwrap_or(false) {
                replace_char(password, UPPERCASE_CHARSET, &mut rng_thread);
            }
            if !has_number && options.include_numbers.unwrap_or(false) {
                replace_char(password, NUMBERS, &mut rng_thread);
            }
            if !has_special && options.include_special_chars.unwrap_or(false) {
                replace_char(password, SPECIAL_CHARSET, &mut rng_thread);
            }
        }
    }

    pub fn check_password_strength(password:&str) -> &str {
        let mut score = 0;
        let password_length = password.len();

        if password_length >= 12 {
            score += 2;
        } else if password_length >= 10 {
            score +=1;
        }

        if password.chars().any(|c| c.is_lowercase()) {
            score += 2;
        }

        if password.chars().any(|c| c.is_uppercase()) {
            score += 2;
        }

        if password.chars().any(|c| c.is_digit(10)) {
            score += 2;
        }

        if password.chars().any(|c| SPECIAL_CHARSET.contains(c)) {
            score += 2;
        }

        match score {
            10 => "Strong",
            7..=9 => "Medium",
            _ => "Weak",
        }
    }

     fn generate_charset(options:&PasswordOptions) -> String {
        let mut charset = String::from("");

        if let Some(existed_phrase) = &options.phrase {
            charset.push_str(&remove_whitespace(existed_phrase));
        } else {
            charset.push_str(LOWERCASE_CHARSET);
            if options.include_uppercase.unwrap_or(false) {
                charset.push_str(UPPERCASE_CHARSET);
            }
            if options.include_numbers.unwrap_or(false) {
                charset.push_str(NUMBERS);
            }
            if options.include_special_chars.unwrap_or(false) {
                charset.push_str(SPECIAL_CHARSET);
            }
        }

        charset
    } 

    fn replace_char(password: &mut String, set: &str, rng_thread: &mut rand::prelude::ThreadRng) {
        let index = rng_thread.gen_range(0..password.len());
        let replace_char = set.chars().nth(rng_thread.gen_range(0..set.len())).unwrap();
        password.replace_range(index..=index, &replace_char.to_string());
    }
    

    fn remove_whitespace(input: &String) -> String {
        input.split_whitespace().collect()
    }


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_generate_password_without_args() {
    
      let password = generate_password(None);
      assert_eq!(password.len(), 12);
      assert_eq!(check_password_strength(&password), "Strong");
    }

    #[test]
    fn test_generate_password() {
        let options = PasswordOptions {
            length: Some(15),
            include_special_chars: Some(true),
            include_uppercase: Some(true),
            include_numbers: Some(true),
            with_balancing: Some(true),
            phrase: None,
        };
    
      let password = generate_password(Some(options));
      assert_eq!(password.len(), 15);
      assert_eq!(check_password_strength(&password), "Strong");
      assert!(password.chars().any(|c| LOWERCASE_CHARSET.contains(c)));
      assert!(password.chars().any(|c| UPPERCASE_CHARSET.contains(c)));
      assert!(password.chars().any(|c| NUMBERS.contains(c)));
      assert!(password.chars().any(|c| SPECIAL_CHARSET.contains(c)));
    }

    #[test]
    fn test_generate_password_from_phrase() {
        let options = PasswordOptions {
            length: Some(12),
            include_special_chars: Some(true),
            include_uppercase: Some(true),
            include_numbers: Some(true),
            with_balancing: Some(true),
            phrase: Some("a".to_string()),
        };
    
      let password = generate_password(Some(options));
      assert_eq!(password, "aaaaaaaaaaaa");
    }

    #[test]
    fn test_balance_password() {
        let options = PasswordOptions {
            length: Some(12),
            include_special_chars: Some(true),
            include_uppercase: Some(true),
            include_numbers: Some(true),
            with_balancing: Some(true),
            phrase: None,
        };
     let mut password = "qwertyuiop".to_string();
     assert_eq!(check_password_strength(&password), "Weak");

     balance_password(&mut password, &options);
     assert_eq!(check_password_strength(&password), "Strong");
    }
    
    
    #[test]
    fn test_check_password_strength() {
        assert_eq!(check_password_strength("StrongP@ssword123!"), "Strong");
        assert_eq!(check_password_strength("MediumPass123"), "Medium");
        assert_eq!(check_password_strength("weakpassword"), "Weak");
    }
}