steg-lab
~/steg-lab > hide messages inside images
# everything runs in this tab, the image never leaves your device — save the result as PNG, re-saving as JPEG destroys the message
Drop a .png / .jpg / .webp image or click to choose

// CARRIER

size
-
usable slots
-
capacity
-
message
0 B

// MESSAGE

bits per channel

placement

passphrase (optional)

With a passphrase the message is encrypted with AES-GCM before it is hidden, so finding the bits is not enough to read it. Required for scattered placement.

// RESULT

original
with message hidden
lowest bit, before
lowest bit, after
Download PNG
Share the PNG as a file. Messaging apps and social sites that re-compress images will strip the message out.
Drop the image to check or click to choose

passphrase (if one was used)

// MESSAGE

// BIT PLANE

lowest bit of every channel
Clean photos show soft structure here - edges, shading, texture. Flat static across part of the image is a strong sign something is hidden in it.

The idea

Cryptography hides what a message says. Steganography hides that a message exists at all. A stego image looks identical to the original to anyone who receives it - only someone who knows to look, and knows how, can find anything.

Least significant bit (LSB)

Each pixel stores red, green and blue as a number from 0 to 255. Changing the last bit shifts a channel by 1 in 256 - a difference the eye cannot perceive. That spare bit is where a message goes in every mode except robust.

Type a character to watch it happen:

one character = 8 bits = 8 colour channels

How much fits

width x height x 3 channels x bits per channel / 8 = bytes

A 12 megapixel photo holds roughly 4.5 MB at one bit per channel - far more than anyone is likely to need. Raising to 2 or 3 bits per channel multiplies the capacity, but the change starts to show as noise in smooth areas like skies.

Why PNG and not JPEG

PNG is lossless: every pixel value is preserved exactly when the file is saved. JPEG discards fine detail it assumes you cannot see - and the last bit of each channel is precisely what it discards first. You can load a JPEG as the starting image here, but the output must be saved as PNG. Any platform that re-compresses the image (most messaging apps and social networks do) will destroy the message. Send the PNG as a file attachment.

Placement modes

Sequential fills channels in scan order from the top-left pixel. It is the simplest possible approach and the first thing an analyst checks - the changed bits form a solid block at the start of the image that stands out clearly in a bit-plane view.

Centered orders pixels by their distance from the image centre, filling outward. This moves the message away from the edges that are most likely to be overwritten by caption bars, watermarks, or letterboxing. It does not help against cropping, which changes the image dimensions and re-indexes every pixel, making it impossible to locate any bit no matter where it was placed.

Scattered spreads bits across the image using gaps derived from your passphrase. The gap sequence comes from a 128-bit seed produced by PBKDF2-SHA256 (150k iterations), so finding the positions costs the same effort as guessing the passphrase itself. The changes appear as random sensor noise rather than a locatable block.

Hardened adds two further changes on top of scatter. First, LSB matching: when a channel's last bit already equals the target, the pixel is left completely untouched; when it does not, the value is nudged ±1 at random rather than forced to the target. Standard LSB replacement creates a statistical imbalance between adjacent value pairs (0/1, 2/3, 4/5...) that chi-square and Sample Pair Analysis detect without even knowing where the bits are; LSB matching eliminates that imbalance. Second, no plaintext header: there is no magic string, no bit-depth byte, no length field in the clear. A random 16-byte salt embedded at the start drives one PBKDF2 derivation that produces two AES-256-GCM keys and a scatter seed. The message length is stored as its own AES-GCM ciphertext. A decoder with the wrong passphrase gets an authentication-tag failure - the same outcome as an image with nothing in it. A passphrase is mandatory.

Encryption

Hiding is not the same as protecting. Without encryption, anyone who knows the embedding method can read the message. With a passphrase set, steg-lab encrypts the payload with AES-GCM before it is embedded, using a key derived by PBKDF2-SHA256 at 150k iterations. Extracting the embedded bits then gives an attacker only ciphertext. In hardened mode two independent 256-bit keys are used: one for the encrypted length block, one for the payload.

Robust mode

Every LSB mode dies to lossy recompression. JPEG randomises roughly half the low bits and no error-correction scheme recovers from a 50% bit error rate. Resizing is worse - interpolation produces pixel values that never existed in the original, so the stored bits are gone entirely.

Surviving that requires embedding in something that compression and resampling both preserve: aggregate statistics rather than individual pixel values. Robust mode divides the image into a grid of cells whose boundaries are defined as fractions of the image dimensions, so a uniform resize keeps them aligned. Within each cell the message bit is carried by a smooth two-dimensional ripple:

sin(4 · 2π · u) · sin(4 · 2π · v)

where u and v run 0 to 1 across the cell. The bit is recovered by correlating the received cell against that same pattern - a matched filter, which averages the signal over every pixel in the cell and discards most of the compression noise. The ripple reaches zero at every cell boundary, so there are no seams between cells. Embedding strength follows local texture: busy cells absorb a stronger push invisibly; flat cells like skies get the minimum, capped further by the flattest quadrant within the cell so the ripple never spills from a textured area into a smooth one.

Each frame bit is spread across about five cells rather than one, chosen by a prime stride so the votes are distributed across the whole image rather than clustered in a column. The decoder tallies the correlation from every cell by confidence-weighted voting and checks a CRC-16 over the recovered frame. The grid size is chosen automatically (coarsest that fits the message, since coarser cells survive harder resizing), and the decoder tries each candidate grid in turn - the CRC says which one is right, so the grid never has to be stored anywhere.

The trade-offs are real: capacity is tens of bytes rather than megabytes; the embedding is slightly visible under close inspection (though not as a pattern or block); there is no encryption layer; and it still does not survive cropping. For a short code or identifier that needs to persist through social-network recompression it is the right tool. For a message, the free fix is to send the PNG as a file attachment rather than a photo, which preserves it exactly.

How hidden messages get caught

  • Bit-plane view - isolate the lowest bit of every channel and look for structured noise where a photo should show smooth gradients. The Reveal tab does this, and a solid block at the top-left is a definitive sign of sequential LSB.
  • Chi-square and RS analysis - LSB replacement makes pairs of adjacent values (0 and 1, 2 and 3, ...) artificially equal in frequency. Natural images do not have this property. These tests detect it without knowing anything about the message or its position.
  • File size and history - a 4 MB PNG of a scene that should have produced a 400 KB JPEG is suspicious on its own.
  • Comparison - if the original image is available anywhere, a pixel-by-pixel difference immediately reveals every changed channel.

Other techniques

  • DCT coefficient embedding (JPEG) - embeds data in frequency coefficients rather than pixel values, so it survives JPEG compression. F5 and OutGuess work this way.
  • Palette manipulation in GIF and PNG-8 - reorder or nudge palette entries rather than pixel data directly.
  • Metadata fields - EXIF comments, PNG tEXt chunks. Trivial to insert, trivial to find, and stripped by almost every upload pipeline.
  • Alpha channel embedding - the transparency channel can carry bits, but semi-transparent pixels are frequently mangled by browsers and image editors, making it fragile in practice.
  • Audio and video - LSB embedding in PCM audio samples, echo hiding, and phase coding are the audio equivalents of what this tool does for images.

Wire format reference

Standard modes (sequential, centered, scattered). A 9-byte header written from the first usable channel: four magic bytes STG1, one flags byte (bits 0-1 = bit depth minus 1, bit 2 = scattered, bit 3 = encrypted, bit 4 = centered), then a 4-byte big-endian payload length. The payload follows. The decoder tries bit depths 1 to 3 and checks the magic - nothing needs to be communicated separately except the passphrase.

Hardened mode. No header. Slots 0-127 hold a random 16-byte PBKDF2 salt. Slots 128-383 hold the AES-GCM-encrypted payload length (IV + 4-byte ciphertext + 16-byte tag = 32 bytes). Slots 384+ hold the AES-GCM-encrypted payload, scattered by a seed derived from the same PBKDF2 pass. The decoder reads the salt, derives the keys and scatter seed, decrypts the length, then reads and decrypts the payload. Failure at any AES-GCM step is indistinguishable from an image with no message.

Robust mode. No header. A frame of 1 length byte + message bytes + CRC-16 is written as a sinusoidal ripple across a relative grid. The grid is one of [16, 24, 32, 48, 64, 96] cells per axis, chosen as the coarsest that fits the message and whose cells are at least 30px wide. Frame bits are mapped to cells by a prime-stride permutation. The decoder tries each grid; the CRC validates the result.