first commit
This commit is contained in:
commit
165bd648aa
5 changed files with 259 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# .gitignore
|
||||||
|
/target
|
||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
|
*.hash
|
||||||
|
*.bin
|
||||||
|
*.enc
|
||||||
|
*.json
|
||||||
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -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"
|
||||||
39
src/args.rs
Normal file
39
src/args.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
85
src/main.rs
Normal file
85
src/main.rs
Normal file
|
|
@ -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!("-----------------------");
|
||||||
|
}
|
||||||
|
} // _ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/storage.rs
Normal file
114
src/storage.rs
Normal file
|
|
@ -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<String, String> {
|
||||||
|
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<String, String>, 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()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue