Concept: scramble only part of an image (viewable outside that region) such that the region can be exactly restored with a key, but is hard to interpret without one. Originated from an idea to "reorder the numbers in a JPEG".
1. Non-lossy: pixels outside the scrambled region must decode identically to the original.
2. Region should look like noise/blur — no visible detail — not just rearranged tiles.
3. Region location may be detectable without the key (visibly noisy patch, or bounding box in metadata) — confirmed acceptable, does not need to be hidden.
4. Hard to decode without the key.
5. If a platform reprocesses the image (resize/recompress), decoding without access to the original bytes should still recover an approximation of the original region — confirmed worth the extra implementation complexity to achieve this.
• Non-lossy is deferred to a later phase, not an early gate. Each phase should be the simplest implementation that validates the next increment of the core scheme, provided it's a genuine subset of the full design rather than a dead end to be discarded — "easy" must be on the path to "full", not a detour from it.
• Revised 2026-08-30 (reinstated v1): a fast, genuinely secure (requirement-4-satisfying) milestone is worth having even if it costs a small amount of later-discarded diffusion code. v0.5 alone leaves a gap — fast but knowingly breakable — between it and the ~2.5-day-in point (v1.5) where security first lands in the coefficient domain. Reinstating v1 (pixel domain, permutation + diffusion) closes that gap for ~0.15–0.25 day of code that gets rewritten (not reused) at v1.5, in exchange for a usable-and-secure milestone at roughly half a day.
• Robustness to platform reprocessing is worth extra implementation complexity, not just a nice-to-have.
• Region location does not need to be hidden — simplifies design, metadata-stored bounding box is fine.
• JPEG bytes cannot be reordered directly — the stream is Huffman-coded (variable-length, not byte-aligned per block) and DC coefficients are stored as a running difference from the previous block, so blocks are chained, not independent.
• A full decode → pixel-edit → re-encode cycle requantizes the whole image — accepted for v0.5/v1 (pixel domain), avoided from v1.5 onward by working in the coefficient domain.
• Pure position-shuffling (block permutation) alone is vulnerable to jigsaw-reconstruction attacks — edge-continuity solvers can often reassemble shuffled tiles without the key. This is v0.5's known, accepted limitation; value diffusion (added at v1) is what closes it.
Shared across all phases: KDF-derived keys (one secret → permutation seed + offset keystream), CSPRNG-keyed block permutation (Fisher-Yates, not the modulo/affine map — rejected for keyspace reasons once "hard to decode" was confirmed as a real requirement), region-boundary snapping to a fixed grid. The permutation algorithm and KDF carry forward unchanged in concept across all phases; only what a "block" addresses (pixel-block vs coefficient-block) changes.
Decode the whole image to pixels. Snap the selected region outward to a fixed pixel-block grid (e.g. 8×8 or 16×16). Apply the keyed permutation only — no value diffusion — to blocks within the region. Re-encode as JPEG. Standard libraries suffice (e.g. Pillow) — no coefficient-level tooling needed.
Purpose: validate region-snapping, key derivation, permutation mechanics, and round-tripping cheaply, before committing to coefficient-domain tooling. Explicitly does not satisfy requirement 4 on its own — known and accepted, not a design flaw.
Detailed implementation design: JPEG Region Scrambling v0.5 — Detailed Design.
Same as v0.5, plus a keyed value-offset diffusion step applied to pixel bytes within the region blocks, derived from the KDF's offset keystream. First phase that meaningfully satisfies requirement 4 — fast, usable, and reasonably hard to decode without the key, at the cost of still being lossy everywhere on re-encode.
Known throwaway: this pixel-byte diffusion implementation gets rewritten, not ported, at v1.5 — diffusion logic differs materially between pixel-byte and DCT-coefficient domains. Accepted deliberately in exchange for having a genuinely secure milestone early.
Move to the DCT-coefficient domain: operate on coefficient blocks via lossless JPEG transform tooling (jpegtran/mozjpeg-style coefficient access, or jpegio — see Open Questions) instead of decoded pixels. Diffusion is rewritten here as a keyed in-category DC/AC coefficient offset per block — conceptually the same idea as v1's, implemented against a different value type.
Relaxes exactness at a thin ring of blocks around the region edge rather than solving it precisely: lets category overflows fall through to a plain re-encode of that symbol (affects only a few boundary blocks) and snaps to a rough grid for chroma-subsampling/MCU alignment rather than solving it exactly. Everything outside that boundary ring stays genuinely byte-exact.
Same coefficient-domain permutation and diffusion as v1.5, with the two deliberately-skipped precision steps added back: rigorous in-category offset handling (no overflow fallback) and precise chroma-subsampling/MCU alignment, paired with restart-marker (RSTn) segment boundaries for exact region relocation. Nothing built for v1.5 is discarded reaching v2 — this transition is pure addition.
• : snap to restart-interval boundaries — sloppy selection is fine, no pixel-accurate masking needed.
• : each restart interval is self-contained and byte-aligned (DC predictor resets to zero at each marker), so intervals are independently relocatable without touching the Huffman stream. Marker cycle (0–7) needs renumbering after reordering.
✅ v0.5 implementation complete (2026-08-31). See ideas/jpeg-obscura/project-index for implementation status, test results, and v1+ roadmap.
• — much harder: Canvas only ever exposes decoded pixels, never raw DCT coefficients, so exact unscrambling needs a WASM build of a coefficient-exposing JPEG library (e.g. libjpeg-turbo via Emscripten) or a from-scratch JS decoder. Meaningfully bigger and more open-ended than anything else in this plan — treat as a separate, later project rather than folding into the core estimate.
• : passphrase prompt with local encrypted storage, or — neater — the key in a URL fragment (#key=...), which browsers never send to the server, giving shareable “click to reveal” links with no key touching any server.
• Rough added cost: extension shell + UI + key handling + pixel-domain (v0.5/v1-compatible) unscrambling ≈ 2.5–3 days. Coefficient-domain (v1.5/v2) support is a separate, larger, higher-uncertainty chunk — realistically a week-plus.
• Monetization direction (server owns the key, gates access, charges for unlock via subscription or credit balance) noted separately at ideas/jpeg-region-scrambling-monetization — “interesting if it took off”, not committed to.
Because permutation operates on block position rather than raw file bytes, it degrades gracefully under same-resolution recompression (positions still align, values get noisier) but breaks under resizing (block grid shifts). To survive resizing meaningfully likely needs percentage-based/relative block coordinates rather than absolute — this edges toward robust-watermarking territory and is the least-resolved part of the design. Applies across all phases. Needs further thought before implementation.
• Build v0.5 (pixel-domain, permutation-only, Pillow) first — cheapest validation of the mechanics before any coefficient-level tooling commitment.
• Define the KDF and keystream construction (e.g. HKDF + ChaCha20 block generator) — permutation seed needed from v0.5; offset keystream needed by v1.
• Resize-robust coordinate scheme (see Reprocessing robustness) — biggest unresolved piece, deferred until v0.5/v1 are working.
• Tooling choice for v1.5/v2: check jpegio first (Python library from steganalysis research exposing DCT coefficient arrays directly via libjpeg — read/write coefficients losslessly without restart-marker byte manipulation, could simplify both phases considerably) before falling back to jpegtran/mozjpeg C library or hand-rolled ctypes bindings. Pillow only exposes pixel-level ops, not coefficient-level.
Design-stage only — not yet prototyped. Discussed and refined in chat 2026-08-30, from initial "reorder the numbers" idea through restart-marker/DC-cascade mechanics to the current phased design. Numbering history: v1 (pixel, with diffusion) briefly cut in favour of a permutation-only v0.5 to avoid throwaway diffusion code; reinstated the same day once the resulting security gap between v0.5 and v1.5 was weighed against the small amount of wasted work.
• Time estimate (2026-08-30, revised): v0.5 ≈ 0.25–0.3 day. v1 adds ≈ 0.15–0.25 day (pixel-byte diffusion). v1.5 adds ≈ 2 days (tooling setup ~0.5–1 day — biggest swing factor, depends on jpegio viability; rewriting diffusion at coefficient level ~1 day; boundary/chroma handling near-zero since deliberately not solved precisely). v2 adds ≈ 1–1.5 days (rigorous in-category handling, precise chroma/MCU alignment, byte-exact boundary testing).
• Wasted-work note: the v1 pixel-byte diffusion implementation (~0.15–0.25 day) is discarded at v1.5 — roughly 5–7% of total effort. Accepted deliberately: it buys a genuinely secure, usable milestone at ~0.5 day instead of waiting until ~2.5 days for security to first land in the coefficient domain.