Hide any file inside PNG/BMP/TIFF/WebP — compress, encrypt, embed. Rust-powered steganography engine.
Project description
png-parser
Hide any file inside an image — compress, encrypt, embed.
A high-performance steganography engine written in Rust, available for Python and JavaScript (WebAssembly). Hide any file — PDF, video, ZIP, binary — inside a PNG, BMP, TIFF, or WebP image. The carrier image looks identical to the original. No pixels are changed in chunk mode.
Table of Contents
- How it works
- Packages
- What's new in v0.3.0
- Python
- JavaScript / WASM
- Supported formats
- Security
- Embedding modes
- Expiry system
- Sharding
- CLI
- Build from source
- Architecture
- Versioning
- License
How it works
hide:
secret file
→ flate2 compress (rust_backend, pure Rust, WASM-safe)
→ AES-256-GCM encrypt
→ embed into image
→ save image
reveal:
stego image
→ extract payload
→ AES-256-GCM decrypt
→ flate2 decompress
→ restore original file
The original filename, file size, expiry, and embedding mode are stored inside the ciphertext — not in plaintext. An observer with the stego image but without the password sees none of this metadata.
Packages
| Platform | Install | Status |
|---|---|---|
| Python | pip install png-parser |
✅ Published |
| JS / WASM | npm i @pranjalpanging/png-parser |
✅ Published |
What's new in v0.3.0
| Feature | v0.2 | v0.3 |
|---|---|---|
| Payload type | Text strings only | Any file (PDF, ZIP, video, …) |
| Key derivation | PBKDF2-SHA256 (100k iter) | Argon2id (memory-hard, GPU-resistant) |
| Salt size | 16 bytes | 32 bytes |
| Compression | None | flate2 rust_backend (pure Rust, WASM-safe) |
| Image formats | PNG only | PNG, BMP, TIFF, WebP |
| Embed modes | Chunk only | Chunk + Pixel (adaptive LSB) |
| Expiry granularity | Hours only | Days + hours + minutes + seconds |
| Delete protection | Unprotected | Password-required for encrypted payloads |
| Secure erase | No | Payload zeroed before removal |
| New functions | — | info, verify, reencrypt, capacity, fingerprint, split, merge |
| Filename preservation | No | Original filename stored in header |
| Sharding | No | Split file across multiple images |
Python
Install
pip install png-parser>=0.3.0
Hide a file
import png_parser
# No password — plain compression only
png_parser.hide("photo.png", "out.png", "document.pdf")
# With encryption
png_parser.hide("photo.png", "out.png", "document.pdf",
password="my-password")
# With encryption + flexible expiry (all units are additive)
png_parser.hide("photo.png", "out.png", "document.pdf",
password="my-password",
expires_days=1,
expires_hours=6,
expires_minutes=30,
expires_seconds=0)
# Pixel mode — embeds in image pixels instead of a chunk
png_parser.hide("photo.png", "out.png", "document.pdf",
password="my-password",
mode="pixel")
Reveal a file
# output_path can be a directory — original filename is restored automatically
result = png_parser.reveal("out.png", "./extracted/",
password="my-password")
print(result)
# ./extracted/document.pdf
Inspect without extracting
import json
raw = png_parser.info("out.png", password="my-password")
meta = json.loads(raw)
print(meta["filename"]) # document.pdf
print(meta["file_size"]) # 204800
print(meta["encrypted"]) # True
print(meta["expires_at"]) # unix:1742000000 or permanent
print(meta["fingerprint"]) # a3f9c1d2...
print(meta["mode"]) # chunk or pixel
# Without password — only fingerprint and encrypted flag visible
raw = png_parser.info("out.png")
# {"encrypted": true, "fingerprint": "a3f9c1d2...", ...}
Verify a password
# Check without extracting anything
ok = png_parser.verify("out.png", "my-password")
print(ok) # True or False
Delete a payload
# Password required if payload is encrypted
# Data is zeroed in place before removal — not just stripped
png_parser.delete("out.png", "clean.png", password="my-password")
Change password
# Re-encrypts without writing plaintext to disk at any point
png_parser.reencrypt("out.png", "new.png",
old_password="old-password",
new_password="new-password")
Check capacity
# How many bytes can this image hold?
bytes_available = png_parser.capacity("photo.png", mode="chunk")
print(f"{bytes_available:,} bytes available")
bytes_available = png_parser.capacity("photo.png", mode="pixel")
print(f"{bytes_available:,} bytes available in pixel mode")
Fingerprint
# SHA-256 of the raw envelope — no password needed
# Useful to verify two images carry the same payload
fp = png_parser.fingerprint("out.png")
print(fp) # a3f9c1d2ee4b...
Split a large file across multiple images
# Each carrier image gets one shard
shards = png_parser.split(
"bigfile.zip",
["photo1.png", "photo2.png", "photo3.png"],
"./shards/",
password="my-password",
expires_days=7,
)
print(shards)
# ['./shards/shard_0_photo1.png',
# './shards/shard_1_photo2.png',
# './shards/shard_2_photo3.png']
Merge shards
# Shards can be passed in any order — sorted internally by index
# SHA-256 of reassembled file is verified against stored hash
output = png_parser.merge(
["./shards/shard_2_photo3.png",
"./shards/shard_0_photo1.png",
"./shards/shard_1_photo2.png"],
"./output/",
password="my-password",
)
print(output) # ./output/bigfile.zip
JavaScript / WASM
Install
npm i @pranjalpanging/png-parser
Initialise
import init, {
hide_js,
reveal_js,
info_js,
verify_js,
delete_js,
reencrypt_js,
fingerprint_js,
capacity_js,
} from '@pranjalpanging/png-parser';
await init();
Hide a file
// imageBytes and fileBytes are Uint8Array
const stego = hide_js(
imageBytes, // carrier image bytes
fileBytes, // file to hide
"secret.pdf", // original filename (stored in header)
"password", // encryption password — pass null for no encryption
"chunk", // mode: "chunk" or "pixel"
null, // expires_days
null, // expires_hours
null, // expires_minutes
null // expires_seconds
);
// stego is Uint8Array — the modified image bytes
Reveal a file
// Returns Uint8Array of the hidden file bytes
const fileBytes = reveal_js(stegoBytes, "password");
// Download in browser
const blob = new Blob([fileBytes]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "secret.pdf";
a.click();
URL.revokeObjectURL(url);
Inspect without extracting
// Returns JSON string
const meta = JSON.parse(info_js(stegoBytes, "password"));
console.log(meta.filename); // "secret.pdf"
console.log(meta.file_size); // 204800
console.log(meta.encrypted); // true
console.log(meta.expires_at); // "unix:1742000000" or "permanent"
console.log(meta.fingerprint); // "a3f9c1d2..."
Other functions
// Verify password
const ok = verify_js(stegoBytes, "password"); // true / false
// Delete payload (password required if encrypted)
const cleanBytes = delete_js(stegoBytes, "password");
// Change password
const newBytes = reencrypt_js(stegoBytes, "old-pass", "new-pass");
// Fingerprint (no password needed)
const fp = fingerprint_js(stegoBytes);
// Capacity
const bytes = capacity_js(imageBytes, "chunk");
Full browser example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>png-parser demo</title>
</head>
<body>
<h2>Hide a file inside an image</h2>
<label>Carrier image (PNG/BMP/TIFF/WebP)</label><br>
<input type="file" id="carrier" accept="image/*"><br><br>
<label>Secret file (any format)</label><br>
<input type="file" id="secret"><br><br>
<label>Password (optional)</label><br>
<input type="password" id="password" placeholder="Leave blank for no encryption"><br><br>
<label>Expiry (optional)</label><br>
<input type="number" id="days" placeholder="Days" style="width:80px">
<input type="number" id="hours" placeholder="Hours" style="width:80px">
<input type="number" id="minutes" placeholder="Minutes" style="width:80px"><br><br>
<button onclick="hideFile()">Hide and Download</button>
<button onclick="revealFile()">Reveal from image</button>
<p id="status"></p>
<script type="module">
import init, { hide_js, reveal_js } from './pkg/png_parser.js';
await init();
function status(msg) {
document.getElementById("status").textContent = msg;
}
function download(bytes, name, type = "image/png") {
const blob = new Blob([bytes], { type });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
URL.revokeObjectURL(a.href);
}
window.hideFile = async () => {
const carrier = document.getElementById("carrier").files[0];
const secret = document.getElementById("secret").files[0];
const password = document.getElementById("password").value || null;
const days = parseInt(document.getElementById("days").value) || null;
const hours = parseInt(document.getElementById("hours").value) || null;
const minutes = parseInt(document.getElementById("minutes").value) || null;
if (!carrier || !secret) {
return status("Please select both files.");
}
const carrierBytes = new Uint8Array(await carrier.arrayBuffer());
const secretBytes = new Uint8Array(await secret.arrayBuffer());
try {
const result = hide_js(
carrierBytes, secretBytes, secret.name,
password, "chunk",
days, hours, minutes, null
);
download(result, "stego_" + carrier.name);
status("Done — file hidden successfully.");
} catch (e) {
status("Error: " + e);
}
};
window.revealFile = async () => {
const carrier = document.getElementById("carrier").files[0];
const password = document.getElementById("password").value || null;
if (!carrier) return status("Please select a stego image.");
const carrierBytes = new Uint8Array(await carrier.arrayBuffer());
try {
const fileBytes = reveal_js(carrierBytes, password);
download(fileBytes, "revealed_file", "application/octet-stream");
status("Done — file revealed successfully.");
} catch (e) {
status("Error: " + e);
}
};
</script>
</body>
</html>
Supported formats
| Format | Chunk mode | Pixel mode | Method |
|---|---|---|---|
| PNG | ✅ | ✅ | Custom stEg ancillary chunk / adaptive LSB |
| BMP | ✅ | ✅ | Append after EOF / adaptive LSB |
| TIFF | ✅ | ✅ | Append after EOF / adaptive LSB |
| WebP | ✅ | ❌ | Unknown RIFF chunk |
| JPEG | ❌ | ❌ | Rejected — lossy format destroys data |
Security
| Component | Detail |
|---|---|
| Cipher | AES-256-GCM — authenticated encryption |
| KDF | Argon2id — memory-hard, resistant to GPU/ASIC attacks |
| Salt | 32 bytes, randomly generated per operation |
| Nonce | 12 bytes, randomly generated per operation |
| Compression | flate2 rust_backend — pure Rust, zero C dependencies |
| Metadata | Filename, expiry, mode stored inside ciphertext — tamper-proof |
| Integrity | GCM authentication tag detects any modification |
| Secure delete | Payload bytes zeroed in place before chunk removal |
| Password verify | Decrypt attempted — plaintext never returned on failure |
Two encryptions of the same file with the same password produce different ciphertext because salt and nonce are randomised every time.
Embedding modes
Chunk mode (default)
Inserts a custom ancillary PNG chunk named stEg just before IEND. Per the PNG specification, unknown ancillary chunks are silently skipped by all standard image viewers. The image pixels are never modified — the file is visually and statistically identical to the original.
For BMP and TIFF, data is appended after the image EOF with a magic marker. For WebP, a proper RIFF chunk is inserted.
Pixel mode
Embeds data into the least-significant bit of the R, G, B channels of high-texture pixels only. Flat, uniform regions are skipped entirely. This defeats simple statistical detectors such as chi-square analysis and RS analysis which look for LSB changes in smooth areas.
Use capacity() to check how many bytes are available before embedding.
Expiry system
Expiry is stored as a Unix timestamp inside the ciphertext. It is tamper-proof — an attacker cannot extend or remove the expiry without the password.
All time units are optional and additive:
# Expires in exactly 1 day, 6 hours, 30 minutes from now
png_parser.hide("photo.png", "out.png", "file.pdf",
password="x",
expires_days=1,
expires_hours=6,
expires_minutes=30)
When reveal() is called after expiry, a PermissionError is raised. The file bytes are never written to disk.
Sharding
Split a file too large for a single carrier across multiple images:
file.zip (300 MB)
→ shard 0 → photo1.png (100 MB payload)
→ shard 1 → photo2.png (100 MB payload)
→ shard 2 → photo3.png (100 MB payload)
Each shard header stores the shard index, total count, and SHA-256 of the full original file. merge() sorts shards by index, assembles them, and verifies the SHA-256 before writing output. Any missing or mismatched shard causes an error.
CLI
# Hide a file
png-parser-cli hide -i photo.png -f secret.pdf -o out.png \
-p password --days 1 --hours 6 --minutes 30 --mode chunk
# Reveal
png-parser-cli reveal -i out.png -o ./extracted/ -p password
# Inspect metadata
png-parser-cli info -i out.png -p password
# Verify password
png-parser-cli verify -i out.png -p password
# Delete payload (password required if encrypted)
png-parser-cli delete -i out.png -o clean.png -p password
# Change password
png-parser-cli reencrypt -i out.png -o new.png \
--old-password old --new-password new
# Check capacity
png-parser-cli capacity -i photo.png --mode chunk
png-parser-cli capacity -i photo.png --mode pixel
# Fingerprint
png-parser-cli fingerprint -i out.png
# Inspect PNG chunk structure
png-parser-cli inspect -i photo.png
# Strip metadata chunks (tEXt, zTXt, iTXt, eXIf)
png-parser-cli strip -i photo.png -o clean.png
# Split a file across multiple carriers
png-parser-cli split -f bigfile.zip \
-c photo1.png photo2.png photo3.png \
-o ./shards/ -p password --days 7
# Merge shards (any order)
png-parser-cli merge \
-i shards/shard_0_photo1.png \
shards/shard_1_photo2.png \
shards/shard_2_photo3.png \
-o ./output/ -p password
Build from source
Requirements:
- Rust stable (
rustup install stable) - Python 3.8+ with
pip install maturin(for Python build) npm install -g wasm-pack(for WASM build)
# Clone
git clone https://github.com/PranjalPanging/png-parser
cd png-parser
# Python — installs into current virtualenv
maturin develop --release
# WASM — outputs to pkg/
wasm-pack build --target web --release \
--no-default-features --features js
# CLI binary
cargo build --release --no-default-features
Architecture
src/
├── lib.rs Python + WASM API surface
├── main.rs CLI entry point
├── error.rs All error variants + Python exception mapping
├── chunk_type.rs 4-byte PNG chunk name + spec rules
├── chunk.rs PNG chunk: parse, CRC verify, secure erase
├── png.rs Full PNG file: parse, build, insert, remove
├── header.rs PayloadHeader — inside ciphertext, tamper-proof
├── crypto.rs Argon2id KDF + AES-256-GCM + zlib
├── format.rs Format detection + routing (PNG/BMP/TIFF/WebP)
├── pixel.rs Adaptive LSB engine
└── commands/
├── mod.rs
└── mode.rs All operations: hide, reveal, info, verify,
delete, reencrypt, capacity, fingerprint,
split, merge, inspect, strip
Versioning
This project follows Semantic Versioning.
| Package | Current | Registry |
|---|---|---|
| Python | 0.3.0 |
PyPI |
| JavaScript | 0.3.0 |
npm |
Python and JavaScript versions are kept in sync from v0.3.0 onwards.
| Version | Notes |
|---|---|
0.3.0 |
Full rewrite — any file type, Argon2id, multi-format, WASM, sharding |
0.2.0 |
AES-256-GCM encryption, expiry system |
0.1.0 |
Initial release |
Versions below 1.0.0 may have breaking changes between minor releases.
License
MIT — see LICENSE
Built with ❤️ by Pranjal Panging
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file png_parser-0.3.0-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: png_parser-0.3.0-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 569.2 kB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83a30e2fe08291df855f7588920d96ff76899f28556b4cb55cd7d8a612f872c3
|
|
| MD5 |
65b8c72c330c92ae8873cfb5d0aedad3
|
|
| BLAKE2b-256 |
4d41d2b5ee74dac0deaacf010cfe88438c4c6cb98430469ab44c747a2265c63a
|