-
Notifications
You must be signed in to change notification settings - Fork 0
File Format
Jonas Resch edited this page Nov 27, 2025
·
1 revision
QuantHide uses a custom binary format for storing encrypted data within images. This page documents the internal structure.
┌────────────────────────────────────────────────────────┐
│ Encoded Image │
├────────────────────────────────────────────────────────┤
│ Image Pixels (RGB) │
│ └── LSB contains: ┌─────────────────────────────────┐ │
│ │ Header │ Encrypted Payload │Pad │ │
│ └─────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 bytes | Magic |
0x51 0x48 0x49 0x44 ("QHID") |
| 4 | 1 byte | Version | Format version (currently 0x01) |
| 5 | 1 byte | Flags | Bit flags for options |
| 6 | 4 bytes | Length | Encrypted payload size (big-endian) |
| 10 | 32 bytes | Salt | Argon2id salt |
| 42 | 32 bytes | Nonce | ChaCha20-Poly1305 nonce |
Total Header Size: 74 bytes
| Bit | Meaning |
|---|---|
| 0 | Data type: 0 = text, 1 = file |
| 1 | Has decoy message |
| 2 | Is split part |
| 3 | Reserved |
| 4-7 | Reserved |
The payload is encrypted with ChaCha20-Poly1305:
┌─────────────────────────────────────────┐
│ Kyber Ciphertext (1568 bytes) │
├─────────────────────────────────────────┤
│ Encrypted Data │
│ ├── [If file] Filename length (2 bytes) │
│ ├── [If file] Filename (UTF-8) │
│ ├── [If file] File size (8 bytes) │
│ └── Actual content (text or file data) │
├─────────────────────────────────────────┤
│ Authentication Tag (16 bytes) │
└─────────────────────────────────────────┘
After the payload, random bytes are appended to:
- Hide the actual data size
- Fill remaining image capacity
- Prevent size-based analysis
Data is embedded in the Least Significant Bits of pixel color channels:
Original Pixel: R: 10110100 G: 11001010 B: 01110011
↓ ↓ ↓
Data bits: 1 0 1
↓ ↓ ↓
Modified Pixel: R: 10110101 G: 11001010 B: 01110011
- Default: 2 bits per channel (6 bits per pixel)
- Low capacity mode: 1 bit per channel
- High capacity mode: 4 bits per channel (more visible)
When decoy mode is enabled:
┌─────────────────────────────────────────┐
│ Standard Header (decoy flag set) │
├─────────────────────────────────────────┤
│ Decoy Message (encrypted with decoy pw) │
├─────────────────────────────────────────┤
│ Separator (encrypted marker) │
├─────────────────────────────────────────┤
│ Real Message (encrypted with real pw) │
├─────────────────────────────────────────┤
│ Random Padding │
└─────────────────────────────────────────┘
For split images, each part contains:
┌─────────────────────────────────────────┐
│ Header (split flag set) │
├─────────────────────────────────────────┤
│ Part Index (1 byte) │
├─────────────────────────────────────────┤
│ Total Parts (1 byte) │
├─────────────────────────────────────────┤
│ Data Fragment (encrypted) │
├─────────────────────────────────────────┤
│ Fragment Checksum (32 bytes) │
└─────────────────────────────────────────┘
- ✅ Message content (encrypted)
- ✅ File content (encrypted)
- ✅ Filename (encrypted)
- ✅ Data length (padded)
- ✅ Metadata (stripped)
⚠️ Presence of QuantHide data (magic bytes)⚠️ Format version⚠️ Whether it's text or file (flag bit)
Note: A forensic analyst with the right tools could detect that an image contains QuantHide data, but cannot read the content without the password.
| Version | Changes |
|---|---|
| 0x01 | Initial release, Kyber1024 + ChaCha20-Poly1305 |
// Magic bytes check
const MAGIC: [u8; 4] = [0x51, 0x48, 0x49, 0x44]; // "QHID"
// Version check
const CURRENT_VERSION: u8 = 0x01;
// Minimum image size for header
const MIN_PIXELS: usize = 74 * 8 / 6; // ~99 pixels minimum- Forward compatible: newer versions can read older formats
- Backward compatible: older versions reject newer formats gracefully
- Cross-platform: identical format on Windows, macOS, Linux