QR Code Steganography Hider
Hide secret messages inside fully scannable QR codes. Uses LSB pixel encoding, module color steganography, and PNG metadata embedding. After encoding, the QR is re-scanned with jsQR to check it still reads.
Primary QR Content
A new QR code will be generated that scans to this
Your secret message is hidden from ordinary QR readers. The QR continues to display its original content, but the embedding methods leave detectable traces in the image or PNG metadata.
Dual-Payload QR Code
A new QR code is generated that scans normally to your URL. The secret message is embedded without changing what the QR scans to; read it back with the QR Extractor. Processed in your browser.
How to Hide a Message in a QR Code (5 steps)
- Enter the URL or text the QR code should scan to (primary content), or upload a QR image to read it from
- Type your secret hidden message or upload a file to hide
- Choose a method: PNG Metadata is the default; LSB and Module Color change pixel values instead
- Optionally add a password for AES-256-GCM encryption
- Click Generate — download a QR code that scans to your link and carries your hidden message
QR Steganography Methods — Algorithm Comparison
| Method | Capacity | Detection Risk | Best For |
|---|---|---|---|
| PNG Metadata | Up to 4,096 stored bytes (this tool's limit, counted after the password and Reed-Solomon and before the 4-byte length and the Base64) | Any metadata viewer lists the chunk | Quick embeds; no pixel changes |
| LSB Pixel Encoding | 2,624 to 106,949 message bytes at scale 5, depending on QR size (4-byte length already deducted; password and Reed-Solomon bytes still have to fit) | A bit-plane view shows the changed pixels at the top of the image | Larger payloads |
| Module Color Stego | 22 to 3,844 message bytes, depending on QR size (4-byte length already deducted; password and Reed-Solomon bytes still have to fit) | A pixel dump shows red values 1, 2, 253 and 254 | Small messages; CTF challenges |
| QR Error-Correction Codeword Embedding | — | — | Not implemented — listed so you do not look for it |
What This Tool Produces
You give the tool the link or text the QR code should say. It draws a standard black-and-white QR code for that content (error-correction level L, M, Q or H; each module 3, 5, 8 or 10 pixels wide; a 4-module quiet zone) and hides your message in the PNG with the method you choose. Any QR reader reads the link as usual; the hidden message is only found by something that looks for it, such as the QR Code Steganography Extractor.
This page is for hiding a message in a QR image. For other pictures use the Image Steganography Hider or the LSB Embedder; to check an image you did not make, use the Steganography Detector.
The Three Methods
PNG Metadata stores your bytes, as Base64 text with a 4-byte length in front, in a PNG tEXt chunk with the keyword stego, placed after the IHDR chunk. No pixel changes at all. Any metadata viewer shows the chunk, and saving the image again with a program that rewrites PNG chunks removes it. The 4,096-byte limit counts the stored bytes (your message after the password and Reed-Solomon), before the 4-byte length and the Base64; the table PNG Metadata: Largest Message shows what that leaves for the message.
LSB Pixel Encoding writes your bytes into the lowest bit of the red value of the first pixels, in reading order (top row first, left to right), after a 4-byte length. A pixel only changes when its bit differs from what is there: in the first example below, 77 of the 136 bits needed a change. Because embedding starts at the top-left pixel and proceeds in reading order, the modified pixels form a consecutive region beginning in the quiet zone and extending into subsequent QR rows as the payload grows.
Module Color Steganography changes one pixel per data module: the pixel in the middle of the module. A dark module's red value goes from 0 to 1 (bit 1) or 2 (bit 0); a light module's from 255 to 254 (bit 1) or 253 (bit 0). Modules are used in reading order (row by row, left to right) after a 4-byte length. The modules that carry no bits are the 9 × 9 block in the top-left corner (rows 0 to 8, columns 0 to 8: finder pattern, separator and format information), rows 0 to 8 of the last 8 columns in the top-right corner, the last 8 rows of columns 0 to 8 in the bottom-left corner, and all of row 6 and column 6 (the timing patterns). Alignment patterns and version-information blocks are not excluded. The QR matrix is unchanged, so readers see the same code. The capacity is the data modules divided by 8, rounded down, minus the 4-byte length (see the table Module Color: Data Modules and Capacity).
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.
In this tool the password protects the hidden payload only. The QR code itself, and the link or text it scans to, is not encrypted: any QR reader still reads it exactly as before.
Reed-Solomon and Stored Size
Order matters: your message is encrypted first (if you set a password) and the Reed-Solomon wrapper is put around the encrypted bytes. The Extractor removes the wrapper first, repairs what it can, and then decrypts. The wrapper is 16 bytes plus 10, 20 or 30 parity bytes for every 223 bytes of data, so 223 bytes become 249, 259 or 269. It repairs scattered changed bytes, not an image that was re-saved as JPEG, resized or photographed.
Two different error-correction systems are involved and they do not overlap. The QR code has its own error correction (levels L, M, Q and H) built into the code: it helps a scanner recover the QR content, the link or text, from a damaged or dirty code, and it does nothing for the hidden bytes. The Reed-Solomon option here is applied to the hidden bytes only: it can repair changed bytes in the hidden payload, and it does not help a scanner read the QR. Choosing error-correction level H does not protect the hidden message.
Capacity by QR Size (computed by the tool's own functions)
Largest message each pixel method holds, in bytes, with the 4-byte length already deducted (so these are not total stored bytes) and before the password and Reed-Solomon overhead. LSB: (image pixels ÷ 8, rounded down) − 4. The QR version follows from your content and the error-correction level; the image side is (modules + 8) × scale pixels.
| QR version | Modules | Image at scale 5 | LSB, scale 5 | LSB, scale 10 | Module Color |
|---|---|---|---|---|---|
| 1 | 21 × 21 | 145 × 145 px | 2,624 B | 10,508 B | 22 B |
| 3 | 29 × 29 | 185 × 185 px | 4,274 B | 17,108 B | 70 B |
| 6 | 41 × 41 | 245 × 245 px | 7,499 B | 30,008 B | 172 B |
| 10 | 57 × 57 | 325 × 325 px | 13,199 B | 52,808 B | 364 B |
| 20 | 97 × 97 | 525 × 525 px | 34,449 B | 137,808 B | 1,124 B |
| 40 | 177 × 177 | 925 × 925 px | 106,949 B | 427,808 B | 3,844 B |
What the Password and Reed-Solomon Add
Stored size for a 100-byte message, from the tool's size function.
| Options | Stored bytes |
|---|---|
| No password, no Reed-Solomon | 100 B |
| Password (AES-256-GCM) | 148 B |
| Reed-Solomon RS-20 | 136 B |
| Password and RS-20 | 184 B |
Module Color: Data Modules and Capacity
The same rule the encoder enforces. A data module is any module outside these: the top-left block of rows 0 to 8 and columns 0 to 8 (81 modules), rows 0 to 8 of the last 8 columns (72), the last 8 rows of columns 0 to 8 (72), and the parts of row 6 and column 6 outside those blocks (n − 17 each). That is 225 + 2(n − 17) = 2n + 191 excluded modules, so the data modules are n² − 2n − 191. One bit each, so the bytes are that number ÷ 8 rounded down, and the 4-byte length comes off the top. Checked for all 40 versions.
| QR version | Modules (n × n) | Excluded modules | Data modules (n² − 2n − 191) | Bytes in those modules | Capacity (minus the 4-byte length) |
|---|---|---|---|---|---|
| 1 | 21 × 21 | 233 | 208 | 26 B | 22 B |
| 2 | 25 × 25 | 241 | 384 | 48 B | 44 B |
| 3 | 29 × 29 | 249 | 592 | 74 B | 70 B |
| 6 | 41 × 41 | 273 | 1,408 | 176 B | 172 B |
| 10 | 57 × 57 | 305 | 2,944 | 368 B | 364 B |
| 20 | 97 × 97 | 385 | 9,024 | 1,128 B | 1,124 B |
| 40 | 177 × 177 | 545 | 30,784 | 3,848 B | 3,844 B |
PNG Metadata: Largest Message
The 4,096-byte limit applies to the stored bytes: your message after the password's 48 bytes and the Reed-Solomon wrapper, before the 4-byte length is added and before Base64. Stored bytes of 4,096 plus the length make 4,100 bytes, a 5,468-character Base64 value in the tEXt chunk.
| Options | Largest message | Stored bytes |
|---|---|---|
| No password, no Reed-Solomon | 4,096 B | 4,096 B |
| Password | 4,048 B | 4,096 B |
| Reed-Solomon RS-10 | 3,900 B | 4,096 B |
| Reed-Solomon RS-20 | 3,740 B | 4,096 B |
| Reed-Solomon RS-30 | 3,570 B | 4,096 B |
| Password and RS-10 | 3,852 B | 4,096 B |
| Password and RS-20 | 3,692 B | 4,096 B |
| Password and RS-30 | 3,522 B | 4,096 B |
Examples
Input: Link https://example.com, error correction M, scale 5, message FLAG{qr_demo} (13 bytes), method PNG Metadata.
Result: A 165 × 165 pixel image (QR version 2: 25 × 25 modules plus a 4-module quiet zone on each side). No pixel is different from the plain QR, and the PNG file is exactly 42 bytes larger.
Meaning: The extra 42 bytes are the tEXt chunk: 12 bytes of chunk framing, the keyword and a 24-character Base64 value. The QR itself is untouched, so the file only differs where a metadata tool looks.
Input: Same link and message, method LSB Pixel Encoding.
Result: 77 pixels differ from the plain QR, all white pixels whose red value went from 255 to 254.
Meaning: The 4-byte length and the 13 message bytes are 136 bits, one per pixel; 59 of those bits matched the pixel already there, so only 77 pixels changed.
Input: Same link and message, method Module Color.
Result: 136 pixels differ, one per bit: 33 changed 0 to 2, 31 changed 0 to 1, 44 changed 255 to 253 and 28 changed 255 to 254. The message uses 29.55% of this QR's 44-byte capacity (384 data modules are 48 bytes, minus the 4-byte length).
Meaning: Version 2 has only 44 bytes of room for a message with this method, because the 4-byte length also takes 4 of its 48 bytes. The 13-byte message plus the 4-byte length is 17 bytes, 136 bits, one per module. A larger message needs a bigger QR (a longer link or higher error-correction level) or the LSB method.
Input: A 100-byte message with a password and RS-20.
Result: 184 bytes are stored: 100 + 48 for the encryption, then 16 + 20 for the wrapper.
Meaning: The capacity bar compares those 184 bytes with the method's limit, not the 100 bytes you typed.
Input: A 1,020-character link at error-correction level L, any method.
Result: A version-23 QR. jsQR cannot read even the plain, unmodified code, and the tool reports the scan check as inconclusive; the separate module check still passes.
Meaning: The checker is the limit here, not the hiding. Test such an image with your own phone or scanner.
How the QR Hider Works
1. Draw the QR
The qrcode library builds the module matrix for your content and error-correction level, and the tool paints it on a canvas with a 4-module quiet zone, in black on white.
2. Prepare the bytes
Your message is encrypted when a password is set, then wrapped with Reed-Solomon when a level is chosen. The embedding method adds a 4-byte length in front.
3. Hide
PNG Metadata adds a tEXt chunk. LSB sets the red low bit of consecutive pixels. Module Color sets the red value of one pixel per data module to 1, 2, 253 or 254.
4. Check
The tool re-scans the image with jsQR and compares the result with your link, and separately checks that every module still has the colour the QR matrix requires.
What the results mean
| Result | What it means | What to do |
|---|---|---|
| jsQR reads your link and the module check passes | jsQR decoded the expected content, and the module-colour check found no unexpected changes. Other QR readers and physical scanning conditions were not tested. | Download it; test with your own phone before using it. |
| jsQR cannot read the plain code either | The checker has a limit at this QR size. The module check tells you whether any module changed. | Scan the file with a phone or another reader. |
| jsQR reads a different link or nothing, but the plain code was readable | The hiding disturbed the code. | Use PNG Metadata (no pixel changes), a smaller message or a higher error-correction level. |
| Capacity bar is red (payload above capacity) | The stored bytes, after the password and Reed-Solomon overhead, exceed what the method holds for this QR. | Pick LSB or PNG Metadata, raise the scale (LSB), lengthen the link to get a bigger QR, or shorten the message. |
Limitations
- Only jsQR and the separate module-colour check were used. Phone cameras, other QR readers, screens photographed at an angle and printed copies were not tested.
- LSB and Module Color depend on exact pixel values. A resize, a screenshot, a JPEG conversion or printing changes them (by design; not measured for this tool). PNG Metadata is lost when a program re-saves the image without the chunk.
- An uploaded QR image is only read for its content; the tool draws a new standard QR and does not keep the original image, logo or colours.
- QR Error-Correction Codeword Embedding (hiding data in the QR code's own error-correction codewords) is not implemented, and it is unrelated to the error-correction level and the Reed-Solomon option.
- Nothing here makes a hidden message undetectable: all three methods leave measurable traces, and no detection tool was run against them.
Privacy
QR generation, hiding, encryption and the scan check run in your browser tab; the tool's own code does not upload your content or image. Usage statistics may record which options were used and coarse facts such as sizes, never your content. The host keeps ordinary request logs.
Which Tool Next?
- QR Code Steganography Extractor — read the message back from the image you just made
- LSB Embedder — the same red-channel idea for any PNG or BMP, with a header and a password option
- Image Steganography Hider — hide a message in an ordinary photo instead of a QR code
- Steganography Detector — run statistical checks on an image you did not make
Frequently Asked Questions
Will my QR code still scan after hiding a message?
After encoding, the tool re-scans the image with the jsQR library and compares the result with your link: a pass means jsQR decoded the expected content. It also checks, without jsQR, that every module still has its original colour. These two checks do not prove that every QR scanner will read the image. Phone cameras, other readers and printed copies were not tested. jsQR cannot read every valid QR code (in our tests it could not read an unmodified QR for a 1,020-character link at error-correction level L); when it cannot read the plain QR either, the tool says the check is inconclusive instead of blaming the hidden data.
How much data can I hide in a QR code?
It depends on the method and on the QR size, which depends on your link and the error-correction level. Every figure here is the largest number of bytes the method can hold with the 4-byte length already deducted; the password's 48 bytes and the Reed-Solomon wrapper still have to fit inside it, and the capacity bar counts both. PNG Metadata is limited to 4,096 stored bytes: the limit is applied after the password and Reed-Solomon, so a full 4,096 bytes becomes a 5,468-character Base64 value once the 4-byte length is added, and with a password and RS-20 the largest message is 3,692 bytes. LSB holds one bit per pixel: 2,624 bytes in a version-1 QR at scale 5, up to 106,949 bytes in a version-40 QR. Module Color holds one bit per data module: 22 bytes in version 1 up to 3,844 in version 40 (the table below shows how).
Can I hide a message in an existing QR code image?
The tool can read an uploaded QR image for what it says, but it does not modify that picture. It draws a new, standard black-and-white QR for the same content and hides your message in the new image, so the logo, colours and size of the original are not kept. If the upload cannot be read, there is nothing to re-create and the tool asks for the text instead.
Which method is least detectable?
None is hard to detect with tools. Module Color leaves red values of 1, 2, 253 or 254 where a plain QR has only 0 and 255. LSB rewrites the red low bit of the first pixels in reading order, which is a patch at the top of the image in a bit-plane view. PNG Metadata is a named text chunk. The differences are invisible to the eye, not to analysis; no detection tool was tested.
Is my QR code uploaded to a server?
The QR is generated, encoded and scanned in your browser tab (Canvas API and Web Crypto API), and the tool's own code does not upload your content or image. Usage statistics may record which options were used and coarse facts such as sizes, never your content.
What do the password and Reed-Solomon options do?
The password encrypts the hidden bytes with AES-256-GCM before they are written; the key comes from PBKDF2 with 100,000 iterations. It protects the hidden payload only: the QR code's own content (the link or text) is not encrypted, and any QR reader still reads it. Reed-Solomon wraps the stored bytes (outside the encryption) so that a few changed bytes per 223-byte block can be repaired: up to 5, 10 or 15 for the 10, 20 and 30 levels. It cannot repair a re-compressed or resized image, where far more bytes change. This wrapper is separate from the QR code's own error correction (levels L, M, Q and H): the QR level lets a scanner recover the QR content from a damaged code and does nothing for the hidden bytes, while the wrapper repairs the hidden bytes and does nothing for the scanner.
What is QR Error-Correction Codeword Embedding?
QR codes carry redundant error-correction codewords. Hiding data there is not implemented and the method is not available; the entry only explains the idea. It is unrelated to the error-correction level (L, M, Q or H) you choose for the QR code and to the optional Reed-Solomon wrapper on the hidden bytes.
Related Tools
QR Code Steganography Extractor
Extract hidden payloads from steganographic QR codes. Auto-detects method across LSB, module color, and PNG metadata. Shows a QR anomaly heuristic and per-method results.
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.