DEV Community

Cover image for How I Hacked My Own GPG Key: A Developer's Forensic War Story
freerave
freerave

Posted on

How I Hacked My Own GPG Key: A Developer's Forensic War Story

I forgot my GPG passphrase mid-release. Instead of generating a new key, I treated it as a CTF challenge — using clipboard forensics, vault analysis, and a targeted dictionary attack to crack it in under 3 seconds.

It was 11 PM. The release was almost done.

I was wrapping up a new build of DotScramble — my image redaction tool — when the terminal froze at the worst possible line:

Signing DotScramble-Linux-x86_64.deb with GPG...
[GPG Password Prompt Appears]
Enter fullscreen mode Exit fullscreen mode

My fingers hovered over the keyboard.

I typed my go-to password. Incorrect.
A variation. Incorrect.
Three more attempts. Incorrect. Incorrect. Incorrect.

The cursor blinked at me, completely indifferent to my suffering.

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 — permanently.

Most people would generate a new key and move on.

I decided to do something more interesting.

I treated my own key as a CTF target — simultaneously playing the attacker and the defender, with one night to crack it. Here's exactly how it went down.


🔬 Step 1: Digital Forensics First (Never Skip This)

Before touching any attack tooling, there's a golden rule in forensics:

Look before you break things.

On Linux, desktop environments often cache credentials silently in keyring managers. I opened Seahorse (GNOME's "Passwords and Keys" GUI) and combed through the login keyring entry by entry.

What I found:

  • 🟡 Old browser session tokens
  • 🟡 A stale VS Code credential
  • 🟡 Some SSH passphrases from old servers
  • 🔴 GPG passphrase: nowhere

It had never been saved there. The key was generated on a night when I apparently didn't think to cache it.

Next stop: shell history.

grep "gen-key\|passphrase" ~/.bash_history ~/.zsh_history
Enter fullscreen mode Exit fullscreen mode

Nothing useful. I hadn't typed the passphrase inline during key generation — which is actually correct OpSec, ironically working against me now.

Dead end. Moving on.


📋 Step 2: Interrogating the Clipboard (ghost.db)

Then a thought hit me.

What if I copied the passphrase when I first created the key?

I run DotGhostBoard — a local clipboard manager I built that silently logs everything you copy into a SQLite database:

~/.config/dotghostboard/ghost.db
Enter fullscreen mode Exit fullscreen mode

If that passphrase ever touched Ctrl+C, it would be fossilized in that database.

I wrote a quick query to surface short text items — the kind a passphrase would look like:

sqlite3 ~/.config/dotghostboard/ghost.db \
  "SELECT content, created_at FROM clipboard_items \
   WHERE length(content) BETWEEN 4 AND 100 \
   AND content NOT LIKE '%Device%' \
   ORDER BY created_at DESC LIMIT 50"
Enter fullscreen mode Exit fullscreen mode

Dozens of entries scrolled past — code snippets, terminal output, random URLs. I scanned every line.

No passphrase.

The database has a max_history = 200 prune limit. My GPG key had been generated months earlier. The entry was already garbage-collected.

Result: Forensic trail cold. No direct match found.
Enter fullscreen mode Exit fullscreen mode

Time to shift strategies entirely.


🧠 Step 3: The Human Factor — Building a Targeted Wordlist

At this point I had to accept one thing:

I wasn't going to find the password. I was going to have to deduce it.

Generic brute-force was never on the table. GPG's key derivation function (S2K) is intentionally slow — without lockout policies, you can technically try forever, but the entropy in a real passphrase makes raw brute-forcing take centuries on consumer hardware.

What does work is a targeted dictionary attack — one built not from rockyou.txt, but from your own brain.

I exported my browser password vault to a passwords.csv and ran a filter:

grep -i -E "gpg|git|key|passphrase|sign|dev" ~/passwords.csv
Enter fullscreen mode Exit fullscreen mode

A pattern emerged immediately.

For local dev tooling and signing keys, I tend to use a consistent base formula — a core string I rotate the capitalization and trailing special characters on, depending on mood and how late it is when I'm generating the key.

I'd done the same thing here. I just didn't remember which variant I'd used that night.

I assembled a shortlist of the most likely candidates — each one a small mutation of the same base pattern. No random strings. No guesses. Pure informed deduction.


💀 Step 4: The Targeted Dictionary Attack

Armed with my candidate list, I needed a way to test passphrases programmatically — no GUI prompts, no interruptions, no manual gpg invocations.

This is where --pinentry-mode loopback becomes your best friend.

It tells GPG to accept the passphrase directly from stdin/environment instead of spawning a graphical pinentry dialog — which makes it completely scriptable:

#!/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 "🎉  SUCCESS! Passphrase recovered: $pwd"
        echo "==========================================="
        exit 0
    else
        echo "FAILED"
    fi
done

echo "All candidates exhausted. Expand the wordlist."
exit 1
Enter fullscreen mode Exit fullscreen mode

I hit Enter and watched the output:

Testing: D3vSig@#      ... FAILED
Testing: d3vsig22      ... FAILED
Testing: D3vSig@       ... SUCCESS ✅
Enter fullscreen mode Exit fullscreen mode

Three seconds. Four candidates. One answer.

The passphrase was the variant with a single trailing @ — not @#, not lowercase, not the longer entropy string. The specific variant I'd typed on a tired night when I didn't feel like looking up my usual suffix.

Human pattern recognition in action — working both for me (I knew the base formula) and against me (I didn't remember the exact mutation).


🏆 Step 5: Victory — And a Green Badge

With the passphrase recovered, I ran the build pipeline again:

python3 build.py
Enter fullscreen mode Exit fullscreen mode
Processing: Creating release package...

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! 🎉
Enter fullscreen mode Exit fullscreen mode

I uploaded the GPG public key block to my GitHub profile settings.

Every commit and release I push now shows that beautiful, green Verified badge.

At midnight, after all of that, it felt genuinely earned.


📌 What This Actually Teaches Us

This wasn't just a fun personal war story. It surfaces three things every developer should internalize:

1. Password managers aren't optional for GPG keys

The moment you run gpg --gen-keynot tomorrow, not later — open Bitwarden, 1Password, or KeePassXC and store that passphrase. GPG has zero mercy for forgetful humans. You will forget. Everyone forgets.

2. Targeted attacks will always beat generic ones

We think we make random passphrases. We don't. We make variations of patterns we already know, adjusted by habit, mood, and how late it is. A wordlist of 20 informed candidates will almost always outperform rockyou.txt by orders of magnitude when the target is yourself.

This is also why a single compromised vault can expose your entire security posture — your patterns leak across credentials.

3. Your clipboard manager is a forensic goldmine (and a liability)

DotGhostBoard has saved me in plenty of other recovery scenarios — but it's also a reminder that clipboard history databases are sensitive data stores. If yours isn't encrypted or access-controlled, a single compromised session can expose everything you've ever copied.

  • Know your prune window
  • Exclude sensitive applications from being tracked
  • Treat the database file itself like a secrets vault

🛠️ The Full Attack Surface Summary

Stage Method Result
Keyring check (Seahorse) GUI inspection ❌ Not cached
Shell history grep on .bash_history ❌ Not found
Clipboard forensics SQLite query on ghost.db ❌ Pruned
Vault analysis grep on passwords.csv ✅ Pattern identified
Dictionary attack Loopback pinentry script ✅ Cracked (3.2s)

[DotScramble](https://github.com/kareem2099/DotScramble) is a standalone Python desktop app for image privacy protection — think face detection, license plate auto-blur, OCR text censoring, EXIF metadata spoofing, and batch processing across entire folders. It runs fully offline, ships as a single executable, and supports Arabic RTL out of the box. This build pipeline — the one that needed the GPG passphrase — is what packages and signs every release.

Next up: how DotScramble's freemium licensing system uses **Ed25519 asymmetric signing* to prevent key sharing — with the private key living entirely server-side, never touching the client.*


Did you ever lose a GPG passphrase? How did you handle it? Drop it in the comments 👇

Top comments (0)