From 165bd648aad1f35541618c31a39a381840c05d00 Mon Sep 17 00:00:00 2001 From: ManOfGoldForever Date: Fri, 30 Jan 2026 14:08:46 -0500 Subject: [PATCH] first commit --- .gitignore | 8 ++++ Cargo.toml | 13 ++++++ src/args.rs | 39 +++++++++++++++++ src/main.rs | 85 ++++++++++++++++++++++++++++++++++++ src/storage.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/args.rs create mode 100644 src/main.rs create mode 100644 src/storage.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c70b92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# .gitignore +/target +target/ +Cargo.lock +*.hash +*.bin +*.enc +*.json diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..98528f0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "password-manager" +version = "0.1.0" +edition = "2024" + +[dependencies] +argon2 = { version = "0.5.3", features = ["std", "password-hash"] } +chacha20poly1305 = "0.10.1" +clap = { version = "4.5", features = ["derive"] } +hex = "0.4.3" +rpassword = "7.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..ed76a82 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,39 @@ +use clap::{Args, Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "PassManager")] +#[command(about = " A simple password manager CLI", long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Create a new password with a name + Add(AddPass), + /// Get an already made password using its name + Get(GetPass), + /// Delete an already made password using its name + Delete(DeletePass), + /// Lists the names of all created passwords + List, +} + +#[derive(Args)] +pub struct AddPass { + /// The name of the password to add + pub name: String, +} + +#[derive(Args)] +pub struct GetPass { + /// The name of the password to get + pub name: String, +} + +#[derive(Args)] +pub struct DeletePass { + /// The name of the password to delete + pub name: String, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3a4e5e9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,85 @@ +mod args; +mod storage; + +use args::{Cli, Commands}; +use clap::Parser; +use rpassword::read_password; + +fn main() { + let cli: Cli = Cli::parse(); + let master_hash_path = "master.hash"; + let file_path = "passwords.enc"; + let salt_path = "salt.bin"; + + let encryption_salt = storage::get_or_create_salt(salt_path); + + let encryption_key: [u8; 32]; + + if !std::path::Path::new(master_hash_path).exists() { + print!("Enter Master Password : "); + std::io::Write::flush(&mut std::io::stdout()).expect("Flush failed"); + let masterp1 = read_password().expect("Failed to read password"); + + print!("Confirm Master Password : "); + std::io::Write::flush(&mut std::io::stdout()).expect("Flush failed"); + let masterp2 = read_password().expect("Failed to read password"); + + if masterp1 == masterp2 { + println!("Creating new Master Password..."); + let hash = storage::hash_master_password(&masterp1); + std::fs::write(master_hash_path, hash).expect("Failed to save hash"); + encryption_key = storage::derive_key(&masterp1, &encryption_salt); + } else { + println!("Passwords did not match! Please try again."); + return; + } + } else { + print!("Enter Master Password: "); + std::io::Write::flush(&mut std::io::stdout()).expect("Flush failed"); + let input = read_password().expect("Read failed"); + let saved_hash = std::fs::read_to_string(master_hash_path).expect("Failed to read hash"); + if !storage::verify_master_password(&input, &saved_hash) { + println!("Wrong Master Password! Access Denied."); + return; + } + encryption_key = storage::derive_key(&input, &encryption_salt); + } + + let mut passwords = storage::load_passwords(file_path, &encryption_key); + + match &cli.command { + Commands::Add(args) => { + print!("Enter password for {} : ", args.name); + std::io::Write::flush(&mut std::io::stdout()).expect("Flush failed"); + let p1 = read_password().expect("Failed to read password"); + print!("Confirm Password : "); + std::io::Write::flush(&mut std::io::stdout()).expect("Flush failed"); + let p2 = read_password().expect("Failed to read password"); + if p1 == p2 { + passwords.insert(args.name.clone(), p1); + storage::save_passwords(file_path, &passwords, &encryption_key); + println!("Saved successfully!"); + } else { + println!("Passwords did not match! Please try again."); + } + } + Commands::Get(args) => match passwords.get(&args.name) { + Some(pw) => println!("Password for {} : {}", args.name, pw), + None => println!("No password found for '{}'", args.name), + }, + Commands::Delete(args) => { + storage::delete_password(file_path, &args.name, &encryption_key); + } + Commands::List => { + if passwords.is_empty() { + println!("No passwords saved yet!"); + } else { + println!("--- Saved Passwords ---"); + for name in passwords.keys() { + println!("• {}", name); + } + println!("-----------------------"); + } + } // _ => {} + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..6e172b5 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,114 @@ +use argon2::{ + Algorithm, Argon2, Params, Version, + password_hash::{ + PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::RngCore, + }, +}; +use chacha20poly1305::{ + AeadCore, XChaCha20Poly1305, XNonce, + aead::{Aead, KeyInit, OsRng}, +}; +use std::collections::HashMap; +use std::fs; + +pub fn get_or_create_salt(path: &str) -> String { + if std::path::Path::new(path).exists() { + fs::read_to_string(path).expect("Failed to read salt file") + } else { + let mut salt_bytes = [0u8; 16]; + let mut rng = chacha20poly1305::aead::OsRng; + rng.fill_bytes(&mut salt_bytes); + let new_salt = hex::encode(salt_bytes); + fs::write(path, &new_salt).expect("Failed to save salt file"); + new_salt + } +} + +pub fn derive_key(password: &str, salt: &str) -> [u8; 32] { + let mut key = [0u8; 32]; + let argon2 = Argon2::default(); + + let salt_string = + SaltString::encode_b64(salt.as_bytes()).expect("Salt string is too long or invalid b64"); + + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt_string) + .expect("Failed to hash password"); + + let output = password_hash.hash.expect("Hash output missing"); + + let output_bytes = output.as_bytes(); + if output_bytes.len() >= 32 { + key.copy_from_slice(&output_bytes[..32]); + } else { + panic!("Argon2 output too short! We need 32 bytes."); + } + + key +} + +pub fn load_passwords(path: &str, key: &[u8; 32]) -> HashMap { + let data = match fs::read(path) { + Ok(d) => d, + Err(_) => return HashMap::new(), + }; + if data.len() < 24 { + return HashMap::new(); + } + + let (nonce_bytes, ciphertext) = data.split_at(24); + let nonce = XNonce::from_slice(nonce_bytes); + + let cipher = XChaCha20Poly1305::new(key.into()); + let plaintext = cipher + .decrypt(nonce, ciphertext) + .expect("Decryption failed! Data corrupted or wrong key."); + + let json_string = String::from_utf8(plaintext).expect("Invalid UTF-8"); + serde_json::from_str(&json_string).unwrap_or_default() +} + +pub fn save_passwords(path: &str, data: &HashMap, key: &[u8; 32]) { + let json = serde_json::to_string(data).expect("Failed to serialize"); + + let cipher = XChaCha20Poly1305::new(key.into()); + let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); + + let ciphertext = cipher + .encrypt(&nonce, json.as_bytes()) + .expect("Encryption failed"); + + let mut combined = nonce.to_vec(); + combined.extend(ciphertext); + fs::write(path, combined).expect("Failed to write to file"); +} + +pub fn delete_password(path: &str, name: &str, key: &[u8; 32]) { + let mut passwords = load_passwords(path, key); + if passwords.remove(name).is_some() { + save_passwords(path, &passwords, key); + println!("Successfully deleted password for: {}", name); + } else { + println!("Error: No password found with the name '{}'", name); + } +} + +pub fn hash_master_password(password: &str) -> String { + let salt = SaltString::generate(&mut OsRng); + let params = Params::new(131072, 5, 4, None).expect("Invalid params"); + let argon2 = Argon2::new(Algorithm::Argon2id, Version::default(), params); + + argon2 + .hash_password(password.as_bytes(), &salt) + .expect("Error hashing password") + .to_string() +} + +pub fn verify_master_password(password: &str, recorded_hash: &str) -> bool { + let argon2 = Argon2::default(); + let parsed_hash = PasswordHash::new(recorded_hash).expect("Invalid hash format"); + + argon2 + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok() +}