How I Hacked My Own GPG Key: A Developer's Forensic War Story
Local clipboard forensics, password pattern analysis, and a targeted dictionary attack — all against my own machine at 11 PM

TL;DR — I forgot my GPG passphrase mid-release. Instead of generating a new key, I treated it as a self-CTF: clipboard forensics → password vault analysis → targeted 4-candidate dictionary attack. Cracked in 3.2 seconds. Here's the full playbook.
It was 11 PM when my release pipeline stopped cold.
I was packaging a new build of DotScramble — an image privacy tool I'm building — when the terminal froze at the most inconvenient possible step:
Signing DotScramble-Linux-x86_64.deb with GPG...
[GPG Password Prompt Appears]
I typed my usual passphrase. Incorrect. A variation. Incorrect. Three more guesses. Incorrect, incorrect, incorrect.
The cursor blinked at me, completely indifferent.
Here's the brutal truth about GPG passphrases: there is no "Forgot Password" button. No recovery email. No support ticket. The passphrase is the key. Lose one, you've lost the other.
Most developers would generate a new key and move on. But I decided to do something more interesting: treat my own GPG key A7BC4921FE83D105 as a CTF target, and document every step of the recovery — not just what worked, but why.
Who is this for?
Developers who've ever had a "wait, what was that passphrase again?" moment
Security-curious engineers who want to see real OSINT methodology in practice
Anyone building release pipelines who wants to understand GPG signing better
No prior security background needed. If you know what a terminal is, you'll follow this.
Understanding the Attack Surface
Before diving in: why can't you just brute-force a GPG key?
GPG uses a String-to-Key (S2K) function to derive the AES encryption key from your passphrase. This KDF (Key Derivation Function) is intentionally slow — it iterates a hash function thousands of times to make each guess expensive.
On modern hardware, you can test roughly 50,000 GPG passphrases per second. Against an 8-character mixed-character passphrase, that means:
Keyspace: 72^8 ≈ 7.2 × 10¹⁴ combinations
Speed: 50,000/sec
Estimated time: ~455 years
Generic brute force is dead on arrival. The only viable path: attacking the human, not the cryptography.
Phase 1 — Forensics: Check Before You Break
The first rule of any credential recovery is passive recon. Check every place where the passphrase might already be stored before trying to crack anything.
1.1 Keyring Manager (Seahorse)
Linux desktop environments cache many secrets automatically. I opened Seahorse (GNOME's GUI for gnome-keyring) and searched every entry.
secret-tool search xdg:schema org.gnome.keyring.Note
Found: browser tokens, VS Code credentials, SSH passphrases. GPG passphrase: nowhere. It had never been saved here.
1.2 Shell History
grep -i "passphrase\|gpg\|gen-key" ~/.bash_history ~/.zsh_history
Nothing. The passphrase had been entered interactively at key generation — correct security practice, now working against me.
1.3 Clipboard Forensics — Querying ghost.db
This is where it got interesting.
I run DotGhostBoard, a clipboard manager I built that silently logs every Ctrl+C to a local SQLite database:
~/.config/dotghostboard/ghost.db
If I'd ever copied the passphrase, it would be fossilized here. I ran a targeted query:
sqlite3 ~/.config/dotghostboard/ghost.db \
"SELECT content, created_at FROM clipboard_items
WHERE length(content) BETWEEN 6 AND 60
AND content NOT LIKE 'http%'
AND content NOT LIKE '% %'
ORDER BY created_at ASC"
Dozens of entries scrolled past — code snippets, hashes, short strings. No passphrase.
My database had a max_history = 200 prune limit. The key was generated months ago. Entry long gone.
Note for your own setup: Most clipboard managers (CopyQ, Parcellite, KDE Klipper) maintain similar local databases. Know your retention window — it has real security and recovery implications.
Phase 2 — Intelligence: Building the Targeted Wordlist
Forensics failed. Time to shift to active intelligence.
The insight: I wasn't going to find the password. I was going to deduce it.
I exported my browser password vault to a CSV and filtered for development-related credentials:
grep -i -E "gpg|sign|key|dev|local|tool" ~/vault_export.csv | \
awk -F',' '{print $4}' | sort -u
A pattern emerged immediately. For local dev tools and signing keys, I consistently use a structural template: a numeric base, an alphabetic segment, and a rotating special character suffix.
This is the core vulnerability — not of GPG, but of the human who chose the passphrase. Even security-aware developers build patterns. I just needed to identify mine.
I compiled a shortlist of four candidates — the most likely variants of the base I'd been using at the time of key generation.
Phase 3 — Targeted Dictionary Attack
Armed with my four-candidate list, I needed a way to test passphrases programmatically — no GUI dialogs, fully automated.
The Key: --pinentry-mode loopback
GPG's default behavior is to spawn a graphical pinentry dialog for passphrase input. This is intentional security design — it prevents background processes from silently accessing the passphrase.
However, GPG exposes --pinentry-mode loopback for legitimate batch/automation use cases, which redirects passphrase input to stdin:
echo "test" | gpg \
--batch \
--yes \
--pinentry-mode loopback \
--passphrase "CANDIDATE" \
-u KEY_ID \
--sign > /dev/null 2>&1
Exit code 0 = passphrase correct. Non-zero = wrong guess.
The Script
#!/bin/bash
candidates=(
"D3vSig@#"
"d3vsig22"
"D3vSig@"
"qX7#mLP9sKRvZn2"
)
target_key="A7BC4921FE83D105"
for pwd in "${candidates[@]}"; do
echo -n "[*] Testing: $pwd ... "
echo "test" | gpg \
--batch \
--yes \
--pinentry-mode loopback \
--passphrase "$pwd" \
-u "$target_key" \
--sign > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo ""
echo "======================================="
echo "[+] Recovered: $pwd"
echo "======================================="
exit 0
else
echo "FAILED"
fi
done
echo "[-] All candidates exhausted."
exit 1
The Output
[*] Testing: D3vSig@# ... FAILED
[*] Testing: d3vsig22 ... FAILED
[*] Testing: D3vSig@ ... SUCCESS ✅
Three seconds. Four candidates. One match.
The correct passphrase was the variant with a single trailing @ — not the double @# I use in other contexts, not the all-lowercase version, not the long random string. The variant I'd typed on a tired weeknight without looking up my usual suffix.
Phase 4 — Back in Business
python3 build.py
Signing DotScramble-Linux-x86_64.deb with GPG...
✅ Detached signature created: DotScramble-Linux-x86_64.deb.asc
Signing DotScramble-Linux-x86_64.AppImage with GPG...
✅ Detached signature created: DotScramble-Linux-x86_64.AppImage.asc
Build completed successfully! 🎉
I added the GPG public key to my GitHub account. Every commit and release now shows a green Verified badge.
At midnight, after all of that, it genuinely felt earned.
Full Recovery Summary
| Phase | Method | Tool | Result |
|---|---|---|---|
| Recon | Keyring inspection | Seahorse / secret-tool |
❌ Not cached |
| Recon | Shell history search | grep |
❌ Not found |
| Recon | Clipboard forensics | SQLite on ghost.db |
❌ Pruned |
| Intelligence | Vault export + pattern analysis | grep + manual review |
✅ Pattern identified |
| Exploitation | 4-candidate dictionary attack | Bash + loopback pinentry | ✅ Cracked in 3.2s |
Total time: ~30 minutes. The attack script itself ran in under 4 seconds.
Takeaways
1. Password managers are non-negotiable for GPG keys
The moment gpg --gen-key finishes — before you close the terminal — open Bitwarden, 1Password, or KeePassXC and save the passphrase. GPG has zero mercy for human memory. This is the only takeaway that would have prevented all of this.
2. You are your own biggest attack surface
Even with a passphrase that would take 455 years to brute-force, a targeted dictionary attack built from your own vault cracked it in seconds. We think our passwords are random. They're not — they're variations of patterns we've been using for years. Anyone with access to one of your credentials has partial access to all of them.
3. Clipboard managers are unsung forensic goldmines (and risks)
DotGhostBoard failed me here only because of the prune limit. In other scenarios it's saved me entirely. The point is: your clipboard manager silently knows a lot about your daily workflow. Secure its database. Know what it's storing. Know your retention policy.
4. Consider hardware-backed GPG storage
A YubiKey with GPG support stores the private key in hardware — it can never be exported. The PIN has hardware-enforced retry limits. A targeted dictionary attack against a .gnupg file becomes irrelevant. For production signing keys, this is worth the investment.
What's Next in the DotSuite Series
This build pipeline — the one that needed the GPG passphrase — signs every DotScramble release. In the next post, I'll walk through how DotScramble's freemium licensing system uses Ed25519 asymmetric signing to prevent license key sharing, with the private key living entirely server-side in the Rust/Axum backend.
Spoiler: it's the same principle that makes GPG hard to crack, applied to a custom activation flow with hardware fingerprinting and a PostgreSQL
license_machinestable.
If you're building dev tools with any kind of monetization layer, that one will be worth your time.
DotScramble is a standalone Python desktop app for image privacy — face detection, license plate blur, OCR text censoring, EXIF metadata spoofing, batch processing, and a full RTL/Arabic UI. Built as part of DotSuite, a suite of developer productivity tools.
Did you ever lose a GPG passphrase — or any critical dev credential? How did you handle it? Drop it in the comments 👇
If this helped you, a ❤️ goes a long way — it helps other developers find the article.




