Skip to Content
Course content

230: Building a Command-Line Password Manager

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of a command-line password manager like a physical fireproof safe in your basement. To get anything out of it, you need a master key. Once you're inside, you don't just find a pile of loose papers; you find a set of labeled envelopes. If you want to find your "Bank" password, you look for the envelope labeled "Bank," pull out the slip of paper, and read it. If you want to change a password, you replace the slip of paper in that specific envelope. If you lose the master key, however, the safe is just a heavy, useless box of metal.

In Rust, we map this exactly. The "master key" is our encryption key, derived from a user-provided passphrase. The "safe" is a file on your hard drive, but instead of being plain text, it's a blob of encrypted bytes. The "labeled envelopes" are a HashMap<String, String>, where the key is the service name and the value is the password. We use serde to turn that HashMap into a format we can encrypt, and we use a crate like aes-gcm to handle the actual locking and unlocking.

Handling the Master Key

You can't just use a raw string as an encryption key; AES requires a specific byte length. I usually recommend using a Key Derivation Function (KDF) like PBKDF2 or Argon2. This takes the user's human-readable password and "stretches" it into a cryptographically strong key. If you just cast a string to bytes, you're leaving the door unlocked for anyone with a basic brute-force tool.

use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;

fn derive_key(passphrase: &str, salt: &[u8]) -> [u8; 32] {
    let mut key = [0u8; 32];
    pbkdf2_hmac::(passphrase.as_bytes(), salt, 100_000, &mut key);
    key
}

Locking the Envelopes with AES-GCM

I prefer AES-GCM (Galois/Counter Mode) because it provides "authenticated encryption." This means it doesn't just hide the data; it ensures the data hasn't been tampered with. If a single bit of your encrypted password file changes, the decryption process will fail rather than returning garbage data. This prevents a whole class of attacks that I've seen plague older, simpler encryption implementations.

The workflow looks like this: you serialize your HashMap to JSON, encrypt that JSON string using the derived key and a unique nonce (a "number used once"), and then save both the nonce and the ciphertext to a file.

use aes_gcm::{Aes256Gcm, Key, Nonce, KeyInit, aead::Aead};

fn encrypt_vault(data: &[u8], key_bytes: &[u8; 32]) -> Vec<u8> {
    let key = Key::from_slice(key_bytes);
    let cipher = Aes256Gcm::new(key);
    let nonce = Nonce::from_slice(b"unique nonce 12"); // In real life, generate this randomly!
    
    cipher.encrypt(nonce, data).expect("encryption failure!")
}

Managing the Vault State

The tricky part of a CLI tool is that it's short-lived. You run it, it does one thing, and it exits. This means every time you want to add a new password, you have to: read the whole encrypted file, decrypt it into memory, update the HashMap, re-encrypt everything, and overwrite the file. It feels inefficient, but for a password manager where you might have a few hundred entries, the performance hit is negligible compared to the security benefits of not keeping the vault "open" in the background.

  • Adding: Decrypt $\rightarrow$ map.insert(service, pass) $\rightarrow$ Encrypt $\rightarrow$ Save.
  • Retrieving: Decrypt $\rightarrow$ map.get(service) $\rightarrow$ Print to console.
  • Listing: Decrypt $\rightarrow$ Iterate over map.keys() $\rightarrow$ Print list.

One thing I've learned the hard way: never print the master passphrase back to the console. Use a crate like rpassword to read the password from the terminal without echoing the characters to the screen. There's nothing worse than a "secure" app that leaves your master password visible in the terminal scrollback.




📋 Practical Task

Implement the Vault-Search Functionality

You have the basic logic for adding and retrieving specific passwords. Now, you need to implement a "search" feature. Create a function called search_vault that takes a search query string and the decrypted HashMap<String, String>. The function should return a list of all service names that contain the query string (case-insensitive). For example, if the user searches for "google", it should return both "Google Mail" and "Google Cloud".

Rating
0 0

There are no comments for now.

to be the first to leave a comment.