JPEG Steganography Hider
Hide secret data inside real JPEG images using DCT-domain steganography (F5, JSteg) — embedded in the JPEG's own quantized DCT coefficients, not pixels. Also supports JPEG Comment/APP metadata storage.
Upload your JPEG image
The cover image that will contain your hidden data
Supports JPEG/JPG files · max 50 MB desktop, 20 MB mobile
How to Hide Data in a JPEG Image (5 steps)
- Upload your JPEG photo as the carrier.
- Type your secret message or upload a file to hide.
- Optional: add a passphrase to encrypt the payload with AES-256-GCM (recommended). With F5 the same passphrase also sets the order of the coefficients, which hides nothing by itself.
- Choose an algorithm — F5 holds more and was not flagged by our chi-square test; JSteg is the classic CTF method and was flagged on most of our test files; JPEG Comment is plain metadata storage.
- Click Encode — download your stego JPEG.
JPEG Steganography Methods Compared
| Method | Technique | Capacity | Best For |
|---|---|---|---|
| F5 (recommended) | Matrix encoding (Hamming codes) in DCT coefficients, passphrase-permuted | Depends on the picture's own coefficients: 1,247 to 13,255 bytes on our 640×480 test photo at quality 30 to 95 (the exact figure for your file is shown before you encode) | Default choice — DCT-domain embedding; not flagged by the pairs-of-values chi-square test in our measurements |
| JSteg | LSB of non-zero DCT coefficients, no passphrase needed | Less than F5 on every file we measured: 28%, 63% and 73% of F5's capacity at quality 30, 85 and 95 on the test photo | CTF compatibility — the pairs-of-values chi-square flagged it in our measurements |
| JPEG Comment (COM) | Embeds data in the COM segment (0xFFFE marker) — metadata only, NOT steganography | Up to 49,134 B (one segment holds up to 65,533 B of content, including the tool's text header and the Base64 of your data) | Quick, visible storage — works even on progressive JPEGs |
| APP Extension Segment | Uses custom APP3 marker (0xFFE3) with identifier — metadata only | Up to 49,128 B (same segment limit; the APP identifier takes a few bytes more) | Slightly more obscure than Comment, still visible to hex editors |
Two Kinds of Hiding in One Tool
F5 and JSteg hide bits in the quantized DCT coefficients of the JPEG: the numbers a JPEG stores for each 8×8 block instead of pixels. The tool parses the file, changes some coefficients, and writes the JPEG again without decoding it to pixels, so the picture is not re-compressed. JPEG Comment and APP Extension are not steganography at all: they put the data in a marker segment of the file, where a metadata viewer or hex editor shows it.
To hide data in a photo's pixels, or in a PNG, use the LSB Embedder or Image Steganography Hider. For steghide-compatible files use the Steghide Hider.
F5 and JSteg
JSteg writes your bytes, after a 32-bit length, into the lowest bit of the magnitude of every luminance AC coefficient whose value is not 0, 1 or -1, in file order. Capacity is the number of those coefficients minus 32, divided by 8. The pairs-of-values chi-square test (Westfeld and Pfitzmann) can detect it, because writing random bits makes the counts of coefficient values 2k and 2k+1 equal; in our tests the test flagged JSteg files and not clean photos or F5 files (counts and conditions in the table How the Chi-Square Observation Was Made; 6 of the 60 clean photos were too small to test).
F5 uses matrix encoding: k bits of the message are carried by a group of 2^k − 1 usable coefficients, and a group needs at most one change to carry them, so a larger k means fewer changes per bit. The tool picks the largest k that fits. F5 only ever moves a coefficient's magnitude down by one, never up, so some coefficients become zero and the file shrinks. A coefficient that becomes zero no longer counts as usable, so F5 discards it and fills the group again from the next usable coefficient. That is why one embedding has three different counts: examined (the usable coefficients the embedder looked at), changed (coefficients actually modified) and thrown (modified coefficients that became zero). The number of changes can therefore be larger than the number of groups.
Without a passphrase F5 uses a fixed default permutation key built into the tool (the text StegoToolkit), so the coefficient order is the same for every file and the Extractor needs no input. This default key gives no secrecy against anyone who knows the tool, and it is not an encryption key. With a passphrase the permutation key is the SHA-256 hash of the passphrase and the order depends on it. That SHA-256 step only sets the coefficient order; the AES-256-GCM key is derived separately from the same password with PBKDF2-HMAC-SHA-256 (100,000 iterations), so SHA-256 alone is not what protects the message. In this tool a password does two things: it seeds F5's order and it encrypts the message, a separate layer applied before F5 sees the bytes. The order by itself is not encryption: it hides no secret, and anyone can try passphrases against it.
How Strong Is the Encryption?
If you set a password, the payload is encrypted before it is hidden, in your browser, with the Web Crypto API. The cipher is AES-256 in GCM mode. The 256-bit key is derived from your password with PBKDF2 using HMAC-SHA-256 and 100,000 iterations. Every encryption generates a fresh random 16-byte salt and a fresh random 12-byte IV with the browser's cryptographically secure random generator (crypto.getRandomValues), so encrypting the same message twice gives different bytes. The stored layout is a 4-byte length, the salt, the IV, then the ciphertext followed by a 16-byte authentication tag — 48 bytes added in total. The tag means a wrong password, or any change to the ciphertext, makes decryption fail instead of returning garbage.
These are standard, widely used primitives, but this implementation has not had an independent audit, and using them does not make the system unbreakable. Their limits matter. Anyone who finds the hidden data can copy the ciphertext and try passwords against it offline, so your password is the real defence: use a long random passphrase, not a word or a date. The key derivation uses 100,000 PBKDF2-HMAC-SHA-256 iterations. This is below OWASP's commonly cited 600,000-iteration recommendation for password storage. That recommendation is not a direct requirement for this file-encryption format, but the lower count makes a strong, unique password especially important. The iteration count is fixed in the code and is not written into the file, so raising it requires a new, versioned format so that files already made can still be opened.
Encryption hides what the data says, not that it exists, and it does not hide the length: AES-GCM ciphertext is the same length as the plaintext, and the encrypted layout adds a fixed 48 bytes of overhead: its own 4-byte length field, the salt, the IV and the authentication tag, separate from any length or framing the tool puts around it. No padding is added. There is no key file and no way to recover a forgotten password. We have not measured key-derivation time on phones or older devices, so very large batches or slow devices may take noticeably longer per file. The tool is intended for educational and personal use; do not rely on it as the only protection for sensitive information, or against a well-resourced adversary.
The 4-byte length counted in those 48 bytes is the encryption envelope's own internal length (the length of the ciphertext including its 16-byte tag). It is separate from the length field of the embedding method (JSteg's 4 bytes, F5's 2 or 3), which is not part of the 48 bytes. The table Stored Layout by Method shows both.
Error Correction (Reed-Solomon)
Reed-Solomon is in Forensic mode, in the section Error Correction (Reed-Solomon), with the choices None, RS-10, RS-20 and RS-30. A level adds a 16-byte header and, for every block of up to 223 bytes, 10, 20 or 30 parity bytes, and repairs up to 5, 10 or 15 changed bytes in each block. It is applied to the stored bytes after encryption, so it repairs the ciphertext before it is decrypted. The capacity bar already includes the extra bytes. The 16-byte header is not itself error-corrected (it stores the length twice), and the F5 or JSteg length field, the one-byte flag and the Comment or APP text header are outside the Reed-Solomon wrapper.
Reed-Solomon fixes a few changed bytes. A re-save, resize or re-encode changes almost every coefficient and destroys the data, whatever the level. When damage is beyond the limit the Extractor reports how many blocks it could not repair and shows those bytes as found; with a password the authentication check then fails and you see a decryption failure instead of wrong text. The Extractor page describes the same steps in reverse.
Plausible Deniability (Forensic mode)
There are two separate features, both only in Forensic mode, and the Extractor has a matching control for each. (1) Plausible Deniability Mode, a switch in the F5 options: the JPEG Comment holds a harmless decoy sentence (default: Shot with Canon EOS R5) and the real message is hidden with F5 under your passphrase, encrypted, without this tool's usual flag byte. It needs a passphrase, and the Extractor finds it only with Deniability Mode (F5) switched on and the same passphrase. (2) The panel Plausible Deniability — Decoy + Real Payload: a plain-text Comment as the decoy, and an AES-256-GCM encrypted real payload hidden with JSteg (never F5, because one wrong ciphertext bit would fail the authentication). The decoy needs no password; the real layer needs the passphrase.
Neither feature writes this tool's header, so Auto-Detect does not find either one (checked in the tests), and a wrong passphrase simply fails: for the first feature the Extractor says the passphrase is wrong or there is no deniability-mode payload; for the second it still shows the decoy and says the real layer could not be decrypted. Deniability here means the file can be explained by the decoy; it does not stop someone who looks for hidden data from finding it, and F5 and JSteg data is still lost if the JPEG is re-encoded.
Segment Limits and What Else Is Kept
A JPEG marker segment stores its length in 16 bits, so one Comment or APP segment holds at most 65,533 bytes of content. The tool writes a text header and the Base64 of your stored bytes, which leaves 49,134 bytes for Comment and 49,128 for APP Extension without a password, and 49,080 and 49,077 with one (the 48 bytes of encryption count against the same segment). A larger payload is refused with a message instead of producing a file that cannot be read back.
F5 and JSteg keep the EXIF, ICC profile, Adobe and quantization-table segments byte for byte in our tests, and the picture size and colour layout stay the same. Restart markers are not preserved: F5 and JSteg rewrite the file without them. The output keeps the picture size and colour layout, but F5 and JSteg change some quantised coefficients, so some decoded pixel values differ slightly (see the measurements below). Comment and APP Extension add one segment and leave every other byte of the file as it was. The F5/JSteg engine writes a JFIF header on its own; the tool removes it again when the source had none, because a JFIF header on an RGB-coded Adobe JPEG made decoders read the colours wrongly.
Capacity by Quality (640×480 test photo)
Largest stored message in bytes, from the tool's own capacity functions, on a synthetic photo made by tests/steganography/jpeg/make-fixtures.py and saved by Pillow at 4:2:0. Real photographs differ; the capacity bar shows your file's figure.
| JPEG quality | File size | F5 | JSteg | Comment / APP |
|---|---|---|---|---|
| 30 | 21,278 B | 1,247 B | 352 B | 49,134 / 49,128 B |
| 85 | 64,874 B | 6,951 B | 4,403 B | 49,134 / 49,128 B |
| 95 | 120,800 B | 13,255 B | 9,692 B | 49,134 / 49,128 B |
What F5 and JSteg Change (same photo, quality 85)
| Method | Fill | Message | Output file | Mean pixel difference (of 255) |
|---|---|---|---|---|
| F5 | 10% | 695 B | 64,538 B | 0.23 |
| F5 | 50% | 3,475 B | 62,131 B | 1.03 |
| F5 | 100% | 6,948 B | 56,967 B | 1.76 |
| JSteg | 10% | 440 B | 64,875 B | 0.08 |
| JSteg | 50% | 2,201 B | 64,883 B | 0.41 |
| JSteg | 100% | 4,400 B | 64,886 B | 0.82 |
Stored Layout by Method
Two different 4-byte lengths can exist. The encryption envelope has its own length field, which is part of the 48 bytes. The embedding method has a length field of its own (JSteg's 4 bytes, F5's 2 or 3), which is not. Stored bytes means the encrypted bytes, if there is a password, wrapped in Reed-Solomon, if a level is chosen.
| Method | Bytes written, in order | Counted in the 48-byte encryption overhead? |
|---|---|---|
| Encryption (password set) | 4-byte length of the ciphertext including its tag, 16-byte salt, 12-byte IV, ciphertext, 16-byte authentication tag | Yes: 4 + 16 + 12 + 16 = 48 bytes |
| Reed-Solomon (level chosen) | 16-byte header, then blocks of up to 223 bytes each followed by 10, 20 or 30 parity bytes; it wraps the (encrypted) bytes | No: it is added on top |
| F5 | 2-byte length (3 bytes from 32,768 bytes), 1-byte encrypted flag, stored bytes; the length counts the flag and the stored bytes | No: the length and the flag are extra |
| JSteg | 4-byte length, 1-byte encrypted flag, stored bytes; the length counts the flag and the stored bytes | No: the length and the flag are extra |
| Comment / APP Extension | Text header JPEGsteg:v1:COM: or JPEGsteg:v1:APP: then NONE: or AES256GCM:, then the Base64 of the stored bytes (APP also starts with STEG and a zero byte). No binary length field | No |
Reed-Solomon Stored Size of 1,000 Bytes
| Level | Parity per block | Blocks | Stored size | Repairs per block |
|---|---|---|---|---|
| None | - | - | 1,000 B | nothing |
| RS-10 | 10 B | 5 | 1,066 B (16 + 1,000 + 5 x 10) | up to 5 changed bytes |
| RS-20 | 20 B | 5 | 1,116 B (16 + 1,000 + 5 x 20) | up to 10 changed bytes |
| RS-30 | 30 B | 5 | 1,166 B (16 + 1,000 + 5 x 30) | up to 15 changed bytes |
How the Chi-Square Observation Was Made
This is the test behind the statement that the scan did not flag F5. The files were measured exactly as written, with no re-save. Synthetic photos only: 60 clean files made by tests/steganography/jpeg/make-fixtures.py with --clean 60, and the F5 and JSteg files made from its 640×480 test photo. The statistic is the pairs-of-values chi-square p-value (Westfeld and Pfitzmann) over growing windows from the start of the coefficient walk; the highest p-value is reported. HIGH is above 0.8, MEDIUM above 0.3, and a file counts as flagged at MEDIUM or HIGH. The two JSteg files that were not flagged are quality 30, where the photo has only about 2,850 eligible coefficients. The 0 of 60 is the observed count on this test set, not a measured false-positive rate for clean JPEGs in general, and no other detector was tried. The script chi-measure.mjs in the same folder repeats it.
| Files | Number | Highest p-value over the leading windows | Flagged (p above 0.3, among files with enough coefficients) |
|---|---|---|---|
| Clean synthetic JPEGs (60 sizes, qualities 60 to 95, three subsamplings) | 60 (54 testable) | below 0.001 for the 54 with enough eligible coefficients; the other 6 have fewer than 500, so the test cannot run on them | 0 of 54 testable. The overall count is 0 of 60, but the 6 untestable files are not negative detections |
| F5 outputs (test photo at quality 30, 85, 95; 10, 50 and 100 percent of capacity) | 9 | 0.00 in all nine | 0 of 9 |
| JSteg outputs (same photo, same qualities and fills) | 9 | 0.49 to 0.99 in seven; 0.00 at quality 30 with 10 and 50 percent fill | 7 of 9 |
Examples
Input: A 640×480 photo saved at quality 85 (64,874 bytes) and a 695-byte message with F5.
Result: F5 chooses k = 6. It examines 59,700 coefficients and changes 1,920 of them (1,043 of those changes make a coefficient zero, which F5 discards and replaces); the output is 64,538 bytes, and decoded pixels differ from the original by 0.23 of 255 on average.
Meaning: Small payloads touch few coefficients. The 1,920 changes are more than the 931 groups a 695-byte message needs because every coefficient that becomes zero forces another draw. The whole message is read back from the finished file before you can download it.
Input: The same photo, a 6,948-byte message with F5.
Result: k = 1. F5 examines 76,251 coefficients and changes 38,168 (20,639 of them become zero); the file shrinks to 56,967 bytes (−12%), mean pixel difference 1.76.
Meaning: At full capacity F5 uses its least efficient setting (k = 1) and the change in file size is a measurable statistic.
Input: A 49,135-byte payload with JPEG Comment.
Result: Refused: one segment holds 65,533 bytes, which is 49,134 bytes of data once stored as Base64 with the header.
Meaning: A 49,134-byte payload is accepted and read back exactly; one byte more would wrap the segment's 16-bit length.
Input: 120 clean JPEGs of different sizes, qualities and subsamplings, Auto-Detect on each (with and without a password).
Result: Nothing is reported for any of them.
Meaning: Auto-Detect accepts an F5 or JSteg hit only if it reads as text, a known file type or this tool's encrypted layout. A binary blob of an unknown type must be read by choosing F5 or JSteg yourself.
How the JPEG Hider Works
1. Read
The JPEG is parsed in a Worker: header, quantization tables, Huffman tables and the quantized coefficients of every block. Progressive and multi-image files are refused for F5 and JSteg.
2. Prepare
Your message is encrypted when a password is set (48 bytes added), wrapped with Reed-Solomon when a level is chosen, and given a one-byte encrypted flag (F5 and JSteg) or a text header (Comment and APP). F5 and JSteg then add their own length field.
3. Embed
F5 changes coefficients by matrix encoding in a passphrase-shuffled order; JSteg sets the low bit of eligible coefficients in file order; Comment and APP add one marker segment.
4. Verify
The finished file is decoded with the same method and compared with your message. If it does not match exactly, no file is offered.
What the results mean
| Result | What it means | What to do |
|---|---|---|
| The capacity bar is green | The stored size, including the password and Reed-Solomon overhead, fits the method's capacity for this file. | Encode. A higher-quality or larger photo holds more. |
| The bar is red | The stored bytes exceed the capacity. | Use a larger or higher-quality photo, a shorter message, JSteg (it holds less than F5, not more, on the files we measured), or Comment/APP up to about 49 KB. |
| Refused with 'bad jpeg' | The F5/JSteg engine could not read this file's internal structure. | Re-save the photo with another program, or use Comment/APP. |
| Refused for a progressive or multi-image file | F5 and JSteg need a single baseline JPEG. | Re-export as baseline, or use Comment/APP. |
Limitations
- Anything that decodes and re-encodes the JPEG (a re-save, a resize, many upload services) rewrites the coefficients and destroys F5 and JSteg data; we re-saved a stego file with macOS sips at quality 100 and 85 and lost both. No individual service was tested.
- Only the pairs-of-values chi-square test was tried against F5 and JSteg. Calibration-based or machine-learning detectors were not, and F5's file-size shrinkage is a measurable change.
- Capacity figures come from synthetic photos; real photographs differ.
- Progressive JPEGs, files with a second JPEG glued on, and JPEGs larger than 67 megapixels are not accepted for F5 and JSteg.
- Comment and APP Extension data is plain Base64 text in the file, visible to any metadata viewer; a password only makes the bytes unreadable.
Privacy
The JPEG is parsed, changed and checked in your browser; the tool's own code does not upload it. Usage statistics may record which options were used and coarse facts such as sizes, never your image or message. The host keeps ordinary request logs.
Which Tool Next?
- JPEG Steganography Extractor — read the data back with Auto-Detect or by method
- Steghide Hider — steghide-compatible files that the real steghide program can extract
- EXIF Data Hider — put data in the EXIF fields of a photo instead of its coefficients
- Steganography Detector — statistical checks on an image you did not make
Frequently Asked Questions
What's the difference between F5 and JSteg?
JSteg replaces the LSB of DCT coefficients with magnitude 2 or more, in order — simple, and a pairs-of-values chi-square test picks it up (measured below). F5 uses matrix encoding so several payload bits need only one coefficient change, and a passphrase permutes the positions; in our test the same chi-square test did not flag F5 at any fill, but other detectors were not tried.
Why does re-saving a JPEG destroy hidden data in other tools?
A tool that decodes a JPEG to pixels and then encodes it again changes its DCT coefficients, which destroys F5 and JSteg data. F5 and JSteg here edit the coefficients directly, without decoding to pixels, so the file survives being copied unchanged. Any re-save that decodes and re-encodes the image destroys it: in our test, re-saving the stego file with macOS sips at quality 100 or 85 lost both F5 and JSteg data.
How much can a JPEG hold?
F5 and JSteg capacity depends on the picture's own DCT coefficients, so the capacity bar shows the exact figure for your file. On our 640×480 test photo it was 1,247 bytes (F5) and 352 bytes (JSteg) at quality 30, 6,951 and 4,403 at quality 85, and 13,255 and 9,692 at quality 95. Capacity depends on the actual DCT coefficients, not simply on the resolution or the quality setting, so two photos of the same size can hold very different amounts. JPEG Comment and APP Extension hold up to about 49 KB regardless of the picture, because a JPEG segment is limited to 65,533 bytes of content and the data is stored as Base64 text after a short header. With a password the largest message is 49,080 bytes (Comment) or 49,077 bytes (APP), because 48 bytes of encryption are stored with it.
Does hiding data change how the picture looks?
The metadata methods leave every pixel as it was. F5 and JSteg change quantized coefficients, which changes pixels slightly: on our test photo the mean difference from the original (out of 255) was 0.23 for F5 at 10% of its capacity, 1.03 at 50% and 1.76 at 100%, and 0.08, 0.41 and 0.82 for JSteg at the same fill. F5 also makes the file smaller, 64,874 bytes to 56,967 at full capacity on that photo, because it turns some coefficients to zero; JSteg left the size almost unchanged. These are measurements on one synthetic photo, not a visibility test.
What JPEG quality should I use?
Higher quality means more usable coefficients. On a separate test image, one 1200×800 photograph that is not the 640×480 test photo behind the capacity table, re-saved at different qualities, F5 held about 220 B at quality 50, 580 B at 75, 1.9 KB at 85 and 9.2 KB at 95. The capacity bar shows the exact figure for your file.
Will social media platforms strip this hidden data?
Likely, for all methods, but we have not tested any platform. Services that re-encode uploaded JPEGs destroy DCT-domain embeddings (we showed that any decode-and-re-encode does), and many strip metadata (Comment/APP). Share the file unchanged — email attachment or direct transfer — and test the round trip first.
Can I use F5 without a passphrase?
Yes. Without a passphrase F5 uses a fixed default permutation key built into the tool (the text StegoToolkit). That key only sets the order of the coefficients, it is not an encryption key, and it gives no secrecy against anyone who knows the tool: the order is the same for every file, so anyone with this tool can read the data. With a passphrase the permutation key is the SHA-256 hash of the passphrase, the order depends on it, and the message is also encrypted with AES-256-GCM using a separate key that PBKDF2 derives from the password. The Extractor must be given the same passphrase. The order alone is not encryption.
Is Reed-Solomon worth turning on?
Only for one situation: a few bytes of the stored data changing after embedding. It repairs up to 5, 10 or 15 changed bytes per 223-byte block (levels rs-10, rs-20, rs-30) at a cost of 16 bytes plus 10, 20 or 30 bytes per block. It cannot survive a re-save or re-encode, which changes almost every coefficient, so it is not a way to make the file survive social media. Turn it on in Forensic mode; see the Error Correction section below.
What does Plausible Deniability do here?
In Forensic mode there are two separate features. Plausible Deniability Mode (F5 only) puts a harmless decoy sentence in the JPEG Comment segment and hides the real message with F5 under your passphrase, without this tool's usual header. The Plausible Deniability — Decoy + Real Payload panel writes a plain decoy comment and an AES-encrypted real payload hidden with JSteg. Neither is found by the Extractor's Auto-Detect; see the Plausible Deniability section below.
Is JPEG Comment/APP metadata storage secure?
No — it is explicitly NOT steganography. Anyone with an EXIF viewer or hex editor can see COM/APP segments. Use F5 or JSteg for real concealment; use Comment/APP only for convenience.
Can I hide data in progressive JPEGs?
Only with JPEG Comment/APP — F5 and JSteg require a baseline JPEG (sequential, Huffman-coded). Re-export as baseline in GIMP/Photoshop, or use the Comment method. A JPEG written by macOS software (we tried sips) also failed to load in the F5/JSteg engine with the message 'bad jpeg'; the tool explains this and suggests re-saving with another encoder.
Related Tools
JPEG Steganography Extractor
Extract hidden data from JPEG images encoded with F5, JSteg, or JPEG Comment/APP metadata. DCT scan mode, brute-force mode, and CTF heuristic guess mode included.
Image Steganography Hider
Hide secret messages or files inside PNG, BMP, WebP or JPEG images; the result is saved as PNG. Optional AES-256 encryption, processed in your browser.
Steganography Capacity Calculator
Calculate exact hidden-payload capacity for PNG, BMP, WebP, WAV, FLAC, AIFF and more — plus PSNR and detection risk, all client-side. Links to the right specialist hider/extractor for your file type.