13 KiB
crypha: Overview
What This Is
A multi-format steganography tool written in Go. You hand it a message or a file, and it seals that payload in a passphrase-encrypted, compressed, integrity-checked envelope, then hides the envelope inside an ordinary-looking carrier: the low bits of an image or an audio file, the Reed-Solomon slack of a QR code, zero-width characters in a block of text, or the structure of a PDF. Point reveal at the result and it auto-detects the carrier and hands the message back. It ships as a single static, dependency-free binary and drives from either a scriptable CLI or a guided terminal wizard.
The point of the project is to understand, by building it, the honest exchange between two adversaries. Steganography hides that a message exists; steganalysis is the statistical hunt that finds it anyway. crypha implements five very different hiding channels, each with its own capacity, fragility, and detection story, and the learn/ track walks the analysis that breaks each one. Nothing here is a stub. Every carrier round-trips text and random-binary payloads under test, and the QR carrier is differentially tested against reference encoders and decoders.
Why This Matters
Cryptography and steganography answer two different questions, and people constantly conflate them. Encryption makes a message unreadable. Steganography makes it unnoticeable. Encryption on its own still announces that a secret exists, and an opaque high-entropy blob is itself a signal, the exact thing a data-loss-prevention scanner, an intrusion-detection rule, or a border inspection is trained to flag. Steganography removes the signal by making the carrier look like a holiday photo, a voice memo, a PDF invoice, or a QR code on a poster.
The technique is not academic. It shows up in real intrusions on both sides of the line.
- Witchetty, 2022. The espionage group concealed a backdoor inside a bitmap of an old Windows logo hosted on a public cloud service, so the payload arrived on the target looking like an ordinary image download rather than malware (Symantec). The image still rendered as a logo. The code rode in the bits underneath.
- The Stegano exploit kit, 2016. Malicious script was hidden in the alpha channel of PNG banner ads served to millions of visitors on mainstream sites (ESET). A tiny per-pixel change to transparency, invisible on the page, carried the redirect logic.
- Invoke-PSImage, 2017. An open-source tool that packs a full PowerShell script into the two least-significant bits of the RGB channels of a PNG. It turned image LSB steganography into a point-and-click red-team primitive, and it is the direct ancestor of crypha's
imagecarrier. - Stegoloader / Gatak, 2015. This malware family pulled its own components out of images fetched at runtime, keeping the obvious executable code off disk where a scanner would look for it (Dell SecureWorks).
Defenders answer with steganalysis. The chi-square attack, RS analysis, and sample-pair analysis each estimate how much of an image has been touched, without ever needing the key. crypha exists to teach both moves. It encrypts first, so a discovered payload is still unreadable, then hides the ciphertext, and then tells you honestly how each carrier gets caught.
Real-world scenarios where this applies:
- Covert-channel research. Understanding how a payload survives, or fails to survive, a copy, a re-encode, a normalization pass, or a PDF save-as.
- Blue-team detection. Building the intuition to spot LSB embedding, zero-width runes, and appended-after-EOF data before an exfiltration tool uses them against you.
- Learning applied cryptography. The envelope is a complete, correct AEAD construction: Argon2id key derivation, ChaCha20-Poly1305, associated-data binding, and fail-closed verification, in about two hundred readable lines.
What You'll Learn
Security concepts:
- Steganography versus cryptography, and why you want both. Concealment and secrecy are independent properties. crypha layers them: encrypt for secrecy, hide for concealment.
- Five hiding channels and their trade-offs. LSB in pixels and PCM samples, Reed-Solomon error injection in QR codes, zero-width Unicode in text, and three structural techniques in PDF, each with a different capacity, robustness, and detection profile.
- Steganalysis, honestly. How a defender detects each carrier: the chi-square and RS attacks on LSB, a one-pass rune scan on zero-width text,
strings | tailon an appended PDF payload. - AEAD done correctly. Why the header is authenticated as associated data, why a flipped byte must fail to open rather than decrypt to garbage, and why compress-then-encrypt is safe for an offline file tool when it would be dangerous for a network protocol.
Technical skills:
- A plugin architecture in Go. One
Carrierinterface, a self-registering registry via blank imports, and an engine that dispatches through it without knowing any carrier's internals. - Reimplementing a spec. The QR carrier rebuilds module placement, mask reversal, block de-interleaving, and Reed-Solomon decode from ISO/IEC 18004, because the available Go library exposes only a finished bitmap.
- Bit-level I/O and format quirks. MSB-first bit packing, the mandatory NRGBA conversion that stops Go's PNG encoder from corrupting your low bits, and the WAV encoder's mandatory seek-back on close.
- One engine, two frontends. A shared brain that a cobra CLI and a bubbletea wizard both drive as thin, interchangeable faces, so the terminal UI holds zero carrier logic.
Tools and techniques:
skip2/go-qrcodeto generate a clean QR matrix, used as a generator and a differential-test oracle, never at runtime for decode.golang.org/x/cryptofor Argon2id and ChaCha20-Poly1305, pluscrypto/aesfor the AES-256-GCM alternate.go-audio/wavfor PCM read and write, andmewkiz/flacto decode a FLAC cover to samples.pdfcpufor lossless attachment and metadata techniques, and plainiofor the append-after-EOF technique.
Prerequisites
You do not need prior steganography experience. You do need some comfort with the following.
Required knowledge:
- Go basics. Structs, interfaces, slices, and errors. If you can read a method on an interface value, you can read this code.
- Bytes and bits. What a byte is, what "the least-significant bit" means, and that a
uint32is four big-endian bytes on the wire. - What encryption is, roughly. That a key turns readable data into unreadable data and back. The envelope chapter in 01-CONCEPTS.md explains the rest.
Tools you'll need:
- A Go toolchain, 1.25 or newer, only if you build from source. The prebuilt binary needs nothing. The
install.shscript fetches a toolchain for you if one is missing and you asked to build. - Nothing else to run it. No API keys, no network, no services. crypha is a one-shot offline file tool.
Helpful but not required:
- ImageMagick and ffmpeg, to synthesize throwaway covers for experimenting. DEMO.md shows the exact commands.
- A skim of how QR codes are structured, if you want the showpiece in 03-IMPLEMENTATION.md to land faster.
Quick Start
# Install (grabs a prebuilt binary, no Go needed):
curl -fsSL https://angelamos.com/crypha/install.sh | bash
# or, with a Go toolchain:
go install github.com/CarterPerez-dev/crypha/cmd/crypha@latest
# Launch the guided wizard:
crypha
# Ask how much a cover can hold:
crypha capacity -i photo.png
# Hide a message in an image, then read it back:
crypha hide -i photo.png -o secret.png --format image -m "meet at noon"
crypha reveal secret.png
Expected output: capacity prints a per-carrier table with an exact envelope byte count for the cover you gave it. hide prints a short receipt (output file, format, payload bytes, envelope bytes, whether it was encrypted and compressed). reveal needs no --format; it detects the carrier itself, writes the recovered message to stdout, and prints a one-line status to stderr like revealed 11 bytes via image -> (stdout).
Add a passphrase and reveal asks for it before it decrypts:
crypha hide -i photo.png -o secret.png --format image -m "coordinates inside" --encrypt --compress
crypha reveal secret.png # prompts for the passphrase, then decrypts
A passphrase from any source, the -k flag, the CRYPHA_PASSPHRASE environment variable, or the no-echo prompt, always means the payload is encrypted. crypha never silently writes plaintext when you asked for a key.
Project Structure
steganography-multi-tool/
├── cmd/crypha/ # tiny main: it only calls cli.Execute()
├── internal/
│ ├── cli/ # cobra: hide reveal capacity formats version tui, plus secure passphrase input
│ ├── tui/ # the bubbletea wizard, a pure view over the engine
│ ├── engine/ # the shared brain both frontends call
│ ├── carrier/ # the Carrier interface + Register/Get/All/Detect registry
│ │ ├── all/ # blank-import aggregator that registers every carrier
│ │ └── image/ audio/ qr/ text/ pdf/
│ ├── payload/ # the encrypted envelope: Argon2id, AEAD, flate, CRC32, framing
│ ├── bitio/ # MSB-first BitReader / BitWriter
│ ├── config/ # every constant: magic bytes, KDF params, the format catalog
│ └── report/ # human tables and --json rendering
├── learn/ # this teaching track
├── install.sh # the one-shot curl-able installer
├── .goreleaser.yaml # cross-platform release binaries
└── justfile # every recipe
The single most important thing to understand first is the seam in internal/engine/engine.go. Both frontends call engine.Hide, engine.Reveal, and engine.Capacity; the engine wraps the payload in an envelope and dispatches to a carrier through the registry. Everything in internal/carrier exists to store and retrieve opaque bytes, and everything in internal/payload exists to build those bytes. Neither knows about the other.
Next Steps
- Understand the ideas. Read 01-CONCEPTS.md for steganography versus cryptography, each of the five hiding channels, the AEAD envelope, and the steganalysis that breaks each carrier, grounded in real incidents.
- See the design. Read 02-ARCHITECTURE.md for the one-engine-two-frontends design, the self-registering carrier registry, the exact envelope byte layout, and how auto-detect avoids mistaking a QR-PNG for an ordinary image.
- Walk the code. Read 03-IMPLEMENTATION.md to trace a payload from message to hidden bytes, with the QR-from-ISO/IEC-18004 Reed-Solomon injection as the showpiece.
- Extend it. Read 04-CHALLENGES.md for projects from a new carrier to variable-density LSB modes to stronger steganalysis resistance.
Common Issues
hide --format image refuses the cover
crypha: image cover must be an 8-bit PNG or a 24-bit BMP
Solution: the image carrier refuses paletted and 16-bit PNGs on purpose, because editing palette indices or high bytes would visibly wreck the cover. ImageMagick defaults to a 16-bit PNG for synthetic gradients; force 8-bit truecolor with -depth 8 PNG24:cover.png. A normal photo saved as PNG or a 24-bit BMP just works.
reveal says it found nothing
crypha: no crypha payload detected; pass a format to force a carrier
Solution: the file either does not contain a crypha payload, or the carrier was destroyed. LSB carriers survive a byte-for-byte copy but not a re-encode; if you re-saved the stego PNG as JPEG, or the WAV as MP3, the payload is gone. If you know the format, pass --format to skip detection and get a specific error.
capacity on a QR cover says "does not fit" for encryption
qr 52 38 does not fit
Solution: this is correct, not a bug. A QR code holds only tens of bytes in its correctable-error budget, and an encrypted envelope adds 68 bytes of overhead. QR is a plaintext-only carrier by design, and capacity tells you so up front.
Related Projects
If you found this interesting, look at:
- metadata-scrubber-tool: the inverse instinct, stripping the hidden metadata out of a file instead of adding to it.
- binary-analysis-tool: static analysis of a binary's structure, the same "read a file format precisely" muscle applied to executables.
- nadezhda (security-news-scraper): the same single-static-binary, one-engine-two-frontends shape, applied to security-news intelligence.