Encrypted Notes CLI
Build a command-line notes app in Rust that keeps every note encrypted on disk. You unlock the vault with a password, then add, list, read, edit, delete and search your notes, for example: notes add Groceries, or notes search milk. Notes are encrypted with ChaCha20-Poly1305, using a key derived from your password with Argon2, so anyone who copies the files sees only unreadable data. Search decrypts notes in memory only; plaintext is never written to disk. You'll practice designing a CLI with subcommands, serializing data with Serde, async file I/O with Tokio, and using well-reviewed cryptography crates correctly instead of inventing your own.
- Estimate
- ~14.5h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
Tokio · Serde
Project roadmap
- 01
Set up the CLI
~1.5hCreate a binary crate and add: tokio (with the fs, macros and rt-multi-thread features), serde, serde_json, clap with the derive feature, chacha20poly1305, argon2, rand and rpassword. Define these subcommands with clap: init, add <title>, list, show <id>, edit <id>, delete <id> and search <query>. Use a #[tokio::main] main that matches the subcommand and prints a placeholder for each. Done when: notes --help lists all seven subcommands.
- 02
Design the storage format
~2hStore the vault in a directory (default ~/.notes-vault, changeable with --vault). Define a Serde Note struct with id, title, body, created_at and updated_at, and a vault.json file holding the Argon2 salt and a format version. Each note is saved as <id>.enc. Write async load and save functions with tokio::fs. Done when: the structs serialize to JSON, and notes init creates the vault directory and vault.json.
- 03
Derive the key from a password
~2hIn init, ask for the password twice with rpassword (never echo it or accept it as an argument), generate a random 16-byte salt, and derive a 32-byte key with Argon2id. Save only the salt, never the password or the key. Every other command asks once and derives the same key. To detect a wrong password, store an encrypted known value in vault.json and check that it decrypts. Done when: init creates a vault and a wrong password is rejected clearly.
- 04
Encrypt and decrypt notes
~2.5hSerialize a Note to JSON and encrypt it with ChaCha20-Poly1305 using the derived key and a new random 12-byte nonce for every save. Write the nonce followed by the ciphertext to <id>.enc. Decrypting reads the nonce back, decrypts and deserializes. A modified or corrupted file must return an error, never garbage. Done when: add followed by show returns the same note, and the .enc file contains no readable text.
- 05
Implement the commands
~2.5hImplement add (title from the argument, body read from stdin until Ctrl-D), list (id, title and last update, newest first), show, edit (replaces the body with new text read from stdin and updates updated_at) and delete (asks for confirmation first). Plaintext must never be written to disk, so don't use temporary files. Done when: all five commands work end to end on a real vault.
- 06
Add search
~2hImplement search <query>: decrypt every note in memory and match the query case-insensitively against the title and body. Print each match with its id, title and a short snippet around the first occurrence. Decrypt notes concurrently with Tokio tasks. Done when: searching for a word that appears in two out of five notes returns exactly those two.
- 07
Write tests
~2hWrite tests for: key derivation gives the same key for the same password and salt, encrypting then decrypting returns the original note, a modified ciphertext fails to decrypt, a wrong password is rejected, and search finds the right notes. Use a temporary directory (the tempfile crate) so tests never touch a real vault. Done when: cargo test passes.
Resources
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack