A command-line tool that hides text inside images using steganography. The image looks identical to the original — the hidden text is invisible to the eye and survives JPEG compression.
This is a learning project. Built to understand steganography from first principles and explore LLM security concepts. Not production-ready, not optimized, not hardened.
Studying how hidden payloads survive image processing pipelines — specifically whether a prompt injection embedded in an image can reach a multimodal AI system after JPEG compression, resizing, or re-encoding.
LSB (Least Significant Bit) — flips the last bit of each pixel to encode data. Fast and high capacity, but fragile: JPEG compression destroys the payload. Use only with lossless formats (PNG).
SS (Spread Spectrum) — spreads the payload signal across hundreds of pixels using a pseudorandom chip sequence. Survives JPEG compression. Requires a seed (the key) to embed and extract.
sudo apt install libjpeg-turbo8-dev
make
Dependencies: zlib, libjpeg-turbo, libm. No libpng — PNG codec is hand-rolled.
Embed:
./bin/imgpoison --embed --method ss --seed 42 --payload "text" input.png output.jpg
Extract:
./bin/imgpoison --extract --method ss --seed 42 output.jpg
LSB embed (PNG only):
./bin/imgpoison --embed --method lsb --payload "text" input.png output.png
Detect LSB embedding:
./bin/imgpoison --extract --detect input.png
Analyze — inspect an image for hidden payload without extracting it:
./bin/imgpoison --analyze --method ss --seed 42 output.jpg
./bin/imgpoison --analyze --method lsb input.png
SS analyze requires the same seed used at embed time. It prints the correlation strength for each header bit and a verdict. LSB analyze runs a chi-square test on pixel value distribution.
| Parameter | Default | Notes |
|---|---|---|
| --seed | 42 | key for embed and extract — must match |
| --strength | 10 | signal strength. higher = robust, visible |
| --method | lsb | lsb or ss |
| STRENGTH | SNR vs JPEG noise | Visible? | Survives q95? |
|---|---|---|---|
| 3 | ~96:1 | barely | yes |
| 10 | ~320:1 | slightly | yes |
| 20 | ~1280:1 | yes | yes |
Each payload bit is hidden across two groups of pixels (block A and B). Block A is shifted slightly brighter, block B slightly darker (or vice versa). To extract: compare A and B — whichever is brighter encodes the bit value.
JPEG adds random noise to pixels, but because the noise hits A and B equally, it cancels out when you subtract them. The signal survives. LSB does not survive because JPEG randomizes the exact bit used to store the data.
The seed controls a pseudorandom sequence (chip) that scrambles which pixels are used and how. Without the seed, extraction is not possible.
diffmap — visualize the difference between original and stego image. Useful to verify the embedding is working correctly.
python tests/diffmap.py img/original.png img/stego.jpg
What to look for:
- SS → uniform salt-and-pepper noise across the whole image (signal spread everywhere)
- LSB → a small bright patch in the top-left corner (payload concentrated at the start)
test_robustness — tests payload survival through image transformations.
python tests/test_robustness.py
Results with default settings (STRENGTH=10, CHIP_SIZE=256):
| Transformation | Result |
|---|---|
| baseline | PASS |
| recompress q90 | PASS |
| recompress q85 | FAIL |
| recompress q75 | FAIL |
| rotate 1 degree | FAIL |
q85 and below exceed the noise threshold at STRENGTH=10. Increase --strength to improve robustness at the cost of visibility.
- Shell script regression test (embed → extract round-trip)
- LLM security angle documentation
- Fix strength estimate in --analyze (currently inflated by JPEG noise)
Marvel, Boncelet, Retter — Methodology of Spread-Spectrum Image Steganography, ARL-TR-1698, 1998. apps.dtic.mil/sti/citations/ADA349102
Press, Teukolsky, Vetterling, Flannery — Numerical Recipes in C, 2nd ed., 1992. LCG constants (multiplier 1664525, increment 1013904223) from chapter 7.
Knuth — The Art of Computer Programming, vol. 2, sec. 3.4.2. Fisher-Yates shuffle implementation.
ITU-R BT.601 — luma coefficients reference. en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma