How to conceal an encrypted payload inside a carrier image
- Step 1Encrypt the plaintext first — Open aes-256-encryptor, choose encrypt mode, type or paste your secret text and a strong passphrase (the field requires at least 8 characters). It derives a key with PBKDF2 (100,000 iterations, SHA-256) and encrypts with AES-GCM-256, producing base64 ciphertext.
- Step 2Copy the base64 ciphertext — Take the full base64 output — including the trailing
==padding if present. Because AES-GCM prepends a random 16-byte salt and a 12-byte IV and appends a 16-byte auth tag before base64 encoding, the ciphertext is noticeably longer than your plaintext. Budget for that when sizing the carrier. - Step 3Drop a lossless carrier image — In the Steganography Encoder, upload a PNG or BMP via the dropzone (the picker accepts
.jpg,.jpeg,.png,.gif,.bmp,.webp). Prefer a PNG/BMP source so you start from clean, un-recompressed pixels — a JPEG source is decodable but its LSBs are already perturbed by compression. - Step 4Paste the ciphertext into 'Message to hide' — The only control is one textarea labelled
Message to hide(placeholderHidden text payload…). Paste the base64 ciphertext there. There is no passphrase field on the encoder itself — encryption already happened in the previous tool; the encoder only embeds bits. - Step 5Encode and download the stego PNG — The encoder reads pixels with
getImageData, packsciphertext + "\0"into the R/G/B least-significant bits, callsputImageData, andtoBlobbuilds a<name>-stego.png. If the ciphertext overflows the carrier it throwsMessage too large for this carrier image. Use a larger image.before producing any file. - Step 6Verify the full round-trip — Feed the PNG into steganography-decoder to recover the base64 ciphertext (it stops at the first NUL, capped at 100,000 characters). Then paste that ciphertext into aes-256-encryptor in decrypt mode with the same passphrase to confirm you get the original plaintext back.
Plaintext vs. ciphertext size — plan the carrier for the ciphertext
AES-GCM output carries a 16-byte salt, a 12-byte IV, the ciphertext, and a 16-byte authentication tag, all base64-encoded (≈ 4/3 expansion). The numbers below are approximate base64 lengths; always size the carrier for the ciphertext column, since that is what the encoder actually embeds.
| Plaintext length | Raw encrypted bytes (≈) | Base64 ciphertext chars (≈) | Smallest square carrier that fits |
|---|---|---|---|
| 16 chars ("launch code 7731") | 60 | 80 | 256 × 256 (24,575 cap) |
| 100 chars | 144 | 192 | 256 × 256 (24,575 cap) |
| 1,000 chars | 1,044 | 1,392 | 256 × 256 (24,575 cap) |
| 20,000 chars | 20,044 | 26,728 | 512 × 512 (98,303 cap) |
| 100,000 chars | 100,044 | 133,392 | 1024 × 1024 (393,215 cap) |
What each tool in the chain does
The two tools have separate, fixed jobs. The encoder never encrypts; the encryptor never hides. Neither exposes any option beyond what is listed — there are no presets, channels, or bit-depth controls.
| Stage | Tool | Fixed behaviour |
|---|---|---|
| 1. Confidentiality | aes-256-encryptor | AES-GCM-256, key via PBKDF2 (100,000 iterations, SHA-256), random salt + IV, base64 output |
| 2. Concealment | steganography-encoder | Packs message via charCodeAt(0) into RGB LSBs, NUL terminator, always PNG output |
| Recover ciphertext | steganography-decoder | Reads RGB LSBs, stops at first NUL, returns up to 100,000 chars |
| Recover plaintext | aes-256-encryptor (decrypt) | Verifies the GCM tag; wrong passphrase or tampered data fails loudly |
Encoder constraints for an encrypted payload
Base64 ciphertext is ASCII, so it dodges most encoding pitfalls — but the structural limits of the encoder still apply.
| Constraint | Value | Why it matters for ciphertext |
|---|---|---|
| Character set | Latin-1 / 0–255 via charCodeAt(0) | Base64 is all ASCII, so it always fits |
| End marker | NUL byte (\0) appended | Base64 never contains a NUL, so termination is reliable |
| Capacity check | (chars + 1) × 8 ≤ pixels × 3 | Use the longer ciphertext length, not the plaintext |
| Output | Lossless PNG only | Any lossy re-save destroys both the LSBs and decryptability |
| Where it runs | Browser only (Canvas + Web Crypto) | Passphrase and plaintext never leave the machine |
Cookbook
End-to-end encrypt-then-hide recipes. Every example reflects the real chain: AES-GCM-256 + PBKDF2 100k for confidentiality, RGB-LSB Canvas embedding with a NUL terminator and PNG output for concealment.
Encrypt a short note, then hide the ciphertext
The canonical workflow. Encrypt first so the embedded bits are meaningless without the passphrase; the carrier needs only enough capacity for the (longer) ciphertext.
Step 1 — aes-256-encryptor (encrypt): plaintext: meet at pier 7, 02:00 passphrase: correct-horse-battery-staple -> base64: U2FsdGVkX1+9z3...Aa== (~120 chars) Step 2 — steganography-encoder: Message to hide: U2FsdGVkX1+9z3...Aa== Carrier: cover.png (512x512 -> 98,303 cap) -> cover-stego.png Anyone extracting the LSBs sees only base64 ciphertext.
Full recovery round-trip
Recovering the message is the reverse of the chain: extract the ciphertext from the PNG, then decrypt it. AES-GCM verifies integrity, so a wrong passphrase fails instead of silently returning junk.
Step 1 — steganography-decoder: Input: cover-stego.png Output: U2FsdGVkX1+9z3...Aa== (stops at NUL terminator) Step 2 — aes-256-encryptor (decrypt): ciphertext: U2FsdGVkX1+9z3...Aa== passphrase: correct-horse-battery-staple -> plaintext: meet at pier 7, 02:00 Wrong passphrase -> GCM tag check fails -> decryption error.
Size the carrier for the ciphertext, not the plaintext
A common mistake: picking a carrier that fits the plaintext, then overflowing because the ciphertext is ~33% longer plus salt/IV/tag overhead. Always measure the base64 output before choosing the carrier.
plaintext: 18,000 chars (would fit a 256x256 carrier... barely) ciphertext: ~24,060 chars (18,000 + overhead, x 4/3 base64) 256x256 carrier capacity: 24,575 chars -> fits, but no margin 512x512 carrier capacity: 98,303 chars -> comfortable Choose 512x512: encode succeeds with room to spare.
Overflow is caught before any file is written
The encoder validates capacity up front. If the ciphertext (plus its NUL terminator) exceeds the carrier's 3-bits-per-pixel budget, it throws and produces nothing — no half-written PNG to worry about.
Carrier: icon.png (256x256 -> 24,575 char capacity) Ciphertext: ~40,000 base64 chars Result: Error: Message too large for this carrier image. Use a larger image. (no PNG produced) Fix: re-run with a 512x512 or 1024x1024 carrier.
Why the PNG must stay lossless end-to-end
Encryption raises the stakes on the lossless-PNG rule. A lossy re-save corrupts the LSBs; the recovered ciphertext then fails the AES-GCM authentication tag, so you don't even get partial recovery — you get nothing.
cover-stego.png -> shared via chat that re-compresses to WebP stego-decoder output: garbled base64 (LSBs rewritten) aes decrypt: GCM tag verification FAILED -> no plaintext Keep and share the original PNG only; never let it be re-encoded.
Edge cases and what actually happens
Ciphertext pasted as plaintext (encryption step skipped)
By designIf you skip aes-256-encryptor and paste raw plaintext, the encoder still embeds it perfectly — but it is hidden, not encrypted. Anyone who extracts the LSB channel reads your words. The encrypt step is what provides confidentiality; the encoder only conceals.
Ciphertext exceeds carrier capacity
RejectedBefore writing pixels the encoder checks (chars + 1) × 8 ≤ pixels × 3. Because ciphertext is longer than the plaintext, this is easier to hit than you expect. It throws Message too large for this carrier image. Use a larger image. and produces no file. Use a larger carrier sized for the ciphertext length.
Empty message field
RejectedIf Message to hide is blank — e.g. you forgot to paste the ciphertext — the processor throws Enter a message to encode. before any Canvas work and produces no PNG. Paste the base64 ciphertext and re-run.
Stego PNG re-saved through a lossy step
UnrecoverableA lossy re-encode (JPEG, a chat app re-compressing uploads, a CMS converting to WebP) rewrites every least-significant bit. The recovered ciphertext is corrupt, and because AES-GCM is authenticated, decryption fails the tag check and returns nothing — not even partial text. Distribute and store the original PNG only.
Wrong passphrase at decrypt time
Fails loudlyAES-GCM verifies an authentication tag during decryption. A wrong passphrase derives a wrong key, the tag fails, and aes-256-encryptor reports a decryption error rather than emitting plausible-looking garbage. This is a feature: you know immediately whether you have the right key.
Passphrase lost
UnrecoverableThere is no recovery path, backdoor, or reset. The key is derived from your passphrase with PBKDF2 and never stored. If you lose the passphrase, the ciphertext — and therefore the hidden message — is permanently unrecoverable. Store the passphrase in a password manager separately from the image.
Base64 padding (==) accidentally trimmed
Decrypt failsThe trailing =/== padding is part of the ciphertext. If it is dropped when copying between tools, the base64 decode in aes-256-encryptor will be malformed and decryption fails. Copy the entire ciphertext string, padding included, and verify it round-trips before sharing.
Carrier is an animated GIF or multi-frame WebP
First frame onlyCanvas drawImage paints a single still frame. An animated GIF or WebP is flattened to its first frame, and only that frame's RGB LSBs carry the ciphertext. The output is a single static PNG — animation is dropped. Use a still PNG/BMP carrier to avoid surprises.
Carrier image fails to load
FailedIf the browser cannot decode the uploaded file (corrupt header, unsupported codec, mislabelled extension), loadImage rejects with Failed to load image and no encode runs. Confirm the carrier opens in an image viewer and prefer the standard formats in the picker list.
Reusing the same passphrase across many images
Acceptable but plan accordinglyAES-GCM here generates a fresh random salt and IV per encryption, so re-running aes-256-encryptor on the same plaintext yields different ciphertext each time — safe to reuse a strong passphrase across messages. The real risk is operational: a single leaked passphrase unlocks every image encrypted with it. Use distinct passphrases for distinct recipients.
Frequently asked questions
Why encrypt before hiding — isn't steganography already secret?
Steganography hides *where* the message is, not *what* it says. The encoder writes your text into the RGB least-significant bits as plaintext, so anyone who runs an LSB decoder over the carrier reads it verbatim. Encrypting first with aes-256-encryptor means the extracted bits are only ciphertext — useless without the passphrase. Concealment and confidentiality are two different jobs; this workflow does both.
Does the encoder have a passphrase or encryption option?
No. The Steganography Encoder exposes exactly one control — the Message to hide textarea. It embeds bits and nothing else. Encryption is a separate step you perform first in aes-256-encryptor; then you paste the resulting ciphertext as the message.
Will base64 ciphertext survive the encoder's character limits?
Yes. The encoder packs each character with charCodeAt(0) and assumes it fits in one byte (0–255). Base64 uses only ASCII characters (A–Z, a–z, 0–9, +, /, =), all well under 128, so it always round-trips cleanly. This is one reason encrypt-then-hide is more robust than embedding raw Unicode plaintext.
How big does my carrier need to be?
Size it for the *ciphertext*, which is longer than your plaintext. AES-GCM adds a 16-byte salt, 12-byte IV and 16-byte tag, then base64 expands the result by about 4/3. Capacity is pixels × 3 ÷ 8 characters minus one for the NUL terminator. See the size table — a 512×512 carrier holds ~98,303 characters, enough for tens of kilobytes of ciphertext.
What encryption does aes-256-encryptor use?
Web Crypto AES-GCM with a 256-bit key derived from your passphrase via PBKDF2 (100,000 iterations, SHA-256), using a random salt and IV per encryption. AES-GCM is authenticated, so tampering or a wrong passphrase causes decryption to fail rather than return garbage. It runs entirely in your browser.
What happens if someone tampers with the stego image?
Any change to the pixels corrupts the embedded ciphertext bits. When you try to decrypt the recovered bytes, AES-GCM's authentication tag check fails and you get a clear error instead of plausible-looking text. So tampering is detectable — but it also means there's no partial recovery; the message is all-or-nothing.
Can I recover the message if I lose the passphrase?
No. The key is derived from your passphrase and never stored anywhere — there's no reset or backdoor. Losing the passphrase makes the ciphertext, and therefore the hidden message, permanently unrecoverable. Keep the passphrase in a password manager, stored separately from the image.
How do I get the original message back?
Reverse the chain. Run the -stego.png through steganography-decoder to extract the base64 ciphertext (it stops at the NUL terminator, capped at 100,000 characters), then paste that into aes-256-encryptor in decrypt mode with the same passphrase to get the plaintext.
Is any of this uploaded to a server?
No. Both the AES-GCM encryption (Web Crypto) and the Canvas LSB embed happen in the page. The plaintext, the passphrase, and the carrier never leave your machine. Both tools are browser-only with no server API path.
Why must the output stay a PNG the whole time?
LSB steganography needs every bit preserved, and PNG is lossless — the encoder always outputs PNG via canvas.toBlob(..., "image/png"). If the stego PNG is ever re-saved through a lossy step (JPEG, WebP, a re-compressing chat app), the bits change, the recovered ciphertext is corrupt, and AES-GCM authentication fails. Share and store only the original PNG.
Can statistical steganalysis still detect that something is hidden?
Possibly. Tools like chi-squared or RS analysis can flag LSB embedding, especially when a large fraction of the carrier's LSB budget is used. Encryption makes the *content* unreadable but doesn't make the *embedding* invisible. To reduce detectability, keep the payload small relative to the carrier — a short ciphertext in a large image uses a tiny fraction of the LSBs.
Why is this tool gated to the Developer tier?
The registry sets minTier: "developer" for the encoder. The Developer security tier allows files up to 2 GB with unlimited file count, comfortably covering any raster carrier. For non-image obfuscation needs — generating GDPR-safe mock data from real spreadsheets rather than hiding text in images — see csv-json-data-scrambler.
Privacy first
Every JAD Security operation runs entirely in your browser. Files, passwords, and PGP private keys never leave your device — verified by zero outbound network requests during processing.