Building a Password Cracker Using OpenAI and Rust

by Bwiz

In the evolving landscape of cybersecurity, the ability to test and enhance security measures is critical.

One way to achieve this is by developing tools that can simulate potential threats, such as password crackers.  This article explains the development of a password cracker built with Rust, leveraging the OpenAI API for generating custom word lists.

This tool demonstrates the practical application of AI in enhancing cybersecurity measures.

Project Setup

The Cargo.toml file defines the dependencies required for this project.  Below is the content of the Cargo.toml file:

Cargo.toml:

[package]
name = "password_cracker"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
sha2 = "0.9"
md-5 = "0.10.1"
sha1 = "0.10"
num_cpus = "1.13"
hex = "0.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"

The core functionality is implemented in the main.rs file.

The program starts by reading command line arguments, including the hash type, target hash, start letters, case, length, and total words to generate.  It then fetches a list of potential passwords from the OpenAI API and attempts to crack the given hash using these passwords:

main.rs:

use md5::Md5;
use reqwest::Client;
use serde::Deserialize;
use sha1::{Digest as Sha1Digest, Sha1};
use sha2::{Digest as Sha2Digest, Sha256};
use std::env;
use std::error::Error;
use std::fs;
use std::sync::{Arc, Mutex};
use std::thread;
use tokio;

#[derive(Clone)]
enum HashType {
    Sha256,
    Md5,
    Sha1,
}

#[derive(Deserialize)]
struct Config {
    openai: OpenAIConfig,
}

#[derive(Deserialize)]
struct OpenAIConfig {
    api_key: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 6 {
        eprintln!(
            "Usage: {} <hash_type> <hash> <start_letters> <case> <length> <total_words>",
            args[0]
        );
        std::process::exit(1);
    }

    let hash_type = match args[1].as_str() {
        "sha256" => HashType::Sha256,
        "md5" => HashType::Md5,
        "sha1" => HashType::Sha1,
        _ => {
            eprintln!("Unsupported hash type: {}", args[1]);
            std::process::exit(1);
        }
    };

    let target_hash = args[2].clone();
    let start_letters = &args[3];
    let case = &args[4];
    let length = &args[5];
    let total_words: usize = args[6].parse().unwrap_or(1000);
    let config = read_config("config.toml")?;
    let passwords = get_passwords_from_openai(
        &config.openai.api_key,
        start_letters,
        case,
        length,
        total_words,
    )
    .await?;
    println!("Number of passwords generated: {}", passwords.len());
    // Print number of password

    match crack_password(hash_type, &target_hash, &passwords) {
        Some(password) => println!("Password found: {}", password),
        None => println!("Password not found"),
    }
    Ok(())
}

fn read_config(filename: &str) -> Result<Config, Box<dyn Error>> {
    let contents = fs::read_to_string(filename)?;
    let config: Config = toml::from_str(&contents)?;
    Ok(config)
}

async fn get_passwords_from_openai(
    api_key: &str,
    start_letters: &str,
    case: &str,
    length: &str,
    total_words: usize,
) -> Result<Vec<String>, Box<dyn Error>> {
    let client = Client::new();
    let request_url = "https://api.openai.com/v1/chat/completions";

    let prompt = format!(
        "Make a custom word list, starting with the letters '{}', in '{}', and '{}' characters long, {} words in total. Make the entire {} word list here no matter what. Don't number the list.",
        start_letters, case, length, total_words, total_words
    );

    let request_body = serde_json::json!({
    "model": "gpt-4o",
    "messages": [
    {"role": "system", "content": "You are a cybersecurity expert and educational professional."},
    { "role": " user", "content": prompt}
    ],
    "max_tokens": 4096,
    // Increased token limit to handle larger responses
    });

    println!("Sending request to URL: {}", request_url);
    println!("Request body: {}", request_body);

    let response = client
        .post(request_url)
        .header("Authorization", format!("Bearer {}", api_key))
        .header("Content-Type", "application/json")
        .json(&request_body)
        .send()
        .await?;

    println!("Response status: {}", response.status());
    println!("Response headers:{:?}", response.headers());

    if !response.status().is_success() {
        eprintln!("Failed to fetch passwords: {}", response.status());
        let response_text = response.text().await?;
        eprintln!("Response body: {}", response_text);
        return Err(Box::new(std::io::Error::new(
            std::io::ErrorKind::Other,
            "Failed to fetch passwords",
        )));
    }

    let response_json = response.json::<serde_json::Value>().await?;
    println!("Response JSON: {:?}",response_ json);
    // Debugging line to print the JSON response

    let text = match response_json["choices"][0]["message"]["content"].as_str() {
        Some(text) => text,
        None => {
            eprintln!("Failed to parse the response JSON");
            return Err(Box::new(std::io::Error::new(
                std::io::ErrorKind::Other,
                "Failed to parse the response JSON",
            )));
        }
    };

    let passwords: Vec<String> = text.lines().map(|line| line.trim().to_string()).collect();
    Ok(passwords)
}

fn crack_password(hash_type: HashType, target_hash: &str, passwords: &[String]) -> Option<String> {
    let target_hash = Arc::new(target_hash.to_string());
    let found_password = Arc::new(Mutex::new(None));
    let num_threads = num_cpus::get();
    let chunk_size = (passwords.len() / num_threads) + 1;

    let mut threads = vec![];

    for chunk in passwords.chunks(chunk_size) {
        let target_hash = Arc::clone(&target_hash);
        let found_password = Arc::clone(&found_password);
        let chunk = chunk.to_vec();
        let hash_type = hash_type.clone();

        let handle = thread::spawn(move || {
            for password in chunk {
                let hash = hash_password(&hash_type, &password);
                if hash == *target_hash {
                    let mut found = found_password.lock().unwrap();
                    *found = Some(password);
                    break;
                }

                if found_password.lock().unwrap().is_some() {
                    break;
                }
            }
        });

        threads.push(handle);
    }

    for handle in threads {
        handle.join().expect("Thread failed to join");
    }

    let result = found_password.lock().unwrap().clone();
    result
}

fn hash_password(hash_type: &HashType, password: &str) -> String {
    match hash_type {
        HashType::Sha256 => {
            let mut hasher = Sha256::new();
            hasher.update(password);
            let result = hasher.finalize();
            hex::encode(result)
        }
        HashType::Md5 => {
            let mut hasher = Md5::new();
            hasher.update(password);
            let result = hasher.finalize();
            hex::encode(result)
        }
        HashType::Sha1 => {
            let mut hasher = Sha1::new();
            hasher.update(password);
            let result = hasher.finalize();
            hex::encode(result)
        }
    }
}

Detailed Walkthrough

Command Line Arguments

The program begins by parsing command line arguments.  These arguments specify the hash type (SHA-256, MD5, or SHA-1), the target hash, and parameters for generating the word list (starting letters, case, length, and total words).

Configuration File

The config.toml file contains the OpenAI API key required for making requests to the OpenAI API.  This configuration is read at runtime using the read_config() function.

OpenAI API Integration

The get_passwords_from_openai() function is responsible for generating a custom word list using the OpenAI API.  It constructs a prompt based on the input parameters and sends a request to the OpenAI API.  The response is parsed to extract the generated passwords.

Example:  If you want to generate a list of 1000 passwords starting with pass in lowercase, and eight characters long, the function constructs the following prompt:

Make a custom word list, starting with the letters 'pass', in 'lowercase', and '8' characters long, 1000 words in total. Make the entire 1000 word list here no matter what. Don't number the list.

Password Cracking

The crack_password() function distributes the password-cracking task across multiple threads to leverage multi-core processors.  It compares the hash of each generated password with the target hash.  If a match is found, the function returns the password.

Example:  Here is an example command line execution to crack the MD5 hash of the word password:

$ password_cracker md5 5f4dcc3b5aa765d61d8327deb882cf99 pass lowercase 8 1000

Sample Output:

Sending request to URL: https://api.openai.com/v1/chat/completions 
Request body: {"max_tokens":4096,"messages":[{"content":"You are a cybersecurity expert and educational professional.","role":"system"},{"content":"Make a custom word list, starting with the letters 'pass', 
              in 'lowercase', and '8' characters long, 1000 words in total. Make the entire 1000 word list here no matter what. Don't number the list.","role":"user"}],"model":"gpt-4o"} 
Response status: 200 OK 
Response headers: {"date": "Sat, 20 Jul 2024 00:07:17 GMT", "content-type": "application/json", "transfer-encoding": "chunked", "connection": "keep-alive",

...

Response JSON: Object {"choices": Array [Object {"finish_reason": String("stop"), "index": Number(0), "logprobs": Null, "message": Object {"content": String("Sure, generating a custom word list with the given 
               constraints:\n\npassable\npassably\npassaged\npassages\npassager\npassages\npassangs\npassband\npassbook\npasscame\npasscase\npasserby\npassible\npassifid\npassingy\npassless\npasslike\npasslock
               \npassword\npasstime\npasstive\npasstout\npastoral\npastorem\npasstyle\npassters\npasswept\npasstout\npassages\npassback\npassbuck\npassclip\npassfold\n

...

               passenue\npassenus\n\nI hope this helps!\n"), "role":String("assistant")}}],

...

               "usage": Object {"completion_tokens": Number(1497), "prompt_tokens": Number(70), "total_tokens": Number(1567)}}
Number of passwords generated: 481
Password found: password

In this example, the program successfully cracked the MD5 hash 5f4dcc3b5aa765d61d8327deb882cf99, corresponding to the password: password

Conclusion

This Rust-based password cracker demonstrates how to leverage the OpenAI API to generate custom word lists for security testing.

By integrating AI-generated data with a robust, multi-threaded approach, this tool can efficiently simulate potential security threats and help enhance cybersecurity measures.

Code: Cargo.toml

Code: main.rs

Return to $2600 Index