How I Tried to Break My Own Encrypted Journaling App — Ten Times
Why I ran a ten-round penetration test against my own privacy-first journaling app — the attack lab, the AI-orchestrated workflow, and the rules of engagement. Part 1 of a four-part series.
Contents
An authorized penetration test of MoodHaven Journal, run across ten iterative rounds with a Kali attack box, Windows and Ubuntu victims, and Claude Code as the orchestrator. 65+ targets tested; 41 real vulnerabilities confirmed through the seventh round, all 41 fixed; an eighth round that found the flagship encryption feature had never actually engaged — now verified working on the installed Windows build; a ninth round that turned a custom attack tool on the app and found six more; and a tenth round that red-teamed the ninth round’s fixes themselves, found a bug in each one, fixed both, and then — after an independent verification hunt came back clean — closed the campaign.
The short version (read this part even if you read nothing else)
I build MoodHaven Journal, a private journaling app that keeps everything on your own computer — no accounts, no cloud, no servers reading your entries. The whole point of the app is privacy, so “trust me, it’s secure” was never going to be good enough. I needed to actually try to break it.
So I built a small attack lab — a dedicated attacker machine running Kali Linux, plus two “victim” machines (a Windows 11 PC and an Ubuntu PC) running the real, installed app — and ran a structured penetration test against it, the same kind of adversarial testing a security firm would do. I did it not once but ten times in a row, fixing what I found between each round and then attacking the fixed version again — and the final round attacked the previous round’s fixes specifically. And to do it properly I had to build my own instrumentation — including a from-scratch reimplementation of the app’s own encrypted sync protocol — because off-the-shelf scanners simply can’t speak to a custom local-first app like this. (There’s a whole section on the tools further down; building them turned out to be half the work.)
Here’s the part that matters:
- 65+ specific attacks were attempted. Each one was a real probe, not a checklist tick.
- 41 of them found a genuine vulnerability through the seventh round. Some were serious (a way to read your data, a way to lose your edits silently). Some were small.
- All 41 were fixed, with the code changes tied to specific releases and pull requests.
- The eighth round found the big one: a flagship “encrypted at rest” feature that had never actually engaged. That fix is now verified working end-to-end on the installed Windows build — fresh setup completes the migration, the database is genuinely encrypted on disk, the app unlocks cleanly, and the sync server starts. (Re-validation on Linux is still pending, and I say so wherever it comes up.)
- The ninth round turned my own custom attack tool on the app and found six more issues — including one that surfaced live during the attack, not in code review. All six are fixed and committed, proven by standalone reproductions and regression tests.
- The tenth round did the thing that finally let me stop: it red-teamed the ninth round’s fixes themselves — because a fix is new, untested code — and found a real bug in each of the two trickiest ones. Both were fixed, and then an independent verification hunt came back clean (no new high-severity finding, every happy path intact). That’s what closed the campaign.
- The remaining attacks failed — which is its own kind of good news. They proved that defenses I’d designed actually held up under attack.
The most interesting finding wasn’t a single bug. It was a pattern: round after round — including the most serious findings — uncovered a new problem that an earlier round’s fix had accidentally introduced. The eighth round delivered the sharpest example of all: a flagship “encrypted at rest” feature, added to fix an earlier round’s “your database is readable” finding, that had never actually engaged in any build, on any operating system. The ninth round kept the pattern alive — the very fixes for the eighth round’s encryption work introduced two new data-loss-class bugs of their own. And the tenth round proved the pattern was the method, not an accident: I deliberately attacked the ninth round’s two fixes, and each one had a bug hiding in it. Fixing a security issue is itself a code change, and any code change is new ground that hasn’t been attacked yet. That insight — that you can’t fix your way to “done” in one pass, and that a fix’s own existence is not evidence it works — is the spine of this whole project.
It also tells you how a project like this ends honestly. You don’t reach zero bugs; you converge. The externally-reachable attack surface — what someone on your network or hitting your app from outside can touch — went to zero. The bugs that remained were local-access and lockout-class: they need a thief who already has your unlocked machine or a device you once trusted, and the worst they do is deny you access, not hand your journal to anyone. And the one invariant the whole app exists to protect — that nobody, in any round, could read journal content they weren’t meant to — held across all ten rounds. When the tenth round’s fresh hunt for new high-severity bugs came back empty while the happy paths still worked, that was the signal the loop had terminated, not a failure to keep finding things.
If you’re a hiring manager or a non-technical reader, you can stop here with a fair picture: this was rigorous, honest, finish-the-job security work, done with a modern AI-assisted workflow on bespoke tooling I built for the job — including the humbling discovery that a feature I’d shipped and believed in was silently inert, and a final round that proved the test loop terminates when you keep attacking your own fixes until a clean pass comes back. The rest of this post is the detailed version for people who want to see the actual vulnerabilities, the tools, and how the testing worked. The plain-language story stands on its own; the deep-dives below are folded away behind “For the technically inclined” toggles, so you can open exactly as much depth as you want and no more.
Why bother attacking your own app?
I designed MoodHaven to be secure from day one: every journal entry is encrypted on your device with AES-256-GCM before it ever touches disk, the encryption key is derived from your password and never stored, and there’s no server in the middle that could be breached.
But there’s a trap in that sentence. I designed it to be secure. Reading your own code and nodding along is not the same thing as attacking it. You see what you intended to build, not what you actually built. The gap between those two is exactly where vulnerabilities live.
And I’ll be straight about something a fair reader will think: a more careful developer might have caught some of these in review before they ever shipped. Maybe. But that’s the thing I keep relearning — being able to describe what you want built, clearly and even correctly, is not the same as knowing it works. That gap is wider in an AI-assisted workflow, not narrower: you can produce a lot of plausible, confident, wrong code fast. The flagship encryption feature is the proof — designed right, reviewed, documented, believed, and it had never once run. Describing intent is cheap. Verifying it on a real machine is the work, and it’s the part I’m showing you. I’d rather tell you exactly what I proved and what I haven’t than pretend the first draft was the finished product.
After I shipped a major peer-to-peer sync feature — letting two of your own devices exchange entries directly over your home network, with no cloud — that gap started to bother me. Sync means a network protocol, a pairing handshake, cryptographic key exchange — a lot of new surface. I’d reasoned about it carefully. I hadn’t attacked it.
So I decided to treat my own app the way an external security auditor would: assume nothing, try everything, and only believe a defense works after I’ve failed to break it.
How the testing actually worked
This is the part I’m most proud of from an engineering standpoint, so I want to be concrete about the setup.
The lab
My laptop (Linux) — Claude Code, acting as the test orchestrator
│
├── attacker: Kali Linux — the standard penetration-testing toolkit
├── victim 1: Windows 11 — the real app, installed from the production installer
└── victim 2: Ubuntu — same app, different OS, to catch platform-specific bugs
The attacker and victim machines share a local network, because a lot of the most interesting tests target the peer-sync feature, which is a real network protocol. I drove everything from one terminal on my laptop.
A few deliberate choices here matter:
- I tested the installed app, not a dev build. The thing I attacked is the same artifact a real user downloads. Several of the most important findings only exist in the packaged build (file permissions, encryption-at-rest, OS-level key storage).
- Two operating systems, on purpose. Some of the worst bugs in this whole campaign were Windows-only — for example, a file-locking quirk that silently broke the database encryption migration on Windows while working perfectly on Linux. If I’d only tested one OS, I’d have shipped a serious bug believing it was fixed.
Why use an AI orchestrator (Claude Code)
The novel part of this workflow is that I used Claude Code as the orchestrator running the campaign. In plain terms, that meant the AI could:
- Read the entire codebase and flag plausible weak points before any machine was touched.
- Connect to the attacker and victim machines, write small exploit scripts, run real attack tools, and read back the results.
- Run several independent investigations in parallel — for example, one thread auditing the network protocol while another checked how encryption keys are wiped from memory and a third reviewed access-control rules — then pull the findings back together.
- Write the actual fix, in the same session, while the full context of the bug was still in view.
What it could not do is just as important, and I’ll be honest about it:
- It can’t click around a remote app’s window. Tests that needed the app to be in a specific on-screen state (a pairing dialog open, the lock screen showing) had to be set up by hand. A couple of tests are still marked “deferred” for exactly this reason.
- It refuses to actually steal secrets, even in authorized testing. When a test would have involved exfiltrating a real encryption key, the safety system blocked it regardless of my stated intent. That’s a sensible guardrail — but it means “the key is sitting in memory” had to be confirmed by other means rather than by walking out the door with it.
So this was not “AI does security for you.” It was AI as a tireless, fast, well-read collaborator that kept attack state straight across multi-day sessions, with a human keeping it pointed at the right targets and making the judgment calls.
For the technically inclined — the method, one sentence per step
- Read first. Static analysis of the code to generate candidate weaknesses.
- Actually attack each candidate — a real exploit attempt, not just “the code looks risky.”
- Watch the wire. Capture real network traffic; it reveals things code review never will.
- Look in memory. Dump the running process in locked, unlocked, and just-locked states and search for key material.
- Distinguish “the risky pattern exists” from “the attack actually works.” Many candidates were already defended.
- Fix in the same session, while the context is warm.
- Run the whole thing again against the fixed build — because fixes introduce new code, and new code hasn’t been attacked.
The lab connectivity used Tailscale for remote SSH from the laptop to both victims, with the two victims also sharing a 192.168.1.0/24 LAN adapter for the direct network attacks the peer-sync tests required.
Where this series goes
That’s the setup: the why, the lab, and the method. The rest of this series is the evidence.
- Part 2 — the findings. The confirmed vulnerabilities in plain language: a database you could read without the password, edits that silently vanished, encryption keys leaking across the LAN — and the critical bugs my own fixes introduced.
- Part 3 — the feature that never turned on. The eighth round’s gut-punch — a flagship “encrypted at rest” feature that had never actually engaged on any build, on any OS — plus the rounds where a custom attack tool started finding bugs on its own.
- Part 4 — the tooling and the lessons. The bespoke instrumentation it took (including a from-scratch clone of the app’s own sync protocol), the attacks that failed, and what ten rounds of this taught me that outlives the app.
The “Part 1 of 4” box above will light up each part as it publishes.
Newsletter
Liked this? Get the next one.
One essay or short note every other week — privacy-first software, AI, security, and the occasional dispatch from the trail. No filler.
More writing
Ten Rounds of Breaking My Own App: The Tools, and the Lessons
The bespoke tooling that made a ten-round self-pentest possible, the attacks that failed, the full round-by-round scoreboard, and the lessons that outlive the app. Part 4 of a four-part series.
ReadThe Flagship Encryption Feature That Never Actually Turned On
The eighth round of a self-pentest found MoodHaven's flagship 'encrypted at rest' feature had never engaged on any build — then a custom attack tool and a fix-the-fixes round closed the campaign. Part 3 of a four-part series.
ReadThe Bugs I Found Attacking My Own Journaling App — and the Bugs My Fixes Created
The confirmed vulnerabilities from a ten-round self-pentest of MoodHaven Journal: a readable database, silently lost edits, keys leaking over the LAN — and the critical bugs my own fixes introduced. Part 2 of a four-part series.
Read