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: EXIF 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

  • A
    license
    A
    quality
    D
    maintenance
    Enables reading and verifying C2PA Content Credentials from images and videos through both embedded manifests and invisible watermarks. Supports direct filesystem access, URL checking, and provides structured provenance data including creator identity, AI generation info, and content authenticity.
    2
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Inspect and remove EXIF metadata locally through MCP tools. Supports reading EXIF, detecting GPS, summarizing privacy risks, and stripping EXIF from images.
    11
    3
    MIT