Skip to content

FreeDurok/PixelCloak

Repository files navigation

PixelCloak

PixelCloak wordmark

Lossless LSB steganography for PNG (RGB). Payload is zlib-compressed, AES-256-GCM encrypted; key from PBKDF2-HMAC-SHA256 (200k). Payload bit positions are permuted with an LCG seeded via HKDF-SHA256.
Header (43 bytes) is written sequentially in the first 43*8 LSBs; payload follows the keyed permutation, skipping header area. Supported image format => PNG

RGB LSB (Least Significant Bit) explaination

Pixel in RGB

A single pixel has 3 channels (red, green, blue):

Pixel →  [  R  |  G  |  B  ]
          8bit   8bit   8bit

Each channel is 1 byte (8 bits). Example (real byte values):

R = 10110011
G = 11001001
B = 01101110

Least Significant Bit (LSB)

The rightmost bit is the LSB, the least important visually:

# Pixel RGB LSBs

R = 1011001[1]   <- LSB
G = 1100100[1]   <- LSB
B = 0110111[0]   <- LSB

Embedding data

Each LSB can be replaced with one bit of the secret message:

Secret message bits →  1   0   1
                       |   |   |
Pixel RGB LSBs      → [1] [1] [0]
                       ↓   ↓   ↓
Modified pixel      →  1011001[1]
                       1100100[0]
                       0110111[1]

Capacity

  • 1 pixel = 3 channels = 3 usable bits for hiding data.
  • 8 pixels ≈ 3 hidden bytes.

LCG - Linear Congruental Generator explaination

Sequential embedding (no permutation)

If we wrote bits one after another, they would fill pixels in order:

Pixel 1: R G B
Pixel 2: R G B
Pixel 3: R G B
...

Hidden bits go in the first LSBs, then continue linearly:

Message bits: 1 0 1 1 0 0 1 1 ...
Placed LSBs: [1][0][1][1][0][0][1][1] → predictable pattern

Permuted embedding (with LCG)

Instead of linear order, we use a Linear Congruential Generator (LCG) to jump through positions:

Start = random(seed)
Step  = coprime(N)
Next position = (current + Step) mod N

So message bits are placed in a scrambled order:

Message bits:  1   0   1   1   0   0   1   1
LCG positions: 17  243  58  400  7  129  99  512

Visualization across image:

[Pixel 1] [Pixel 2] [Pixel 3] ... [Pixel N]
   ·         ·         ·             ·
   ↓         ↓         ↓             ↓
   LSBs chosen according to LCG jumps

So instead of filling left-to-right, the bits are scattered across the image in a reproducible, pseudo-random way.

Workflow

Embedding flow

  1. Open the PNG image and take the “raw” colour channels. The programme loads the PNG, converts it to RGB and obtains an array of bytes: for each pixel there are 3 values (red, green, blue). We work directly on these bytes.

  2. Read the file to be hidden. It can be text, binary, any content.

  3. Compress the payload. Lossless compression is used to reduce the bytes to be written to the image (so fewer hidden bits are needed).

  4. Create two random numbers: ‘salt’ and ‘nonce’. Salt: 16 random bytes, used to make the derivation of the key from the password unique. Nonce: 12 random bytes, used by the cipher to never reuse the same encryption sequence.

  5. Derive the secret key from the password. A 32-byte key is calculated from the password and salt using a PBKDF2-HMAC-SHA256 (200k) algorithm (many rounds of computation) to resist attempts to guess the password.

  6. Encrypts compressed data with authentication. The encryption produces two things in a single block: the encrypted data (unreadable), an authentication tag (16 bytes) that allows, during extraction, to verify that no one has altered the data or used the wrong password.

  7. Prepare a small 43-byte header. It contains: a fixed signature (to recognise the format), the version, the salt, the nonce and the length of the encrypted data. This is used by the extractor to know how to proceed.

  8. Check the image capacity. Each LSB (least significant bit) of each channel can carry 1 bit. We verify that the image has enough ‘space’ for the header + encrypted data, while also respecting a percentage limit (so as not to touch too many bits).

  9. Write the header in the very first LSBs of the image. In practice, take the first 43 * 8 channels and replace their least significant bit to encode the header, byte by byte.

  10. Calculate a “permutation” of the remaining LSBs. From the key and the salt, we obtain a pseudo-random seed; from this seed, we obtain the start and step of a linear generator (LCG) that “jumps” through the image channels, generating a scattered and reproducible order.

  11. Write the encrypted payload bits following that scattered order. For each bit of encrypted data, take the next position given by the LCG; if the position falls within the area already used by the header, skip it. This way, the payload bits are distributed throughout the image in a non-sequential manner.

  12. Save the PNG (lossless). The resulting image appears the same to the human eye; only a few LSBs have changed.

Extract flow

  1. Open the PNG and obtain the RGB channels.

  2. Read the first 43 * 8 LSB and reconstruct the header. Extract signature, version, salt, nonce, encrypted data length.

  3. Verify that the header is valid. If the signature or version does not match, it is not a file created by this system (or it is corrupted).

  4. Recalculate the key from the password and salt. Same procedure as for embedding: obtain the same 32-byte key.

  5. Recalculate the permutation (same seed, same order). From the recalculated seed, obtain the start and step of the LCG.

  6. Collect the bits of the encrypted payload following the LCG order, skipping the header area. Reassemble the bits into bytes until you reach the length indicated in the header.

  7. Decrypt and verify integrity. Decryption uses the header nonce and key. If even 1 bit has changed or the password is incorrect, verification fails (invalid tag) and extraction stops.

  8. Decompress the data. Return to the original content.

  9. Write the output file.

Flowchart

%%{init: {'themeVariables': { 'fontSize': '10px', 'lineHeight': '12px' }, 'flowchart': { 'rankSpacing': 20, 'nodeSpacing': 20 }}}%%
flowchart LR
    subgraph EMBED["EMBED"]
        E1[Open PNG → extract RGB bytes] --> E2[Load file to hide]
        E2 --> E3[Compress payload with zlib]
        E3 --> E4[Derive 256-bit key via PBKDF2 + salt]
        E4 --> E5[Encrypt payload with AES-GCM + nonce]
        E5 --> E6[Build 43B header - magic, salt, nonce, length]
        E6 --> E7[Check image capacity vs data size]
        E7 --> E8[Write header bits into first LSBs]
        E8 --> E9[Generate permutation seed with HKDF]
        E9 --> E10[Compute LCG start/step for scatter]
        E10 --> E11[Embed ciphertext bits across image]
        E11 --> E12[Save new PNG - lossless]
    end

    subgraph EXTRACT["EXTRACT"]
        direction TB
        X1[Open PNG → extract RGB bytes] --> X2[Read 43B header from LSBs]
        X2 --> X3[Check magic, version, and data length]
        X3 --> X4[Derive 256-bit key via PBKDF2 + salt]
        X4 --> X5[Recreate permutation seed HKDF + LCG params]
        X5 --> X6[Collect ciphertext bits via LCG skipping header]
        X6 --> X7[Decrypt with AES-GCM - verify tag]
        X7 --> X8[Decompress payload with zlib]
        X8 --> X9[Write recovered file]
    end

    EMBED -.-> EXTRACT
Loading

Why these choices?

  • Header at the beginning: allows the extractor to know the size, salt and nonce without guessing.
  • Permutation of payload bits: avoids a linear pattern and makes it more difficult to identify modified bits.
  • Least significant bits (LSB): changing the last bit of the colour alters the pixel very little, so the image looks identical.
  • Encryption with authentication: ensures confidentiality (no one reads it) and integrity/authenticity (alterations and incorrect passwords are detected).
  • Compression before encryption: reduces the space occupied and makes the data more “random” before insertion.

Project layout

PixelCloak/
├─ embed/
│ ├─ pixelcloak_embed.py
│ └─ requirements.txt
├─ extractor/
│ ├─ vcpkg/ # vcpkg clone (created by bootstrap)
│ └─ src/
│   ├─ pixelcloak_extract.cpp
│   ├─ lodepng.h
│   └─ lodepng.cpp
├─ scripts/
│ ├─ bootstrap_kali.sh
│ └─ build_windows.sh
├─ dist/ # build artifacts (extractor .exe)
└─ README.md

Quick start

1) Bootstrap (Kali/Ubuntu host)

Installs toolchains, clones vcpkg into extractor/vcpkg, installs static OpenSSL/zlib, fetches LodePNG, sets up Python venv.

bash scripts/bootstrap_kali.sh

2) Build Windows extractor (static, PBKDF2)

Outputs dist/pixelcloak_extract.exe.

bash scripts/build_windows.sh

# [*] Building PixelCloak extractor (static, PBKDF2)…
# [OK] → /home/user/projects/PixelCloak/dist/pixelcloak_extract.exe

3) Embed (Python)

# usage: pixelcloak_embed.py -i <cover.png> -p <payload> -o <stego.png> -k <pass>
#        [--max-ratio 0.30]

source .venv/bin/activate

python embed/pixelcloak_embed.py -i cover.png -p secret.bin -o stego.png -k "pass"

# for large payload - 1 = 100%
python embed/pixelcloak_embed.py -i cover.png -p secret.bin -o stego.png -k "pass" --max-ratio 1 

deactivate

Notes:

  • Use true PNG (no alpha; script converts to RGB). Avoid editors that resave with lossy or palette changes.
  • The stego capacity of an image is approximately (width × height × 3) / 8 bytes, since each RGB channel can hide one bit and eight bits form a byte.
  • Out of this capacity, 43 bytes are reserved for the header (magic, version, salt, nonce, length), while the remaining space is used by the encrypted payload, which contains the compressed data and the authentication tag.
  • The max-ratio parameter defines the maximum fraction of the image’s available LSB capacity that can be altered to embed data, limiting visual artifacts and reducing the risk of detection.

4) Extract (Windows)

# Usage: extract_png.exe -i <stego.png> -k <pass> [-o output.bin]

.\pixelcloak_extract.exe -i stego.png -k "pass" -o payload.bin

Notes:

  • Static MinGW build; no external OpenSSL providers required.

Errors:

  • invalid header (MAGIC/VER) → not a PixelCloak image.
  • GCM tag invalid → wrong password or corrupted image.
  • inflate failed → corrupted ciphertext/plain.

Screenshots

Demo Usage

Build details

Toolchain

  • Host: Linux (Kali/Ubuntu) with mingw-w64
  • Deps via vcpkg (cloned in extractor/vcpkg):
    • openssl:x64-mingw-static
    • zlib:x64-mingw-static
  • LodePNG single-file (extractor/src/lodepng.*)

Manual compile (reference)

x86_64-w64-mingw32-g++ -std=c++17 -O2 -s -static -static-libstdc++ -static-libgcc -Wno-deprecated-declarations \
  extractor/src/pixelcloak_extract.cpp extractor/src/lodepng.cpp \
  -I extractor/vcpkg/installed/x64-mingw-static/include \
  -L extractor/vcpkg/installed/x64-mingw-static/lib \
  -lcrypto -lz -lcrypt32 -lws2_32 -lbcrypt \
  -o dist/pixelcloak_extract.exe

Troubleshooting

  • unrealistic cipher_len: image not produced by PixelCloak, or header LSBs damaged.
  • GCM tag invalid: wrong key or image altered (resave, resize, filters).
  • Ensure the same password used for embedding.
  • Do not pass images through tools that recompress/quantize (e.g., messaging apps).

About

Simple way to hide information inside pictures and retrieve it on demand without altering their appearance. Helpful for confidential communication and cybersecurity education.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors