Educational cryptographic project demonstrating image encryption through pixel permutation, chaotic key generation, and XOR ciphering.
- Overview
- How It Works
- Encryption Pipeline
- Decryption Pipeline
- Project Structure
- Installation
- Usage
- Example Output
- Security Discussion
- License
IES combines three classical cryptographic building blocks to encrypt images so that the encrypted result appears as random noise, while the original can be perfectly reconstructed with the correct key.
original.png ──▶ permute pixels ──▶ chaotic XOR ──▶ encrypted.png
▲ ▲
│ │
secret key secret key
Concept:
Rather than processing pixels in order, we shuffle them to a random arrangement before encryption. This destroys spatial correlation—edges, shapes, and colour gradients all disappear.
Implementation:
- Flatten the
(H × W × 3)image array into a list ofH×WRGB triples. - Use a seeded
numpy.random.default_rng(PCG64) to generate a permutationPof indices[0, H×W). - Reorder:
out[i] = in[P[i]]. - Reshape back to
(H, W, 3).
Reversal:
Compute the inverse permutation inv_P where inv_P[P[i]] = i, then apply it to restore the original order.
P = [3, 0, 2, 1] # shuffle
inv_P = [1, 3, 2, 0] # inverse: inv_P[P[i]] = i
Why chaotic maps?
Chaotic systems exhibit:
- Sensitivity to initial conditions — a difference of
10⁻¹⁵in the starting value produces a completely different sequence (butterfly effect). - Determinism — given the same parameters, the sequence is exactly reproducible.
- Pseudo-randomness — the output passes basic statistical randomness tests.
The Logistic Map:
x(n+1) = r · x(n) · (1 − x(n))
For r ∈ (3.57, 4.0] the system enters full chaos: the orbit is aperiodic, dense, and ergodic over (0, 1).
Key derivation:
- Hash the secret key string → two sub-hashes.
- Map to
x₀ ∈ (0.001, 0.999)andr ∈ (3.570, 4.000). - Discard 1 000 warm-up iterations to escape transient behaviour.
- Collect
H × W × 3float values, scale to[0, 255], cast touint8.
Principle:
XOR (⊕) satisfies:
ciphertext = plaintext ⊕ key
plaintext = ciphertext ⊕ key ← same operation!
Because XOR is its own inverse, the same function handles both encryption and decryption. Every byte of the permuted image is XOR'd with the corresponding byte from the chaotic key stream.
Why it works visually:
Even a single bit difference in the key byte flips bits in the cipher byte unpredictably. Combined with the chaotic stream, the result is statistically indistinguishable from random noise.
┌─────────────────────────────────────────────────────────────┐
│ ENCRYPTION PIPELINE │
├─────────────────────────────────────────────────────────────┤
│ │
│ original.png │
│ │ │
│ ▼ │
│ [1] image_loader.load_image() │
│ │ → NumPy array (H, W, 3) uint8 │
│ ▼ │
│ [2] permutation.permute_pixels(image, key) │
│ │ → spatial structure destroyed │
│ ▼ │
│ [3] chaotic_map.generate_image_key(x₀, r, shape) │
│ │ → uint8 key array of same shape │
│ ▼ │
│ [4] encrypt.xor_encrypt(permuted, key) │
│ │ → each byte XOR'd with key byte │
│ ▼ │
│ encrypted.png │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DECRYPTION PIPELINE │
├─────────────────────────────────────────────────────────────┤
│ │
│ encrypted.png │
│ │ │
│ ▼ │
│ [1] image_loader.load_image() │
│ ▼ │
│ [2] chaotic_map.generate_image_key(x₀, r, shape) │
│ │ ← identical parameters re-generated │
│ ▼ │
│ [3] encrypt.xor_encrypt(encrypted, key) ← XOR again │
│ │ → XOR cancels itself: A⊕K⊕K = A │
│ ▼ │
│ [4] permutation.inverse_permute_pixels(result, key) │
│ │ → pixels restored to original positions │
│ ▼ │
│ decrypted.png (pixel-identical to original) │
└─────────────────────────────────────────────────────────────┘
image-encryption-system/
│
├── image_loader.py # Load / save images as NumPy arrays
├── chaotic_map.py # Logistic map + chaotic key generation
├── permutation.py # Deterministic pixel permutation & inverse
├── encrypt.py # Full encryption pipeline + CLI
├── decrypt.py # Full decryption pipeline + CLI + verification
├── example.py # Interactive menu (main entry point)
│
├── images/
│ ├── original.png # Source image (example)
│ ├── encrypted.png # Encrypted output (looks like noise)
│ └── decrypted.png # Reconstructed image (matches original)
│
└── README.md
# Clone the repository
git clone https://github.com/yourusername/image-encryption-system.git
cd image-encryption-system
# Install dependencies
pip install numpy pillowNo other dependencies required.
python example.py ██╗███████╗███████╗
██║██╔════╝██╔════╝
██║█████╗ ███████╗
██║██╔══╝ ╚════██║
██║███████╗███████║
╚═╝╚══════╝╚══════╝
Image Encryption System v1.0
Pixel Permutation · Chaotic Maps · XOR
──────────────────────────────────────────────
[1] Encrypt an image
[2] Decrypt an image
[3] Run full demo (generate → encrypt → decrypt → verify)
[4] Verify reconstruction
[5] About / How it works
[0] Exit
──────────────────────────────────────────────
# Encrypt
python encrypt.py path/to/photo.png images/encrypted.png --key "my-secret-key"
# Decrypt
python decrypt.py images/encrypted.png images/decrypted.png --key "my-secret-key"from encrypt import encrypt_image
from decrypt import decrypt_image, verify_reconstruction
# Encrypt
encrypt_image("images/original.png", "images/encrypted.png", secret_key="my-key")
# Decrypt
decrypt_image("images/encrypted.png", "images/decrypted.png", secret_key="my-key")
# Verify
result = verify_reconstruction("images/original.png", "images/decrypted.png")
print(result)
# {'match': True, 'max_difference': 0, 'mse': 0.0, 'psnr_db': inf}| Original | Encrypted | Decrypted |
|---|---|---|
![]() |
![]() |
![]() |
Encrypted image appears as random noise — no structure, shapes, or colours from the original are visible. Decrypted image is pixel-identical to the original (PSNR = ∞ dB, MSE = 0).
| Property | Description |
|---|---|
| Confusion | XOR with a chaotic stream obscures byte values |
| Diffusion | Pixel permutation spreads spatial information |
| Key sensitivity | Logistic map is highly sensitive to x₀ and r |
| Determinism | Same key always reconstructs the same image |
| Weakness | Explanation |
|---|---|
| Logistic map is not a CSPRNG | It has detectable statistical biases and is not cryptographically secure |
| No authentication | An attacker can tamper with the encrypted image undetected |
| Key derivation is weak | A polynomial hash is not a proper KDF (no salt, no iterations) |
| Susceptible to chosen-plaintext attacks | With enough image pairs an attacker can recover key parameters |
- Replace logistic map with
secretsmodule oros.urandom()+ AES-256 in CTR mode. - Add authentication using AES-GCM or ChaCha20-Poly1305 (AEAD ciphers).
- Use a proper KDF — PBKDF2-HMAC-SHA256 with salt and ≥ 100 000 iterations.
- Implement block cipher modes — CBC or GCM prevents pattern leakage in uniform regions.
# Production-grade alternative (Python stdlib)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
nonce = os.urandom(12)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, associated_data=None)MIT License. Free for educational and portfolio use.
Built for educational purposes in applied cryptography.
Do not use this system to protect sensitive data in production.


