Date: 2026-08-31 • Status: DESIGN PHASE (pre-implementation review)
Purpose: Add value diffusion to v0.5 permutation for genuine cryptographic security (requirement 4)
v0.5 (permutation-only): blocks are shuffled but pixel values unchanged → vulnerable to edge-continuity reconstruction attacks.
v1 (permutation + diffusion): after shuffling blocks, XOR each pixel byte with a keyed offset stream → makes reconstruction exponentially harder.
Speed: Still pixel-domain (no coefficient tooling), full decode/encode cycle as in v0.5.
Security: Meaningfully satisfies requirement 4 ('hard to decode without the key').
Throwaway: Diffusion logic gets rewritten (not ported) at v1.5 for coefficient domain. This is deliberate.
1. Load image → numpy array
2. Snap region to 8×8 block grid
3. Generate deterministic permutation from KDF-derived perm_key
4. Permute blocks: rearrange pixels in the region
1. After permutation, iterate over every pixel in the region (raster order)
2. For each pixel channel (R, G, B), XOR with byte from deterministic stream
3. Outside region: leave pixels untouched
Formula: new_pixel = old_pixel XOR stream_byte
v0.5: KDF → (perm_key_32B + check_key_32B)
v1: KDF → (perm_key_32B + diffusion_key_32B)
Two independent keys prevent cross-contamination between permutation PRNG and diffusion stream. Same HMAC-SHA256 counter-mode infrastructure, different keys and domains.
Same DeterministicStream class as permutation
Key: diffusion_key (32 bytes from KDF)
Domain: 'jpeg-obscura/v1/diffusion' (domain separation from v0.5)
Output: Counter-mode HMAC-SHA256 producing 64-byte blocks
Consumption: One byte per pixel channel in raster order
Raster order (top-to-bottom, left-to-right):
for y in [region_y .. region_y+region_h): for x in [region_x .. region_x+region_w): for ch in [R,G,B]: pixel[y,x,ch] ^= stream.byte()
Deterministic, reproducible, order-independent (same region always produces same diffusion regardless of when scramble ran).
Forward (scramble): Permute blocks → Diffuse bytes → Save JPEG
Backward (restore): Load JPEG → Un-diffuse bytes (XOR with same stream) → Un-permute blocks
Un-diffuse is identical operation (XOR is self-inverse).
Stored in metadata:
• schema_version=0: v0.5 (permutation only)
• schema_version=1: v1 (permutation + diffusion)
CLI auto-detects and routes to correct logic.
✓ Pixel values meaningfully obscured (XOR'd with random bytes)
✓ Edge-continuity attacks fail (pixel values don't match across shuffled blocks)
✓ Genuine key-dependent security (requires breaking KDF or finding XOR plaintext)
✓ Fast (pixel domain, no coefficient tooling needed)
✗ Byte-exact restoration on re-encode (JPEG lossy requantizes)
✗ Coefficient-domain security (vulnerable to FFT/DCT analysis at boundaries)
• diffuse_region(array, region, diffusion_key, block_size=8): XOR pixels with stream
• undiffuse_region(array, region, diffusion_key, block_size=8): Same function (XOR is self-inverse)
• kdf.py: Clarify output labels (perm_key, diffusion_key instead of perm_key, check_key)
• transform.py: Call diffuse_region() after permute_blocks() in scramble path
• cli.py: Schema version detection; route scramble/restore by version
• metadata.py: schema_version=1
• diffuse(diffuse(x)) == x (XOR is self-inverse) ✓
• Same key+region → same output (deterministic) ✓
• Different key → different output (key-dependent) ✓
Load image → Scramble (v1) → Save JPEG → Load → Restore → Compare pixels
Expected: Mean Δ ≤ 0.5 bytes (JPEG lossy baseline, same as v0.5)
Verify: Correct passphrase restores; wrong passphrase produces garbage
Create 3 new samples from different WebDAV images
Find main subject (face, building, art) and define region
Scramble + restore to demonstrate effectiveness
Keep v0.5 samples for version comparison
Same commands as v0.5:
jpeg-obscura scramble INPUT.jpg OUTPUT.jpg --region X,Y,W,H --passphrase PASS
jpeg-obscura restore INPUT.jpg OUTPUT.jpg --passphrase PASS --region X,Y,W,H --salt HEX
Internally: auto-detect schema_version, use v0.5 or v1 logic transparently.
□ Design review (this doc)
□ diffusion.py module
□ kdf.py: clarify output labels
□ transform.py: add diffuse call
□ cli.py: schema version routing
□ Unit tests (in-memory)
□ Integration tests (JPEG file)
□ 3 new sample images with restore demo
□ Git tag v1-implementation
1. Is global counter-mode stream (vs per-block seed) sufficient? → YES, proven in permutation
2. Defer metadata embedding to v1.5? → YES, maintain v0.5 compatibility model
3. Handle alpha channel? → YES, treat as 4th byte in RGBA