Open-source optical (perceptual) image encryption — for research and education.
OptiCipher turns an image into a real, viewable PNG that looks like pure noise, yet still contains every bit of the original. Pixels are spatially scrambled and their colour values are diffused with a stream cipher. Everything needed to decrypt (except your passphrase) is embedded inside the image itself, so a single PNG is fully self-contained. Decryption is bit-exact: you get back the original image, pixel for pixel.
Status: research / educational reference implementation. Read Threat model & security notes before using it to protect anything that genuinely matters.
- What "optical encryption" means here
- Is this idea new?
- How it works (the algorithm)
- The self-contained header
- Project layout
- Installation
- Quick start
- Command-line usage
- Library usage
- Research metrics
- Results on the bundled sample
- Threat model & security notes
- Running the tests
- Benchmarking
- Contributing
- License
A normal cipher turns data into an opaque blob of bytes. OptiCipher instead keeps the output as a valid image. The encrypted file is a PNG you can open in any viewer; it simply shows visual noise. Two things are scrambled:
| Stage | What it hides | Effect |
|---|---|---|
| Scramble (permutation) | where each colour is | destroys shapes, edges, text |
| Diffuse (stream cipher) | what each colour is | flattens the histogram |
Together they remove both the spatial structure and the colour statistics of the original, which is exactly what the standard "confusion + diffusion" principle of cipher design asks for.
The building blocks are well established; the specific combination here is the interesting part. Image-encryption research dates back to the 1990s. You will find extensive prior work on:
- Pixel permutation / scrambling — Arnold's Cat Map, chaotic maps, etc.
- Value diffusion — XOR with chaotic or cryptographic keystreams.
- Visual / perceptual encryption — keeping the ciphertext as an image.
- LSB steganography and watermarking — hiding data inside images.
What OptiCipher emphasises, and what is comparatively uncommon in one tidy package, is being fully self-contained and authenticated:
- A modern, audited stream cipher (ChaCha20) for diffusion rather than a home-rolled chaotic map.
- A size-agnostic keyed permutation (Fisher–Yates) that works for any width × height — no square-image restriction like Arnold's Cat Map.
- All decryption metadata (salt, iterations, dimensions) embedded inside the image, so there is no separate key file to manage.
- HMAC-SHA256 integrity, so a wrong passphrase or any tampering is detected instead of silently producing garbage.
So: not a new primitive, but a clean, documented, reproducible synthesis that is pleasant to study and to build on.
ENCRYPT
passphrase
│
┌───────────▼───────────┐
random salt ──►│ PBKDF2-HMAC-SHA256 │ (keys.py)
└───────────┬───────────┘
│ stream_key │ scramble_seed │ mac_key
original ─► SCRAMBLE positions ─► DIFFUSE values ─► HMAC ─► append header strip
(H×W×C) (scramble.py) (diffusion.py) (core) (stego.py)
│
▼
self-contained ciphertext PNG
(H+strip rows × W × C)
DECRYPT
ciphertext ─► read+parse header ─► re-derive keys ─► verify HMAC
(stego/header) (keys.py) (core.py)
│ (fails fast on wrong passphrase / tampering)
▼
remove strip ─► UN-DIFFUSE ─► UN-SCRAMBLE ─► exact original
Step by step:
-
Key derivation (
keys.py). Your passphrase plus a fresh random 16-byte salt are stretched by PBKDF2-HMAC-SHA256 (default 200,000 iterations) into 64 bytes, split into three role-specific sub-keys: a 32-byte stream key, a permutation seed, and a 16-byte MAC key. Same passphrase + salt ⇒ identical keys, which is what makes decryption possible. -
Scramble (
scramble.py). A keyed Fisher–Yates shuffle permutes pixel positions (colours travel with their pixel). It works for any image shape and is perfectly invertible. -
Diffuse (
diffusion.py). Every colour byte is XOR-ed with a ChaCha20 keystream. Because each image gets a unique key (fresh salt), reusing a constant-derived nonce is safe. XOR is its own inverse, so the same routine encrypts and decrypts. -
Authenticate + embed (
core.py,header.py,stego.py). An HMAC-SHA256 tag is computed over the header fields and the ciphertext ("encrypt-then-MAC"). The header is then written into a small reserved strip of extra pixel rows appended to the bottom of the image.
Decryption reverses everything and refuses to proceed unless the HMAC verifies.
Every encrypted image carries a 70-byte header in a thin appended strip:
| Field | Size | Purpose |
|---|---|---|
magic OPTC |
4 | identify an OptiCipher file |
| version | 1 | format versioning |
| salt | 16 | re-derive the keys |
| iterations | 4 | PBKDF2 cost actually used |
| height / width | 4 + 4 | original dimensions |
| channels | 1 | 3 (RGB) or 4 (RGBA) |
| strip_rows | 4 | how many rows to remove on decrypt |
| HMAC-SHA256 | 32 | integrity / passphrase check |
The salt and iteration count are not secret — exactly like the plaintext salt stored next to a password hash. All confidentiality comes from the passphrase.
Why a reserved strip and not LSB steganography? Hiding the header in the least-significant bits of the ciphertext creates a self-referential sizing paradox (the bits you overwrite are themselves ciphertext that must be restored exactly). A dedicated strip sidesteps that entirely and guarantees a lossless round trip. The trade-off is a few extra pixel rows — invisible in practice.
OptiCipher/
├── opticipher/ # the library
│ ├── __init__.py # public API
│ ├── __main__.py # enables `python -m opticipher`
│ ├── keys.py # PBKDF2 key derivation
│ ├── scramble.py # keyed pixel permutation
│ ├── diffusion.py # ChaCha20 value diffusion
│ ├── header.py # binary header layout
│ ├── stego.py # reserved-strip storage
│ ├── core.py # encrypt/decrypt orchestration + HMAC
│ ├── io_image.py # PNG load/save helpers
│ ├── metrics.py # entropy, correlation, NPCR, UACI
│ └── cli.py # command-line interface
├── tests/ # pytest suite (run with `pytest`)
│ ├── test_roundtrip.py
│ ├── test_components.py
│ └── test_metrics_and_sample.py
├── samples/ # the bundled "CLAUDE CODE" sample + outputs
│ ├── sample_claude_code.png
│ ├── sample_encrypted.png
│ └── sample_decrypted.png
├── examples/ # runnable demos
│ ├── demo.py
│ └── benchmark.py
├── docs/ # extra documentation
│ ├── ALGORITHM.md
│ └── METRICS.md
├── pyproject.toml # packaging / install metadata
├── requirements.txt
├── LICENSE # MIT
├── CONTRIBUTING.md
└── README.md
Requires Python 3.9+.
git clone https://github.com/bio-colab/OptiCipher.git
cd OptiCipher
# Option A: install the package (gives you the `opticipher` command)
pip install -e .
# Option B: just install the dependencies and run in place
pip install -r requirements.txtDependencies are minimal and widely used: NumPy, Pillow, cryptography.
# Encrypt (you'll be prompted for a passphrase, twice)
opticipher encrypt samples/sample_claude_code.png encrypted.png
# Decrypt
opticipher decrypt encrypted.png recovered.png
# See the statistics for any image
opticipher metrics encrypted.pngOr run the guided demo:
python examples/demo.pyopticipher encrypt <input> <output> [--passphrase PASS] [--iterations N]
opticipher decrypt <input> <output> [--passphrase PASS]
opticipher metrics <input>
- Omit
--passphraseto be prompted securely (no echo, no shell history). --iterationslets you tune the PBKDF2 cost (default 200,000). The value is stored in the image so decryption uses the same count automatically.- Output is always written as lossless PNG (a
.pngsuffix is enforced), because a lossy format like JPEG would destroy the ciphertext.
import numpy as np
from opticipher import load_image, save_image, encrypt_image, decrypt_image
# Load any image as a uint8 (H, W, C) array
img = load_image("photo.png")
# Encrypt
enc = encrypt_image(img, "my strong passphrase")
save_image(enc, "photo_encrypted.png")
# Decrypt (bit-exact)
dec = decrypt_image(load_image("photo_encrypted.png"), "my strong passphrase")
assert np.array_equal(img, dec)A wrong passphrase or a tampered file raises opticipher.DecryptionError.
opticipher.metrics implements the standard image-encryption diagnostics so you
can quantify quality rather than just eyeballing the noise. See
docs/METRICS.md for the formulas and interpretation.
| Metric | Function | Ideal for ciphertext |
|---|---|---|
| Shannon entropy | entropy_report |
≈ 8.0 bits/byte |
| Adjacent-pixel correlation | adjacent_correlation |
≈ 0 |
| NPCR (pixel change rate) | npcr |
≈ 99.6 % |
| UACI (intensity change) | uaci |
≈ 33.4 % |
| Histogram | histogram |
flat |
Measured on samples/sample_claude_code.png (322 × 620 RGB). Reproduce with
python examples/demo.py.
| Metric | Original | Encrypted | Ideal |
|---|---|---|---|
| Mean Shannon entropy (bits/byte) | 2.475 | 7.999 | 8.000 |
| Correlation, horizontal (ch0) | 0.941 | ≈ 0.00 | 0 |
| Correlation, vertical (ch0) | 0.923 | ≈ 0.02 | 0 |
| Correlation, diagonal (ch0) | 0.870 | ≈ 0.01 | 0 |
| NPCR (1-pixel change) | — | ≈ 99.6 % | 99.6 % |
| UACI (1-pixel change) | — | ≈ 33.5 % | 33.4 % |
| Round-trip | — | bit-exact | exact |
Visually:
| Original | Encrypted | Decrypted |
|---|---|---|
sample_claude_code.png |
sample_encrypted.png (noise) |
sample_decrypted.png (identical) |
OptiCipher is an honest, well-documented reference implementation. Please read this section before trusting it with anything important.
What it provides
- Confidentiality of image content against someone without the passphrase, via ChaCha20 diffusion keyed by PBKDF2.
- Integrity / authenticity via HMAC-SHA256 (encrypt-then-MAC). Tampering or a wrong passphrase is detected.
- Per-image uniqueness via a fresh random salt, so identical plaintexts do not produce identical ciphertexts.
Known limitations and honest caveats
- PBKDF2 is memory-cheap, so it is comparatively weak against GPU/ASIC brute-force. For high-value secrets, a memory-hard KDF (Argon2id, scrypt) would be stronger. PBKDF2 is used here to keep dependencies minimal and the code readable; the iteration count is exposed and benchmarked.
- The permutation is driven by NumPy's PRNG seeded from key material. It provides spatial confusion and contributes to the statistics, but the cryptographic confidentiality guarantee rests on ChaCha20, not on the permutation alone.
- The statistical metrics (entropy, correlation, NPCR, UACI) are necessary but not sufficient indicators. Passing them does not constitute a security proof.
- This project has not had a formal third-party cryptographic audit. Do not use it as the sole protection for safety-critical or legally sensitive data.
If you are a cryptographer or security researcher, contributions that harden or formally analyse the scheme are very welcome — see below.
pip install -e ".[dev]" # installs pytest
pytest -vThe suite (34 tests) covers each component in isolation, full encrypt→decrypt round trips across many image shapes (including RGBA and degenerate 1×N images), Unicode passphrases, wrong-passphrase rejection, tamper detection, and the statistical behaviour on the real sample image.
python examples/benchmark.pyPrints encryption/decryption throughput across image sizes, the isolated PBKDF2
cost, and the full statistical suite, and writes
examples/benchmark_results.json for further analysis.
OptiCipher is open source and community contributions are warmly invited.
Whether you are a student, a security researcher, or just curious, there is room
to help. See CONTRIBUTING.md for details. Good first issues
include:
- Adding Argon2id as an optional KDF.
- Adding new metrics (e.g. χ² histogram uniformity test, local Shannon entropy, key-sensitivity analysis).
- A GUI or web front-end over the library.
- Performance work (vectorisation, optional native backend).
- More sample images and reproducible metric reports.
- Independent security analysis and write-ups.
Please open an issue to discuss substantial changes first, keep the heavy commenting style consistent with the existing code, and add tests for new behaviour.
Released under the MIT License — you are free to use, modify, and
redistribute it, including commercially, provided the copyright and licence
notice are retained. See LICENSE.