kg_passgen 0.1.0

Password generator that hashes an input master password and a service url
Documentation
//! The generator module contains the core logic for generating passwords
//! based on user input and configuration settings. It includes functions
//! for hashing, applying generator-specific transformations, validating
//! passwords, and generating the final password.
//! 
//! # Examples
//! ```
//! use kg_passgen::config::{Config, HashAlgorithm, GeneratorType};
//! use kg_passgen::generator::generate_password;
//! let config = Config::KGPG;
//! 
//! let example_password = generate_password("https://example.com", "my_master_password", &config).unwrap();
//! assert_eq!(example_password.len(), config.length as usize);
//! assert_eq!(example_password, "mXApUt1OgTb$xZh");
//! 
//! let different_password = generate_password("https://test.com", "my_master_password", &config).unwrap();
//! assert_eq!(different_password.len(), config.length as usize);
//! assert_eq!(different_password, "jtNRe$VWbnE#F6y");
//! ```

//! Returns an error if the length in the config is invalid for the selected hash algorithm
//! ```
//! use kg_passgen::config::{Config, HashAlgorithm, GeneratorType};
//! use kg_passgen::generator::generate_password;
//! let config = Config::default()
//!     .with_hash_algorithm(HashAlgorithm::MD5)
//!    .with_length(30) // invalid length for MD5
//!   .with_hops(1);
//! 
//! let result = generate_password("https://example.com", "master", &config);
//! assert!(result.is_err());
//! assert!(matches!(result, Err(InvalidLengthError)));
//! ```

use core::fmt;

use base64::Engine;
use crate::config::{Config, HashAlgorithm, GeneratorType};

/// Custom error type for invalid length configurations
#[derive(Debug, Clone)]
pub struct InvalidLengthError;

impl fmt::Display for InvalidLengthError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Invalid length for the selected hash algorithm")
    }
}


/// Hashes the input string using MD5 and encodes the result in Base64
pub fn hash_md5(input: &str) -> String {
    let digest = md5::compute(input.as_bytes());
    base64::prelude::BASE64_STANDARD.encode(digest.0)
}

/// Hashes the input string using SHA512 and encodes the result in Base64
pub fn hash_sha512(input: &str) -> String {
    use sha2::{Sha512, Digest};

    let mut hasher = Sha512::new();
    hasher.update(input.as_bytes());
    let result = hasher.finalize();
    base64::prelude::BASE64_STANDARD.encode(result)
}

/// Validates that the generated password meets the required criteria
/// KGPG requires at least one special character from the set !#%@$&, 
/// as well as one uppercase letter, one lowercase letter, and one digit.
/// It cannot start with an uppercase letter.
/// Additionally it validates the length of the generated password based on the config.
/// SGP requires at least one uppercase letter, one lowercase letter, and one digit.
/// It cannot start with an uppercase letter.
/// Additionally it validates the length of the generated password based on the config.
/// # Examples
/// ```
/// use kg_passgen::config::{Config, HashAlgorithm, GeneratorType};
/// use kg_passgen::generator::validate_password;
/// let config = Config::KGPG;
/// assert!(validate_password("bAcdef1!asdfgasd", &config));
/// assert!(!validate_password("abcAefgasd12asd", &config));
/// ```
/// 
/// ```
/// use kg_passgen::config::{Config, HashAlgorithm, GeneratorType};
/// use kg_passgen::generator::validate_password;
/// let config = Config::SGP;
/// assert!(validate_password("bcAdef1123as", &config));
/// assert!(!validate_password("abcdefgfsadg", &config));
/// ```
pub fn validate_password(password: &str, config: &Config) -> bool {
    let sliced_password = match password.get(0..config.length as usize) {
        Some(slice) => slice,
        None => return false,
    };

    let password_regex = fancy_regex::Regex::new(r"(?=.*^[a-z])(?=.*[A-Z])(?=.*[0-9])([a-zA-Z0-9#?!@$%^&*]){8,}$").unwrap();
    if !password_regex.is_match(sliced_password).unwrap() {
        return false;
    }

    if config.generator_type == GeneratorType::KGPG {
        let special_char_regex = fancy_regex::Regex::new(r"[!#%@$&]").unwrap();
        if !special_char_regex.is_match(sliced_password).unwrap() {
            return false;
        }
    }

    true
}

/// Applies KGPG-specific character replacements
pub fn apply_kgpg (password: &str) -> String {
    let mut kgpg_password = String::new();
    for c in password.chars() {
        match c {
            '+' => kgpg_password.push('!'),
            '/' => kgpg_password.push('#'),
            '=' => kgpg_password.push('%'),
            '0' => kgpg_password.push('@'),
            '8' => kgpg_password.push('$'),
            '9' => kgpg_password.push('&'),
            _ => kgpg_password.push(c),
        }
    }
    kgpg_password
}

/// Applies SGP-specific character replacements
pub fn apply_sgp (password: &str) -> String {
    let mut sgp_password = String::new();
    for c in password.chars() {
        match c {
            '+' => sgp_password.push('9'),
            '/' => sgp_password.push('8'),
            '=' => sgp_password.push('A'),
            _ => sgp_password.push(c),
        }
    }
    sgp_password
}

/// Applies the password generation logic based on a single concatenated input
/// For the KGPG and SGP algorithms, it expects a password in the format "master_password:host"
pub fn apply_password_hops (password: &str, config: &Config) -> Result<String, InvalidLengthError> {
    let mut hopped_password = password.to_string();
    let mut iteration = 0;
    let md5_length_invalid = config.hash_algorithm == HashAlgorithm::MD5 && (config.length < 8 || config.length > 24);
    let sha512_length_invalid = config.hash_algorithm == HashAlgorithm::SHA512 && (config.length < 8 || config.length > 84);
    if md5_length_invalid || sha512_length_invalid {
        return Err(InvalidLengthError);
    }
    while iteration < config.hops {
        hopped_password = match config.hash_algorithm {
            HashAlgorithm::MD5 => hash_md5(&hopped_password),
            HashAlgorithm::SHA512 => hash_sha512(&hopped_password),
        };

        hopped_password = match config.generator_type {
            GeneratorType::KGPG => apply_kgpg(&hopped_password),
            GeneratorType::SGP => apply_sgp(&hopped_password),
        };

        if iteration == config.hops - 1 && !validate_password(&hopped_password, config) {
            iteration -= 1;
        }
        iteration += 1;
    }


    let sliced_password = match hopped_password.get(0..config.length as usize) {
        Some(slice) => slice,
        None => return Ok(hopped_password),
    };

    Ok(sliced_password.to_string())
}

/// Main function for generating a password
/// # Examples
/// ```
/// use kg_passgen::config::{Config, HashAlgorithm, GeneratorType};
/// use kg_passgen::generator::generate_password;
/// let config = Config::KGPG;
/// 
/// let example_password = generate_password("https://example.com", "my_master_password", &config).unwrap();
/// assert_eq!(example_password.len(), config.length as usize);
/// assert_eq!(example_password, "mXApUt1OgTb$xZh");
/// 
/// let different_password = generate_password("https://test.com", "my_master_password", &config).unwrap();
/// assert_eq!(different_password.len(), config.length as usize);
/// assert_eq!(different_password, "jtNRe$VWbnE#F6y");
/// ```
pub fn generate_password(url: &str, master_password: &str, config: &Config) -> Result<String, InvalidLengthError> {
    // Placeholder for password generation logic
    let host =  crate::url_helper::get_host(url, &config.strip_subdomain);

    let concat =format!("{}:{}", master_password.trim(), host.trim());
    apply_password_hops(&concat, config)
}

#[cfg(test)]
mod tests;