Subpixel
Encrypts a message and hides the ciphertext in the low bits of a PNG.
Derives an encryption key from a passphrase via Argon2id with a random per-message salt. Encrypts with AES-256-GCM so a wrong passphrase or a modified carrier fails loudly. Embeds the ciphertext across the least significant bits of a PNG, checking capacity first.
Under the hood
- Derives the key with Argon2id at m=64 MiB, t=3, p=4, using a 16-byte random salt per message so identical passphrases produce distinct keys and precomputation does not apply.
- Generates a 12-byte random nonce per message so no key/nonce pair repeats, and verifies GCM's 16-byte authentication tag on extraction so tampering or a wrong passphrase raises rather than returning garbage.
- Checks carrier capacity against payload size before writing, and packs the blob as a length prefix followed by salt, nonce, ciphertext, and tag.
- Covers the encode/decode round trip, capacity limits, and tamper detection with pytest so regressions surface between versions.
Limitations
- Requires a lossless carrier — PNG only. JPEG's lossy compression destroys the embedded bits.
- Capacity is 3 bits per pixel, so a 1920x1080 PNG carries roughly 750 KB.
- LSB embedding is statistically detectable via chi-square and RS analysis. This demonstrates the technique; it is not a covert channel.
Why I built this
Started as a way to brush up on Python after a stretch of not writing any. It took its direction from cybersecurity work — a municipal government IT internship, plus reading on key derivation and brute-force defense — and became a chance to implement encryption hands-on rather than only read about it. LSB steganography got bundled in as a second topic I’d stumbled onto and wanted to actually build.
Payload layout
The embedded blob is structured as:
┌──────────┬──────────┬───────────┬──────────────┬──────────┐
│ length │ salt │ nonce │ ciphertext │ tag │
│ 4 bytes │ 16 bytes │ 12 bytes │ N bytes │ 16 bytes │
└──────────┴──────────┴───────────┴──────────────┴──────────┘
The length prefix lets the extractor know how many bits to read before it can authenticate anything, so a truncated or oversized carrier is caught early.