Skip to main content

Command Palette

Search for a command to run...

EntroPass Postmortem: What I Learned Shipping a Cryptography-Heavy Desktop App

Updated
9 min read
EntroPass Postmortem: What I Learned Shipping a Cryptography-Heavy Desktop App

I shipped my first real project a few weeks ago. Not a tutorial clone, not a "hello world" with a button—an actual desktop password manager with AES-256-GCM encryption, PBKDF2 key derivation, BCrypt password hashing, and a fully encrypted local SQLite database.

It's called EntroPass. You can find the GitHub release here.

Before I go any further, this is a portfolio piece, not a product. I'm not competing with Bitwarden. The framing matters because it shaped every decision I made—what I prioritized, what I deferred, and what "done" actually meant. I'll get into those tradeoffs honestly.

This is my postmortem.


Why I Built It

The starting point was my family.

At home, passwords get forgotten constantly — email credentials, account logins, things that matter. Earlier this year, a few family members got swindled and hacked. Not because they were careless, but because they didn't have a system. Credentials were reused, written down loosely, or just forgotten entirely. I wanted to fix that with something I could actually hand to them and say: this is safe, it works offline, and nobody can reach it through the internet.

The offline-first requirement wasn't a technical preference — it came directly from their situation. Cloud-based password managers are fine in theory, but they're another account to manage, another potential breach surface, and another thing that requires trust in a third party. For people who'd already been burned, that wasn't good enough. A local vault that never phones home was the only model that made sense.

That said, I didn't pick a password manager at random. I'm targeting embedded security and medical IoT, and the primitives here—authenticated encryption, key derivation, and data-at-rest protection—are the same ones I'll need to implement in environments with far less room for error. Building this for my family was the motivation. Understanding those primitives in practice was the other half of the point.


Key Technical Decisions

AES-256-GCM over simpler encryption

AES-256-GCM is authenticated encryption—it doesn't just encrypt your data; it also produces an authentication tag that verifies the ciphertext hasn't been tampered with. A simpler approach like AES-CBC can decrypt corrupted or modified data without ever telling you something went wrong. For a security tool, silently returning garbage is worse than failing loudly.

PBKDF2 for key derivation

You can't use a password directly as an encryption key. Passwords are short, predictable, and low-entropy. PBKDF2 runs the password through a hash function thousands of times with a random salt, producing a high-entropy key while making brute-force attacks expensive. The salt ensures that two users with the same password end up with completely different keys.

BCrypt for the master password

This might look redundant — why hash the master password separately if PBKDF2 already handles it? The answer is that they serve different purposes. PBKDF2 derives the encryption key from the password. BCrypt stores a verifiable credential—a way to check "is this the right password?" without storing the password itself or the encryption key. They're different problems.

SQLite for local-first storage

Local-first was a deliberate choice. No cloud sync, no account registration, no server to breach. The tradeoff is obvious — you lose your vault if you lose your device and haven't backed it up. That's a real limitation. For this use case (a portfolio project with a defined threat model), it was the right call.

The TextField limitation—and why I'm being upfront about it

JavaFX's TextField component keeps the master password in memory as a String object. Java strings are immutable and not garbage-collected on demand, which means the plaintext password may sit in the JVM heap longer than you'd want. A more secure implementation would use a char[] that can be explicitly zeroed after use.

I know about this gap. I didn't close it in v1.0. The reason is that it falls outside the threat model I was actually defending against—more on that next.


What I Was Defending Against — and What I Wasn't

The threat I designed around: data-at-rest compromise. An attacker who gets access to your file system—through malware, physical access, or a stolen drive—but does not have your master password. That's what encrypting the entire SQLite database defends against. Your vault file is useless without the key, and the key never gets stored anywhere.

What this model explicitly does not defend against:

  • An attacker with active process memory access (e.g., a sophisticated keylogger or memory scraper)

  • Someone who already knows your master password

  • The TextField residency issue described above, which is relevant under a memory-access threat

Knowing the boundary matters. It's the difference between "I added encryption" and "I know what my encryption actually protects." The TextField limitation isn't an oversight—it's out of scope for the threat I was solving.


The Query Problem: What Happens When Your Database Can't Read Its Own Data

This was the hardest part of the project—and it had nothing to do with cryptography.

Originally, the search feature was simple: the user types, the app queries SQLite, and results come back. Straightforward.

Then I encrypted the database. The whole thing. Every row, every field.

SQLite can't query ciphertext. When you ask for passwords containing "github," SQLite just sees random bytes. It has no idea what's in there. The query returns nothing, or it returns everything, neither of which is correct.

The fix sounds simple in retrospect: on vault unlock, decrypt all entries into an in-memory list. Search operates on that list, never the database. When you close the app, the list is gone.

But getting there required rethinking the entire data flow. The app had been built around the assumption that SQLite was the source of truth at runtime. Encrypting the database made the database opaque at runtime — it's now storage, not a queryable data layer. Those are different things, and refactoring that distinction out touched more of the codebase than I expected.

The reason this was harder than the cryptography implementation itself: cryptography is well-specified. There are clear right answers. "How do I restructure my runtime data layer while keeping the UI working and not breaking the encryption" is not a well-specified problem. You figure it out by breaking things.

There's also a pattern here worth naming. The in-memory approach—decrypt into a controlled buffer, operate on plaintext, and discard on close—is the same pattern you'd use on an embedded system handling sensitive data. On a microcontroller, you don't have the luxury of querying a database. You decrypt what you need, use it, and clear it. EntroPass ended up there accidentally, but it's the right instinct.


What Went Wrong / What I'd Do Differently

No written threat model before building

My individual cryptographic decisions were sound, but I arrived at them through instinct rather than a structured analysis. I knew I was defending against "someone getting into my computer and grabbing the vault file"—but I never wrote that down, never enumerated the attack surface, and never formally decided what was in scope.

If I'd done that upfront, I would have made the same implementation choices, but I would have understood why I made them. The TextField limitation would have been a conscious, documented decision rather than something I realized was a gap later.

In security work, a decision you can't explain is a liability.

Fixed window size

EntroPass runs at 1316×900. That's it. No resizing, no responsive layout. I made this call early to avoid the complexity of dynamic layout in JavaFX, and at the time it seemed reasonable.

It costs you: the app won't run comfortably on smaller displays, feels rigid, and screams "I didn't finish this." For a portfolio piece targeting technical reviewers, it's acceptable. For anything beyond that, it's the first thing to fix.

Scope creep during "finishing"

The hardest moment in any project isn't starting—it's the purgatory between "functionally complete" and "actually shipped." I spent more time in that zone than I want to admit, adding small things, refining things that didn't need refining, and finding reasons not to call it done.

The honest version: perfectionism dressed up as diligence. Shipping is a skill. I'm practicing it.

Earlier cross-environment testing

I tested the JPackaged build on a second laptop, and it worked. But I did that late in the process. If it hadn't worked, I would have been debugging environment issues under deadline pressure. Test your build artifact early on a machine that isn't yours.


What I Learned

How to actually test cryptographic code

I wrote unit tests for the encryption layer, verifying that encrypt/decrypt round-trips produced correct output, that different keys produced different ciphertext, and that tampered ciphertext failed authentication. That's correctness testing.

Cross-environment testing on the JPackaged app is a different thing entirely. That's portability testing—does the runtime bundle work on a machine without my dev setup, without my JDK, without my file paths? Both matter. They catch different classes of failure.

Cryptography in practice vs. in theory

In theory, you pick the right algorithm and you're done. In practice, you have to manage keys, decide where plaintext can live and for how long, handle the failure modes, and make sure every component that touches sensitive data treats it as such. The cryptography itself was the easy part. The architecture around it was not.

The gap between "functionally complete" and "actually shipped"

Every project has a version that works on your machine in your IDE. Getting from there to a packaged, versioned, publicly releasable artifact is a separate skill with its own failure modes. I know that now in a way I didn't before I did it.


What's Next

Embedded systems. I have an Arduino Uno, a Mega, a couple of I2C LCDs, and a SIM900A GSM module on my desk. The plan is to start from C fundamentals and work up—the goal isn't to build toys; it's to eventually do the same kind of deliberate, security-conscious work I did here, but on hardware where the margin for error is smaller.

The reason EntroPass matters to that goal isn't the password manager. It's the habit of thinking: what am I defending against, where does sensitive data live, and what happens when my assumptions turn out to be wrong?

That question scales.

More from this blog

Building in Public

2 posts

A CS student developer documenting the process of building real things—from a Java password manager to IoT. Written honestly, including the parts that didn't work.