Skip to main content
Glama
brunkstr
by brunkstr

Stegstr

Steganographic social networking. Hide messages in images and share them anywhere — local-first, with optional Nostr sync. This fork of brunkstr/Stegstr fixes 9 bugs — 5 pre-existing in upstream (including a Nostr signature-spoofing vulnerability), 3 more found in its own new work — and adds JPEG-domain steganography that survives WhatsApp and Instagram recompression — confirmed with real sends, not just simulation.

Download v0.1.0

No Rust, no Node, no build step. Pick your platform:

Platform

Download

Windows

Stegstr-Windows.exe (or .msi)

macOS

Stegstr-macOS.dmg

Linux

Stegstr-Linux.AppImage (or .deb)

All builds and the release itself run in GitHub Actions from a clean clone, not a local machine. SHA256SUMS.txt is in the release if you want to verify your download (sha256sum -c SHA256SUMS.txt, or certutil -hashfile <file> SHA256 on Windows).

Verified before publishing, not just built: the Windows download was installed and launched for real — an actual app window opened and stayed running — and the bundled CLI was round-tripped (embed → decode, byte-exact) before this link went out. The Linux AppImage is checksum-verified and the correct size, but launching it was not tested — this was built and verified from a Windows machine with no Linux environment available. If you're on Linux, you're the first real launch test; let us know if something's wrong.

Related MCP server: img-convert MCP Server

What this fork fixes

5 bugs pre-existing in the upstream application — each confirmed by building and running a pristine clone of brunkstr/Stegstr @ ad2e10e directly, not inferred from reading the code:

  • A Nostr signature-spoofing vulnerability — received events were never cryptographically verified. Any relay (or a MITM) could inject events that display as posted by any pubkey, including someone else's. (full writeup)

  • Silent payload corruption on the default encoder — the default embed (no flags) returns wrong bytes on decode for any cover ≥256px with a payload over ~9 bytes, no error, just wrong data. (full writeup)

  • A ~9dB image-destruction bug — encoding into any cover wider than one 256px tile visibly wrecked the image itself (a measured PSNR of 8.96dB / 9.02dB on two covers, against 30-67dB everywhere else) — the payload still decoded fine, which is exactly why every prior correctness test missed it; nothing had compared the output image to the cover pixel-for-pixel. Evidence images show the corruption directly. (full writeup)

  • Decode flipped bits on high-contrast/noisy covers (unguarded clamping in the inverse transform). (BUGS.md #2)

  • Decode's tile-aligned search silently skipped for extreme aspect ratios (same corruption class as above, different geometry). (BUGS.md #4)

Separately: 3 bugs found and fixed in this fork's own new work — these aren't upstream's fault, they're in code upstream doesn't have (the new JPEG/QIM encoder, and the Nostr publish-confirmation feature):

  • A malformed/legacy JPEG could leak a file handle and libjpeg's memory pool on every decode attempt. (BUGS.md #3)

  • Publish silently treated "1 of 5 relays confirmed" the same as "5 of 5" in the UI. (BUGS.md #7)

  • Security-relevant: an old QIM image (pre-adaptive-delta header format) could crash the app outright via a Reed-Solomon buffer underflow — a malformed/mismatched header, not just an incompatibility. Fixed with a bounds check plus a versioned decoder that still reads the old format. (BUGS.md #8)

One more, found in a later regression pass, that doesn't cleanly fit either group above: the default decoder trusted a file's extension over its actual content, so a valid image saved with the "wrong" extension failed with a misleading "corrupt file" error. The faulty line is identical to upstream's, but upstream has no second encoder and never promises extension-agnostic decoding — the bug only became reachable once this fork's own dual-encoder decode path existed. (BUGS.md #9)

Plus repo and toolchain fixes: 8 npm vulnerabilities (1 critical), a broken mobile-android git submodule failing every CI checkout, no declared Rust MSRV, and outdated GitHub Actions. Full detail on all 9 application bugs, with repro steps, root cause, fix commit, and regression test each: BUGS.md. Consolidated report with before/after numbers and an honest "what we did not test" section: ROBUSTNESS_REPORT.md.

Live platform results — stated precisely

"Survives recompression" and "passes through untouched" are different claims. Both are documented, not conflated:

Platform

Result

What was actually shown

Instagram

Live, genuine survival

Sent through real Instagram: file size changed (+9.1%, 48,241 → 52,609 bytes) — Instagram actually re-encoded it — and the payload still decoded correctly.

WhatsApp

Live, but pass-through, not survival

Sent through real WhatsApp: came back byte-identical. A control test (an untouched, unembedded original sent the same way) came back ~76% smaller, proving WhatsApp's pipeline does recompress — just not this file. The stego output is small/compliant enough that WhatsApp's own pipeline treats it as a no-op, so this is not evidence the payload survived a recompression pass.

Telegram

Simulated only

Same code path validated for WhatsApp/Instagram, run through a local simulator matching Telegram's known resize/quality settings — not sent through a real Telegram client.

Full methodology, the control-test details, and the simulated 45/45 channel matrix (9 cover types × 5 platform profiles): channel_simulator/BASELINE_RESULTS.md.

Usage

You need Rust (latest stable) to build the CLI, or use a downloaded binary above.

./stegstr-cli post "Hello from CLI" --output bundle.json
./stegstr-cli embed cover.png -o out.png --payload @bundle.json --encrypt
./stegstr-cli detect out.png

Sending through WhatsApp, Instagram, or Telegram? Add --robust — it switches to the JPEG/DCT (QIM) encoder built to survive those platforms' own re-compression (the example above, without --robust, does not survive being re-uploaded). Output is always a .jpg:

./stegstr-cli embed cover.jpg -o out.jpg --robust --payload "hello world"
# send out.jpg through WhatsApp/Instagram/Telegram, download the received copy, then:
./stegstr-cli decode received.jpg
# decode tries the robust JPEG/QIM decoder first, then falls back to PNG/DWT --
# you don't need to know which encoder produced an image you were sent.

AI agent operability

The CLI and an MCP server are both first-class here, not an afterthought — skill/stegstr/ documents the full zero-human-input flow, and every claim below is verified against the real binary in src-tauri/tests/cli_json_schema.rs and tests/e2e/agent_smoke.sh.

  • --json on every command (decode, detect, embed, post, calibrate) — exactly one JSON object on stdout, no prose mixed in. Schemas are committed at schema/cli/ and validated against the actual binary in CI-runnable tests, not just documented and hoped-for.

  • Documented, stable exit codes:

    Code

    Meaning

    0

    Success

    1

    Invalid usage or an otherwise-unclassified error

    2

    Capacity exceeded — payload doesn't fit the cover

    3

    No payload found in the image

    4

    Decryption failure (--encrypt/--decrypt)

    5

    Malformed input (unreadable image, bad base64/hex, non-UTF-8 text)

    In --json mode the exit code always matches the emitted error.kind.

  • No interactive prompts, ever — nothing in this CLI reads stdin interactively, on a TTY or not. --yes is accepted everywhere as a no-op, for scripts that want to pass it defensively.

  • stegstr-cli calibrate — channel fingerprinting: compare a sent original against the file received back after a real platform round trip, and infer that platform's actual re-encode pipeline (resize rule; exact JPEG quality recovered from quantization tables, or a clearly labeled best-fit estimate when it isn't exact; chroma subsampling; progressive/baseline; whether metadata was stripped). Verified against this repo's own real captured Instagram and WhatsApp evidence in live_test/ — correctly recovered exact JPEG quality for both, and independently inferred WhatsApp's ~1600px longest-side resize from pixel dimensions alone.

  • stegstr-cli mcp — an MCP server over stdio (built on the official rmcp SDK) exposing embed, decode, detect, and calibrate as tools, each with a typed input schema and description, returning the identical JSON shape as the matching CLI command.

What isn't included in this pass, honestly: binary-safe stdin/stdout piping for embed/detect, and a --seed flag for deterministic output where randomness is used (event keys, the QIM permutation) — both are in the original AI-agent-operability brief but weren't built here; flagged as open scope, not silently dropped.

Verify it yourself

./scripts/verify.sh     # clean clone -> Rust build/test/clippy -> npm test -> full channel matrix
./scripts/demo.sh        # fixed cover, fixed payload: embed + decode round trip

Windows: scripts\verify.ps1 (same steps, PowerShell). Needs Rust, and optionally Node.js + Python for the frontend tests and channel-simulator matrix (skippable: ./scripts/verify.sh --skip-python).

Expect this to take a while on a cold machine — roughly 10-20 minutes with no warm caches (network- and CPU-dependent), most of it the Rust release build alone (measured at ~7-8 minutes from an empty target/). The script prints a heartbeat line every 20 seconds during long steps and a [step N] marker with elapsed time per step, so a quiet stretch is normal, not a hang — cargo's own Compiling <crate> output is the sign of life during the build itself.

If npm test reports a timeout waiting for a worker process to start, that's a resource-contention flake (something else on the machine was compiling at the same moment), not a real test failure — it doesn't happen when nothing else is running concurrently. Just re-run it.

Build from source (full app)

Prerequisites: Node.js 18+, Rust (latest stable).

git clone https://github.com/brunkstr/Stegstr.git
cd Stegstr
npm install
npm run build:mac   # or build:win, build:linux

See the repo for platform-specific build deps (e.g. Xcode CLI tools, Visual Studio Build Tools, Linux dev packages). CLI-only:

cd src-tauri && cargo build --release --bin stegstr-cli

Binary: target/release/stegstr-cli (Windows: stegstr-cli.exe).

License

MIT

Related MCP Connectors

Related MCP Servers