A Python steganography toolkit that hides AES-256-GCM encrypted data inside six different carrier formats. Encode a secret, decode it back with the correct password — carriers remain perceptually identical to originals.
| Format | Technique | Carrier types |
|---|---|---|
| Image | Least-significant-bit (LSB) pixel manipulation | PNG, BMP |
| Audio | LSB sample manipulation | WAV, FLAC |
| QR Code | Reed-Solomon error correction exploitation | PNG (generated) |
| Text | Zero-width Unicode characters (U+200B / U+200C) | Any .txt |
| Invisible text layers (rendering mode 3) | Any PDF | |
| Git | Commit timestamp second-field encoding | Any git repo |
Plus cross-format utilities: auto-detect decode, capacity analysis, and batch processing.
- Python 3.11+
- git on
PATH(only required for git commit steganography) - libzbar (only required for QR code steganography)
Install libzbar for your platform:
# Debian / Ubuntu
sudo apt-get install libzbar0
# macOS
brew install zbar
# Fedora
sudo dnf install zbarpip install pysteggit clone https://github.com/MatrixMagician/PySteg.git
cd PySteg
pip install -e .Or with uv:
git clone https://github.com/MatrixMagician/PySteg.git
cd PySteg
uv syncpysteg --version
# pysteg, version 1.0.0
pysteg --helpEvery format follows the same pattern: encode hides data, decode extracts it.
All payloads are encrypted with AES-256-GCM before embedding — you'll be prompted for a password if --password is omitted.
Hides data in the least-significant bits of pixel colour channels.
# Encode a text message into a PNG
pysteg image encode \
--carrier photo.png \
--message "Top secret message" \
--password mypassword \
--output stego.png
# Encode a binary file into a BMP
pysteg image encode \
--carrier photo.bmp \
--payload secret.zip \
--password mypassword \
--output stego.bmp
# Decode
pysteg image decode \
--carrier stego.png \
--password mypassword \
--output recovered.txtHides data in the least-significant bits of PCM-16 audio samples.
# Encode
pysteg audio encode \
--carrier music.wav \
--message "Hidden in audio" \
--password mypassword \
--output stego.wav
# Decode
pysteg audio decode \
--carrier stego.wav \
--password mypassword \
--output recovered.txtGenerates a QR code encoding visible content (e.g. a URL) with a hidden payload embedded by flipping data modules within the Reed-Solomon error correction tolerance. The QR scans normally with any standard reader.
# Create a text file with the visible QR content
echo "https://example.com" > url.txt
# Encode a hidden message into the QR code
pysteg qr encode \
--carrier url.txt \
--message "Secret payload" \
--password mypassword \
--output qr_stego.png
# Decode the hidden message from the QR image
pysteg qr decode \
--carrier qr_stego.png \
--password mypassword \
--output recovered.txtNote: The
--carrierfor encode is a text file containing the visible QR content. For decode, it is the PNG image.
Inserts invisible zero-width characters between visible characters. The text looks completely normal to human readers.
# Encode
pysteg text encode \
--carrier document.txt \
--message "Invisible secret" \
--password mypassword \
--output stego.txt
# Decode
pysteg text decode \
--carrier stego.txt \
--password mypassword \
--output recovered.txtAppends invisible text (PDF rendering mode 3 — neither fill nor stroke) to page content streams. The PDF reads normally in any viewer.
# Encode
pysteg pdf encode \
--carrier report.pdf \
--message "Hidden in plain sight" \
--password mypassword \
--output stego.pdf
# Decode
pysteg pdf decode \
--carrier stego.pdf \
--password mypassword \
--output recovered.txtEncodes 5 bits per commit in the seconds field (0–31) of author/committer timestamps. Commits look completely normal in git log.
# Encode — creates commits in the target repo
pysteg git encode \
--repo /path/to/repo \
--message "Hidden in history" \
--password mypassword
# Output: "Encoded in N commits."
# Decode — specify how many recent commits to scan
pysteg git decode \
--repo /path/to/repo \
--commits N \
--password mypassword \
--output recovered.txtCheck how much hidden data a carrier can hold before encoding:
# Single file
pysteg capacity photo.png
# Multiple files — outputs a Rich table
pysteg capacity photo.png music.wav report.pdf document.txtThe table shows Raw capacity (total bytes available) and Usable capacity (after subtracting the 53-byte encryption header).
Decode without specifying the format — PySteg detects it automatically:
pysteg decode stego.png --password mypassword --output recovered.txt
pysteg decode stego.wav --password mypassword --output recovered.txt
pysteg decode stego.pdf --password mypasswordSupports image (PNG/BMP), audio (WAV/FLAC), text, and PDF. For PNG files, tries image LSB first, then falls back to QR decoding. Git repos require pysteg git decode with --commits.
Encode the same payload into every compatible carrier in a directory, or decode all carriers at once:
# Batch encode — encodes into all supported files in ./carriers/
pysteg batch encode \
--message "Same secret everywhere" \
--password mypassword \
--carriers ./carriers/ \
--output-dir ./encoded/
# Batch decode — decodes all supported files in ./encoded/
pysteg batch decode \
--password mypassword \
--input-dir ./encoded/ \
--output-dir ./decoded/Files that lack capacity are skipped. Git and QR formats are excluded from batch operations.
All payloads are encrypted with AES-256-GCM before embedding into any carrier:
- Key derivation: PBKDF2-HMAC-SHA256, 600,000 iterations
- Fresh salt and nonce generated per encryption call
- Authentication tag detects tampering — wrong password or modified ciphertext raises an error
- Wire format v1:
PST\x00magic + version(1) + salt(16) + nonce(16) + tag(16) + ciphertext = 53-byte header overhead
See docs/GUIDE.md for the full user guide covering theory behind each technique, detection/steganalysis methods, and detailed examples.
git clone https://github.com/MatrixMagician/PySteg.git
cd PySteg
uv sync
# Run the test suite (258 tests)
uv run pytest tests/
# Lint
uv run ruff check pysteg/ tests/