use clap::Parser;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use reqwest;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
master_password: String,
input_string: String,
#[arg(default_value_t = 4)]
num_words: usize,
#[arg(default_value = "https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt")]
wordlist_url: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let wordlist = fetch_wordlist(&args.wordlist_url)?;
let seed = generate_seed(&args.master_password, &args.input_string);
let mut rng = ChaCha8Rng::from_seed(seed);
let passphrase = generate_passphrase(
&wordlist,
&mut rng,
args.num_words,
&args.master_password,
&args.input_string,
)?;
println!("Generated Passphrase: {}", passphrase);
Ok(())
}
fn fetch_wordlist(url: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let cache_dir = get_cache_dir()?;
fs::create_dir_all(&cache_dir)?;
let url_hash = format!("{:x}", Sha256::digest(url.as_bytes()));
let cache_file = cache_dir.join(format!("wordlist_{}.txt", &url_hash[..16]));
if cache_file.exists() {
println!("Loading wordlist from cache: {:?}", cache_file);
match fs::read_to_string(&cache_file) {
Ok(cached_content) => {
let wordlist = parse_wordlist(&cached_content);
if !wordlist.is_empty() {
println!("Successfully loaded {} words from cache.", wordlist.len());
return Ok(wordlist);
}
}
Err(e) => {
println!("Failed to read cache file: {}, fetching fresh copy", e);
}
}
}
println!("Fetching wordlist from: {}", url);
let response = reqwest::blocking::get(url)?;
let body = response.error_for_status()?.text()?;
let wordlist = parse_wordlist(&body);
if wordlist.is_empty() {
return Err("Fetched wordlist is empty.".into());
}
if let Err(e) = fs::write(&cache_file, &body) {
println!("Warning: Failed to cache wordlist: {}", e);
} else {
println!("Wordlist cached to: {:?}", cache_file);
}
println!("Successfully fetched {} words.", wordlist.len());
Ok(wordlist)
}
fn parse_wordlist(content: &str) -> Vec<String> {
content
.lines()
.map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
parts[1].to_string() } else {
line.trim().to_string() }
})
.filter(|line| !line.is_empty())
.collect()
}
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let home_dir = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| "Unable to determine home directory")?;
Ok(PathBuf::from(home_dir).join(".passphrase_generator"))
}
fn generate_seed(master_password: &str, input_string: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(master_password.as_bytes());
hasher.update(input_string.as_bytes());
let result = hasher.finalize();
let mut seed = [0u8; 32];
seed.copy_from_slice(&result);
seed
}
fn generate_passphrase(
wordlist: &[String],
rng: &mut impl rand::Rng,
num_words: usize,
_master_password: &str, _input_string: &str,
) -> Result<String, Box<dyn std::error::Error>> {
if wordlist.is_empty() {
return Err("Wordlist is empty, cannot generate passphrase.".into());
}
let mut passphrase_words = Vec::with_capacity(num_words);
let mut has_capital = false;
for _ in 0..num_words {
let index = rng.gen_range(0..wordlist.len());
let mut word = wordlist[index].clone();
if !has_capital && rng.gen_bool(0.5) { word = word.to_uppercase();
has_capital = true;
}
passphrase_words.push(word);
}
if !has_capital {
if let Some(first_word) = passphrase_words.first_mut() {
*first_word = first_word.to_uppercase();
}
}
let current_year = get_current_year()?;
let mut passphrase = passphrase_words.join("-");
passphrase.push('-');
passphrase.push_str(¤t_year.to_string());
Ok(passphrase)
}
fn get_current_year() -> Result<i32, Box<dyn std::error::Error>> {
let duration = SystemTime::now().duration_since(UNIX_EPOCH)?;
let years_since_epoch = duration.as_secs() / (365 * 24 * 60 * 60);
let current_year = 1970 + years_since_epoch as i32;
Ok(current_year)
}