Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
230: Building a Command-Line Password Manager
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".
There are no comments for now.