Building a Password Manager
The password manager I built 20 times.
The password manager I built 20 times.
Building a password manager is a rite of passage for security-focused developers. It is a project where high-level architectural decisions directly collide with the friction of daily use. Over the last decade, I have built, scrapped, and rebuilt my personal vault three times.
Each version represented a different philosophy of trust. Here is what I learned about why "perfect" security often fails the user—and where I finally landed.
About ten years ago, I built my first custom password manager. At the time, it was a playground for experimental browser features. I used the early Chrome Crypto APIs for client-side RSA encryption and handled binary arrays directly in the browser—a rarity for web apps back then.
The security model was intentionally rigid:
Ephemeral Encryption: Each backend session was encrypted via a unique RSA public/private key pair.
Forced Session Expirations: Users could select a session duration—5, 20, or 60 minutes. Only 1 session per account is allowed at any given moment.
Session Oddness: If you didn't explicitly log out, the session remained "active" on the server until the timer expired. This meant if you had a 1-hour session and closed your tab, you couldn't log back in from anywhere until that hour was up.
It was a revolutionary end-to-end encrypted tool for its time, and I used it for years. But as my needs grew—requiring TOTP, MFA, and the ability to change a master password—the cracks in the "experimental" foundation began to show.
When I set out to build the successor, I had a new goal: to prove that production-ready, highly secure code could be developed using LLMs. I wanted to open-source the project and host it publicly, which meant moving from a "me-only" tool to a robust system capable of handling self-registration.
This led me down two failed paths:
Attempt 1: The Usability Wall
I doubled down on zero-knowledge encryption. However, the UX was a disaster. Accessing a credential required "unlocking the vault" by re-entering a vault password (different from the account password) periodically. Changing that password required a convoluted process of decrypting every single record and re-encrypting them with the new key. It was technically secure but practically exhausting. It was a strange implementation that didn't make a ton of sense.
Attempt 2: The Pseudo-Security Trap
In the second iteration, I tried to simplify, but the system became a paradox. It claimed "zero-knowledge," yet it stored the encryption keys in the server-side user session to facilitate easier API requests. This not only violated the principle of zero-knowledge but also increased the attack surface by sending the key over the network repeatedly.
Scrapping those iterations led to a core realization: Security boils down to trust. I had to ask myself:
Do I trust HTTPS? If the answer is no, the web is already broken for me.
Do I trust the Client or the Server? Many developers insist on client-side encryption, but end-user devices (phones and laptops) are far more likely to be compromised by malware than a hardened cloud server.
Corner-Cases: Interesting issues arise when the server cannot encrypt or decrypt credentials. You force the front-end to handle processing a batch of credentials if you allow them to change the password to their password manager. One thing I do personally is build a password protected ZIP file containing a backup of all of my passwords in plain text. Should the worst happen, I can always grab my last backup and get the passwords I need. I can't create this ZIP file and allow you to download it without access to decrypt the passwords.
For the final version, I prioritized usability and hardened server-side security over the theoretical purity of client-side encryption.
The current version handles encryption and decryption on the server. To protect this, I implemented:
Aggressive Rate Limiting: To prevent brute-force attempts.
Multi-Factor Authentication: Built-in support for TOTP and Email MFA.
Immutable Backups: The database is backed up daily to an S3 bucket with Object Versioning. The server permissions are restricted to PutObject only; even if the server is hacked, a malicious actor cannot delete or overwrite previous backups. They can only add new versions, allowing for perfect recovery.
On the frontend, I experimented with a unique architectural pattern: React + React Router with almost no useState. By leaning heavily into React Router’s loader and action methods, the application treats the URL and form submissions as the primary source of truth. The input fields themselves hold the state. This minimized the complexity of the code, making the application feel incredibly sleek, responsive, and easy to maintain.
I decided on a closed-source solution. Partly because if a vulnerability were discovered it would compromise the entire project- including my own passwords. If it's closed source, then identifying a potential attack vector becomes much more difficult. Lingering security threats are less likely to be caught, but also less likely to be used. It's a trade off between transparency and trade secrets.
The journey from a paranoid, experimental RSA tool to a streamlined, server-encrypted React app taught me that security isn't just about the strength of the algorithm—it's about the reliability of the system as a whole. By focusing on a trustworthy server environment and a frictionless user experience, I finally built a vault I’m happy to use every day.