Stegstr
Embeds messages steganographically into images with the --robust JPEG/QIM encoder so the hidden payload survives Instagram's own re-encoding pipeline; verified live with a real Instagram send where the file was re-encoded and the payload still decoded correctly.
Supports embedding robust steganographic payloads intended for images sent through Telegram, with the platform's known resize/quality settings validated via the local channel simulator rather than a live send.
Targets WhatsApp image sharing for steganographic payload delivery using the --robust JPEG/DCT (QIM) encoder, plus a channel-fingerprinting workflow (calibrate) that compares a sent original against the file received back to infer WhatsApp's re-encode pipeline and recognize its pass-through behavior.
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 | |
Linux |
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 |
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. | |
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.pngSending 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.
--jsonon every command (decode,detect,embed,post,calibrate) — exactly one JSON object on stdout, no prose mixed in. Schemas are committed atschema/cli/and validated against the actual binary in CI-runnable tests, not just documented and hoped-for.Documented, stable exit codes:
Code
Meaning
0Success
1Invalid usage or an otherwise-unclassified error
2Capacity exceeded — payload doesn't fit the cover
3No payload found in the image
4Decryption failure (
--encrypt/--decrypt)5Malformed input (unreadable image, bad base64/hex, non-UTF-8 text)
In
--jsonmode the exit code always matches the emittederror.kind.No interactive prompts, ever — nothing in this CLI reads stdin interactively, on a TTY or not.
--yesis 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 inlive_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 officialrmcpSDK) exposingembed,decode,detect, andcalibrateas 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 tripWindows: 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:linuxSee 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-cliBinary: target/release/stegstr-cli (Windows: stegstr-cli.exe).
Links
Latest release — downloads for all platforms
Robustness report — before/after numbers, live-platform confirmation, what wasn't tested
Bugs found and fixed — 9 bugs (5 pre-existing upstream, 3 in this fork's own work, 1 that's neither), repro steps, root cause, fix, regression test each
Evidence images — visual proof of the image-destruction bug, upstream vs. fixed
Agent skill — zero-human-input CLI/MCP workflow for AI agents, every command verified
CLI JSON schemas —
--jsonoutput shapes for every commandWebsite — Downloads, getting started, wiki
Wiki / CLI docs — Full CLI reference
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Image toolkit: resize, compress, crop, watermark, convert, rotate, EXIF read/strip.
Verify photos are real camera captures, not AI: C2PA and RAW+JPEG forensics. OAuth or API key.
Image risk scoring, EXIF, reverse-image backlinks, and image content detection via PicDefense.io.
Inspect and remove C2PA content credentials from AI-generated images, video and audio. Free, no key.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables 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.2MIT
- AlicenseAqualityDmaintenanceInspect and remove EXIF metadata locally through MCP tools. Supports reading EXIF, detecting GPS, summarizing privacy risks, and stripping EXIF from images.113MIT
- AlicenseNot gradedqualityCmaintenanceProvides AI agents with tools to convert images between formats and inspect image metadata, enabling seamless image processing within agent workflows.11 npm3MIT
- AlicenseNot gradedqualityFmaintenanceProvides local image analysis tools using Pillow for dimensions, colors, metadata, and format conversion without external APIs.1MIT