Compare commits

...

17 commits

14 changed files with 364 additions and 8 deletions

29
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,29 @@
# Contributing to pm 🦀
First, thanks for your interest in contributing to "pm"! Contributions are what make open-source so amazing.
## How Can I Contribute?
### Reporting Bugs
- Check the Issues tab to make sure the bug hasn't already been reported
- If it has not, open an issue with the label `bug` and describe the problem, your operating system, and steps to reproduce the bug.
### Suggesting Features
- Open an issue with the label `enhancement`.
- Describe the feature you'd like to see and why it would be useful. (e.g., "Add a cli to improve user experience").
### Pull Requests (Code Contributions)
- If you've fixed a bug, added a new feature, or even just fixed a README typo, you can submit a pull request with your changes.
- Make sure the code is easy to read and understand, include tests for new features or bug fixes and they all pass, and follow the Rust Style Guide (You can use the `cargo fmt` command to make sure it fits the style guide.)
- Also since this is a Rust project you will need the standard toolchain to run cargo commands like `cargo build`.
## Security Note
If you find a security vulnerability, please do not open a public issue and instead contact me directly so it can be fixed without being disclosed.
## License
By contributing, you agree that your contributions will be licensed under the project's MIT License.

View file

@ -2,6 +2,7 @@
name = "pm" name = "pm"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "MIT"
[dependencies] [dependencies]
argon2 = { version = "0.5.3", features = ["std", "password-hash"] } argon2 = { version = "0.5.3", features = ["std", "password-hash"] }

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ManOfGoldForever
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
README.md
View file

@ -0,0 +1,112 @@
# pm 🦀
A fast, secure, and lightweight password manager CLI built entirely in Rust. No cloud, no tracking, just local encryption.
## Table of Contents
- [Why use this?](#why-use-this?)
- [Installation](#installation)
- [Build from Source](#build-from-source)
- [Uninstallation](#uninstallation)
- [Usage](#usage)
- [Security and Data](#security-and-data)
- [Future Plans](#future-plans)
- [Contributing](#contributing)
- [License](#license)
### Why use this?
- **Privacy First:** Your passwords never leave your machine.
- **Modern Encryption:** Uses XChaCha20-Poly1305 and Argon2id.
- **Memory Safe:** Sensitive data is wiped from RAM immediately after use.
- **Lightweight:** Tiny binary with an instant startup and no gui bloat.
### Installation
1. Download the latest compressed folder for your OS (.tar.gz for linux/Mac or .zip for Windows) from the [releases](https://github.com/ManOfGoldForever/Rust-Password-Manager/releases/tag/first-release) tab.
2. Extract the folder
3. Run the installer (this also works when updating to a newer downloaded version):
- **Linux/Mac:** Open a terminal in the extracted folder and run: `chmod +x linux-mac-install.sh && ./linux-mac-install.sh`
- **Windows:** Right-click windows-install.ps1 and select `Run with PowerShell`
This script moves the pm binary to your "PATH" so you can use it anywhere by typing pm in a terminal.
### Build from Source
If you would rather compile the binary yourself rather than using pre-built releases, follow these steps:
1. **Prerequisites**
You will need the Rust toolchain installed.
- visit [rustup.rs](https://rustup.rs/) and follow the instructions for your OS.
> **WARNING!** Linux users may need build essentials (like `gcc` or `make`) installed using their package manager to compile Rust crates.
2. **Clone and Build**
Use the following code in the terminal.
`git clone https://github.com/ManOfGoldForever/Rust-Password-Manager.git`
`cd Rust-Password-Manager`
`cargo build --release`
3. **Locate Binary**
After the build finishes your executable will be located at:
- **Linux/Mac:** `target/release/pm`
- **Windows:** `target/release/pm.exe`
### Uninstallation
If for whatever reason you want to remove this perfect and amazing tool along with permanently deleting your encrypted vault:
- **Linux/Mac:** Open a terminal in the extracted folder and run: `chmod +x linux-mac-uninstall.sh && ./linux-mac-uninstall.sh`
- **Windows:** Right-click windows-uninstall.ps1 and select `Run with PowerShell`
> **WARNING!** This will delete all saved passwords, your master password, and everything else along with the tool!
### Usage
The first time you run any command you'll be prompted to set a master password.
> **IMPORTANT!** The master password is the only key to access your vault so remember it and don't lose it or you won't be able to access your data.
```rust
A simple password manager CLI
Usage: pm <COMMAND>
Commands:
add Create a new password with a name
get Get an already made password using its name
delete Delete an already made password using its name
list Lists the names of all created passwords
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
```
### Security and Data
All data lives in a hidden folder in your home directory if you ever need to start from scratch you could delete it:
- **Linux/Mac:** `~/.my_pass_manager`
- **Windows:** `%USERPROFILE%\.my_pass_manager\`
This tool uses Argon2id to perform some black magic. So even if someone stole your passwords.enc file from your computer, they would not be able to read it without your master password. Basically, if someone manages to steal your passwords.enc file from your computer you have bigger problems than them figuring out your passwords.
### Future Plans
I plan to add more to this eventually like maybe a secure password generator or a TUI or something, but I don't have a list of things to add yet. Maybe after using it some I will come up with some stuff we'll see what happens.
### Contributing
If you are interested in contributing to this project look at [CONTRIBUTING](./CONTRIBUTING.md) for more information on how you can.
### License
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
# Enjoy :)

11
linux-mac-install.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash
BINARY="pm"
if [ -f "./$BINARY" ]; then
echo "Installing $BINARY to /usr/local/bin..."
sudo cp "./$BINARY" /usr/local/bin/
sudo chmod +x /usr/local/bin/$BINARY
echo "Done! You can now use 'pm' from any terminal."
else
echo "Error: Binary '$BINARY' not found. Make sure you are in the folder with the downloaded file."
fi

5
linux-mac-uninstall.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
echo "Removing pm binary and encrypted data..."
sudo rm -f /usr/local/bin/pm
rm -rf ~/.my_pass_manager
echo "Uninstalled successfully."

Binary file not shown.

Binary file not shown.

24
release.sh Executable file
View file

@ -0,0 +1,24 @@
#!/bin/bash
set -e
echo "Starting build process..."
echo "Cleaning up old builds..."
rm -f pm-linux-mac.tar.gz pm-windows.zip
cargo build --release
cargo build --release --target x86_64-pc-windows-gnu
echo "Packaging Linux..."
mkdir -p pm-linux-mac
cp target/release/pm linux-mac-install.sh linux-mac-uninstall.sh pm-linux-mac/
tar -czvf pm-linux-mac.tar.gz pm-linux-mac/
echo "Packaging Windows..."
mkdir -p pm-windows
cp target/x86_64-pc-windows-gnu/release/pm.exe windows-install.ps1 windows-uninstall.ps1 pm-windows/
zip -r pm-windows.zip pm-windows/
rm -rf pm-linux-mac pm-windows
echo "Done! Upload pm-linux-mac.tar.gz and pm-windows.zip to GitHub."

View file

@ -3,9 +3,20 @@ use clap::{Args, Parser, Subcommand};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "PassManager")] #[command(name = "PassManager")]
#[command(about = " A simple password manager CLI", long_about = None)] #[command(about = " A simple password manager CLI", long_about = None)]
#[command(before_help = r#"
_~^~^~_
\) / o o \ (/
'_ - _'
/ '-----' \
"#)]
pub struct Cli { pub struct Cli {
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Option<Commands>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -30,9 +41,6 @@ pub struct AddPass {
pub struct GetPass { pub struct GetPass {
/// The name of the password to get /// The name of the password to get
pub name: String, pub name: String,
/// Copy the password to the clipboard instead of printing it
#[arg(short, long)]
pub copy: bool,
} }
#[derive(Args)] #[derive(Args)]
@ -40,3 +48,38 @@ pub struct DeletePass {
/// The name of the password to delete /// The name of the password to delete
pub name: String, pub name: String,
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_add_command() {
let cli = Cli::try_parse_from(["pm", "add", "github"]).expect("parse add");
match cli.command {
Some(Commands::Add(add)) => assert_eq!(add.name, "github"),
_ => panic!("expected add command"),
}
}
#[test]
fn parses_get_command() {
let cli = Cli::try_parse_from(["pm", "get", "email"]).expect("parse get");
match cli.command {
Some(Commands::Get(get)) => assert_eq!(get.name, "email"),
_ => panic!("expected get command"),
}
}
#[test]
fn rejects_removed_copy_flag() {
let cli = Cli::try_parse_from(["pm", "get", "email", "--copy"]);
assert!(cli.is_err());
}
#[test]
fn parses_list_command() {
let cli = Cli::try_parse_from(["pm", "list"]).expect("parse list");
assert!(matches!(cli.command, Some(Commands::List)));
}
}

View file

@ -11,6 +11,13 @@ use zeroize::Zeroize;
fn main() { fn main() {
let cli: Cli = Cli::parse(); let cli: Cli = Cli::parse();
if cli.command.is_none() {
print_banner();
println!("Welcome to pm - The Rust Password Manager");
println!("Use 'pm --help' to see available commands.");
return;
}
let home_str = std::env::var("HOME") let home_str = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE")) .or_else(|_| std::env::var("USERPROFILE"))
.expect("Could not find home directory"); .expect("Could not find home directory");
@ -76,7 +83,7 @@ fn main() {
let mut passwords = storage::load_passwords(file_str, &encryption_key); let mut passwords = storage::load_passwords(file_str, &encryption_key);
match &cli.command { match &cli.command {
Commands::Add(args) => { Some(Commands::Add(args)) => {
print!("Enter password for {} : ", args.name); print!("Enter password for {} : ", args.name);
std::io::stdout().flush().expect("Flush failed"); std::io::stdout().flush().expect("Flush failed");
let mut p1 = read_password().expect("Failed to read password"); let mut p1 = read_password().expect("Failed to read password");
@ -95,14 +102,14 @@ fn main() {
p1.zeroize(); p1.zeroize();
p2.zeroize(); p2.zeroize();
} }
Commands::Get(args) => match passwords.get(&args.name) { Some(Commands::Get(args)) => match passwords.get(&args.name) {
Some(pw) => println!("Password for {} : {}", args.name, pw), Some(pw) => println!("Password for {} : {}", args.name, pw),
None => println!("No password found for '{}'", args.name), None => println!("No password found for '{}'", args.name),
}, },
Commands::Delete(args) => { Some(Commands::Delete(args)) => {
storage::delete_password(file_str, &args.name, &encryption_key); storage::delete_password(file_str, &args.name, &encryption_key);
} }
Commands::List => { Some(Commands::List) => {
if passwords.is_empty() { if passwords.is_empty() {
println!("No passwords saved yet!"); println!("No passwords saved yet!");
} else { } else {
@ -113,6 +120,7 @@ fn main() {
println!("-----------------------"); println!("-----------------------");
} }
} }
None => unreachable!(),
} }
encryption_key.zeroize(); encryption_key.zeroize();
@ -121,3 +129,18 @@ fn main() {
v.zeroize(); v.zeroize();
} }
} }
fn print_banner() {
let banner = r#"
_~^~^~_
\) / o o \ (/
'_ - _'
/ '-----' \
"#;
println!("{}", banner);
}

View file

@ -118,3 +118,74 @@ pub fn verify_master_password(password: &str, recorded_hash: &str) -> bool {
.verify_password(password.as_bytes(), &parsed_hash) .verify_password(password.as_bytes(), &parsed_hash)
.is_ok() .is_ok()
} }
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_file_path(prefix: &str) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time went backwards")
.as_nanos();
std::env::temp_dir()
.join(format!("pm_test_{}_{}_{}", prefix, std::process::id(), nanos))
.to_string_lossy()
.to_string()
}
#[test]
fn key_derivation_is_deterministic_for_same_input() {
let mut p1 = String::from("master-password");
let mut p2 = String::from("master-password");
let salt = "abc123fixedsalt";
let k1 = derive_key(&mut p1, salt);
let k2 = derive_key(&mut p2, salt);
assert_eq!(k1, k2);
}
#[test]
fn hash_and_verify_master_password_work() {
let password = "correct-horse-battery-staple";
let hash = hash_master_password(password);
assert!(verify_master_password(password, &hash));
assert!(!verify_master_password("wrong-password", &hash));
}
#[test]
fn save_load_and_delete_roundtrip() {
let path = temp_file_path("vault");
let key = [7u8; 32];
let mut data = HashMap::new();
data.insert("email".to_string(), "pw1".to_string());
data.insert("bank".to_string(), "pw2".to_string());
save_passwords(&path, &data, &key);
let loaded = load_passwords(&path, &key);
assert_eq!(loaded, data);
delete_password(&path, "email", &key);
let after_delete = load_passwords(&path, &key);
assert!(!after_delete.contains_key("email"));
assert_eq!(after_delete.get("bank"), Some(&"pw2".to_string()));
let _ = std::fs::remove_file(path);
}
#[test]
fn get_or_create_salt_reuses_existing_value() {
let path = temp_file_path("salt");
let first = get_or_create_salt(&path);
let second = get_or_create_salt(&path);
assert_eq!(first, second);
assert_eq!(first.len(), 32);
let _ = std::fs::remove_file(path);
}
}

12
windows-install.ps1 Normal file
View file

@ -0,0 +1,12 @@
$binary = "pm.exe"
if (Test-Path ".\$binary") {
$binDir = "$HOME\.cargo\bin"
if (!(Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir }
Write-Host "Installing $binary to $binDir..." -ForegroundColor Cyan
Copy-Item ".\$binary" "$binDir"
Write-Host "Done! Restart your terminal and type 'pm'." -ForegroundColor Green
} else {
Write-Host "Error: $binary not found in this folder." -ForegroundColor Red
}

4
windows-uninstall.ps1 Normal file
View file

@ -0,0 +1,4 @@
Write-Host "Uninstalling pm..." -ForegroundColor Cyan
Remove-Item "$HOME\.cargo\bin\pm.exe" -ErrorAction SilentlyContinue
Remove-Item "$HOME\.my_pass_manager" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "pm and all encrypted data have been removed." -ForegroundColor Yellow