RustVault is a password vault and privacy toolkit written in Rust that runs only on your own machine. Building it meant finding a deadlock that locked out the owner after five logins, a backup screen that made up its numbers, and a password reset that silently destroyed every saved entry. All three are fixed, and this is the story of how.
Why build your own
Your password manager is the one app that holds the keys to every other app. Most of them keep that vault on somebody else’s server, and you take their word that it is safe.
RustVault takes the opposite bet. The vault, the web interface and the browser protection all run on your own computer. The web interface listens on 127.0.0.1 only, so nothing else on your network can even knock. No account, no subscription, no sync server to breach.
The trade-off is real: if you lose the machine and the backups, nobody can help you. That is exactly why backups and recovery got so much attention below.
What it does today
One Rust binary runs a terminal menu and a web interface over the same vault, backed by PostgreSQL or a local SQLite file.
| Feature | What it does | Where |
|---|---|---|
| Encrypted vault | AES-256-GCM per entry, key derived with Argon2id (64 MB, 3 passes) | CLI, web |
| Recovery key | Reset a forgotten master password without losing a single entry | CLI, web |
| Password health | Flags weak, reused, year-old and undecryptable entries; never shows the passwords | Web |
| Automatic backups | Made in the background at unlock when the last one is too old; old automatic ones pruned | CLI, web |
| Lockout protection | Five wrong passwords lock unlocking for 10 minutes, doubling on repeat | CLI, web |
| Imports | Firefox, CSV and JSON | CLI, web |
| Firefox hardening | Standard or Strict profiles: anti-fingerprinting, WebRTC IP leak protection, tracking pings off | Web |
| JavaScript kill switch | Turn JavaScript off for a whole Firefox profile | Web |
| Script Guard | Browser extension that blocks every page script until you approve it | Firefox |
| Tailscale panel | Shows which exit node your traffic leaves through and switches it | Web |
| Sandboxed browsers | Launch, list and stop browsers inside Firejail | CLI, web |
| Encrypted DNS | DNS-over-HTTPS through Cloudflare, Quad9, AdGuard, CleanBrowsing, Google or your own | CLI |
It installs as a rustvault command that works from any folder, plus rustvault-web, which starts the server if needed and opens the browser.
How it fits together
The terminal menu and the web interface share one vault core, and the API server only answers the vault’s own page on 127.0.0.1. The only path to the internet runs through Firefox and your Tailscale exit node; the vault itself only connects out when you ask it to, such as downloading a browser extension or sending a recovery code.
The bugs that tried to kill it
Every one of these compiled cleanly and looked fine in a quick test. Most only showed up when the app was used the way a real person uses it: unlocking several times, restarting, forgetting a password.
| Bug | What it did to you | Fix |
|---|---|---|
| Rate limiter deadlock | After five unlocks in five minutes, even correct ones, every unlock hung forever until restart. The lockout code took a lock it already held. | Release the lock before waiting; count only wrong passwords; regression test that hung on the old code |
| Lockouts that never stuck | The blocked state was saved, then overwritten a line later by the unblocked copy | Save the blocked entry, return 429 with a clear message |
| Reset wiped the vault | Recovering a forgotten password made every saved entry permanently unreadable | Envelope encryption with a recovery key (next section) |
| Make-believe backup status | The status screen always said 5 backups at “/path/to/backups”, last one made “now” | Real counts from the backup folder |
| Backups that ate each other | Two unlocks in the same second made two backups with the same file name; one overwrote the other | One automatic backup at a time; unique names |
| SQLite crash on relaunch | A fresh local vault worked once, then panicked on every start after | Read the profile id as text, as it is stored |
| Wrong table on Postgres | An early setup step created the sessions table with the wrong columns, so viewing or stopping sessions crashed the app | Detect and rebuild the table, keeping any rows |
| Unlock keys left on disk | Every unlock wrote the decryption key to a file that was never deleted | Owner-only files, deleted when the session expires |
| Any website could call the API | Every response said Access-Control-Allow-Origin: * |
Only the vault’s own page may call it |
| Recovery token in the response | The password-recovery endpoint returned its own secret token | Removed |
The lesson: test the second launch, the fifth login and the bad day, not just the happy path.
Forgetting your password no longer costs everything
The old design encrypted every entry with a key made from the master password. Forget the password and the key is gone, so a “reset” could only set a new password over a vault it could no longer read.
Now every entry is encrypted with a random data key, and that key is stored twice, each copy locked a different way:
- Locked by the master password. Unlocking opens this copy.
- Locked by a recovery key. A 28-character code like
PVJY-QXAG-Z6AN-…, shown once for you to write down and never stored readable.
What that buys you:
- Changing the master password re-locks one small key instead of re-encrypting the whole vault.
- A forgotten password plus the recovery key gives a new password with every entry intact.
- Existing vaults upgrade themselves on the next unlock. The old password-derived key simply becomes the data key, so not one entry is re-encrypted.
Wrong recovery keys count toward the same lockout as wrong passwords, and each guess costs a full Argon2id derivation.
Script Guard: every script has to ask
Modern pages run dozens of scripts you never asked for. Script Guard blocks all of them by default, like a text browser, then lets you decide script by script.
Open the shield on any page and each script gets a verdict with its reasons:
| Verdict | Typical reason |
|---|---|
| Recommended | From this site, or a public library CDN, with nothing suspicious in the code |
| Caution | Third party with an unknown purpose, or a single fingerprinting call |
| Don’t allow | Known tracker or ad network; several fingerprinting techniques combined; records typing or mouse movement and sends it out; looks like a crypto-miner; loads a known tracker |
You can allow the scripts you pick, the recommended ones, or everything, for this visit, until Firefox closes, or always, and only on that one site. Under the hood it rewrites the page’s security policy to list exactly the approved script addresses and the fingerprints (SHA-256 hashes) of approved inline code. Nothing else runs.
The analysis happens inside the browser. External scripts are only downloaded for inspection when you press Analyze, and without your cookies. The extension is signed by Mozilla as a private, unlisted add-on, so it installs in regular Firefox.
What still is not good enough
A vault that only lists its wins is marketing. These are the weak spots as of today.
- The unlocked key still touches disk. Session files are owner-only and expire, but the key should live only in memory and be wiped on lock.
- Secrets linger in memory. Decrypted passwords are ordinary strings, not zeroed after use, and memory is not locked against swap.
- Recovery by email or security questions still loses entries. Only the recovery key keeps them. Those older methods should either wrap the data key too or be retired.
- Chrome and Edge imports do not work on Linux or macOS. Their password decryption is not implemented there.
- 156 compiler warnings remain. Mostly unused code, but noise hides the next real bug.
- Thin test coverage. 24 Rust tests and two extension test suites, concentrated on the parts that broke. The web interface has none.
- No continuous integration. Tests run when someone remembers to run them.
- Script Guard approvals live only in the browser. The vault’s web interface cannot see or manage them yet.
- Strict mode is blunt. Blocking every cookie signs you out of everything, which pushes people back to weaker settings.
What comes next
Ordered by how much safety each one buys for the work it takes. The first three close the gaps above that matter most.
- Keep the key in memory only. Hold the unlocked data key in locked, zeroed memory; nothing on disk, gone the moment you lock.
- Wrap the data key for every recovery method. Email and security-question recovery stop being destructive.
- Continuous integration. Every push builds, runs the Rust and extension tests, and fails on new warnings.
- Breach checks without leaking. Compare passwords against known breaches using only the first five characters of their hash (k-anonymity), so the passwords never leave the machine.
- One-time codes. Store TOTP secrets and show the six-digit codes, so the vault replaces an authenticator app.
- Script Guard in the vault. Review and edit per-site approvals from the web interface, and back them up with the vault.
- Reach it from your phone, privately. Serve the web interface over Tailscale only, so your own devices can use it and nothing on the open internet can.
- Smarter Strict mode. Block third-party cookies everywhere, but keep first-party logins for sites you choose.
- Packaging. An Arch (AUR) package and a .deb, with checksums, instead of building from source.
- Fuzz the parsers. Imports and backup restore read untrusted files; fuzzing them finds crashes before an attacker does.
The bottom line
RustVault is not a polished commercial product, and it does not pretend to be. It is a vault you can read end to end, that never phones home, and whose worst bugs have been found, fixed and pinned down with tests. The next stretch is about making the secure path the easy one.