JPEG Region Scrambling v0.5 — Detailed Design

Child design of Reversible Region Scrambling in JPEGs. This specifies the smallest implementation that validates keyed, region-local block permutation in the pixel domain using Pillow. It is implementation-ready, but deliberately not secure against jigsaw reconstruction and not pixel-lossless because JPEG is decoded and re-encoded.

1. Goal and success definition

Given a JPEG, a rectangular selection, and a passphrase, produce an ordinary JPEG whose selected region is visually obscured by a deterministic keyed permutation of fixed-size pixel blocks. Given that output JPEG and the same passphrase, reconstruct a recognisable approximation of the selected region without needing the original file.

v0.5 succeeds when it proves the end-to-end mechanics: region normalisation, key derivation, deterministic Fisher–Yates permutation, metadata carriage, inverse permutation, CLI ergonomics, and measurable localisation of changes. It does not claim confidentiality: permutation alone leaks local content and may be attacked by edge-continuity/jigsaw solvers.

2. Reversibility contract

the permutation mapping and its inverse, when applied to the same decoded pixel array before any lossy encoding.

encoding the scrambled pixels changes values through colour conversion, chroma subsampling, DCT quantisation, and ringing. Decoding and unpermuting therefore restores an approximation of the pixels that entered the first encode, not the original JPEG bytes or exact original pixels.

survival of third-party resizing, cropping, metadata stripping, rotation, or recompression. Same-size recompression may remain partly recoverable if metadata and the block grid survive, but this is observational rather than an acceptance guarantee.

3. Scope

In scopeOut of scope
One rectangular region; RGB or grayscale input; 8×8 or configurable square blocks; passphrase-based scrambling and restoration; embedded metadata plus explicit CLI overrides; deterministic cross-run behaviour; ordinary JPEG output.Diffusion/value masking; multiple or non-rectangular regions; coefficient-domain editing; byte- or pixel-exact preservation; resizing/cropping recovery; browser extension; payments; adversarial confidentiality; concealment of the region location.

4. User interface

jpeg-obscura scramble INPUT.jpg OUTPUT.jpg \
  --region X,Y,W,H --passphrase-file PATH [--block-size 8] \
  [--quality keep|1..100] [--subsampling keep|0|1|2] [--metadata-out FILE]

jpeg-obscura restore INPUT.jpg OUTPUT.jpg \
  --passphrase-file PATH [--metadata FILE] \
  [--region X,Y,W,H --block-size 8 --salt HEX] \
  [--quality keep|1..100] [--subsampling keep|0|1|2]

jpeg-obscura inspect INPUT.jpg [--json]

A direct --passphrase option may exist for interactive use but must warn that command-line arguments can be exposed through shell history and process listings. Default input is a hidden terminal prompt; automation uses --passphrase-file or standard input. Refuse to overwrite an existing output unless --force is supplied.

5. Image normalisation

Open with Pillow, apply EXIF orientation once using ImageOps.exif_transpose so coordinates refer to the displayed orientation, then convert to RGB. Grayscale may be retained only if the implementation keeps a single-channel path equally well tested; CMYK, palette, and unusual modes are normalised to RGB. The output records orientation as normal and does not preserve an orientation transform that would move the region after scrambling.

Preserve safe descriptive EXIF/ICC data where Pillow supports it, but remove or normalise orientation. Metadata preservation is best-effort in v0.5 and must not be confused with pixel preservation.

6. Region and block geometry

Coordinates use the oriented image's pixel space: half-open rectangle [x, x+w) × [y, y+h). Validate positive width and height and a non-empty intersection with the image. The block grid is anchored at image origin (0,0), making geometry independent of the selected rectangle.

Let B be block size. The effective region contains only complete B×B grid cells: left/top round down to multiples of B; right/bottom round up, then clamp to floor(image_dimension/B) * B. A trailing image strip smaller than B is never moved. If clamping leaves fewer than two complete blocks, fail with a clear error. Return and embed the effective rectangle, and print it when it differs from the requested rectangle.

Permute all full B×B blocks in row-major order inside the effective rectangle. This rule avoids partial edge blocks of unequal shape and makes the inverse unambiguous. Default B=8 aligns with JPEG's common DCT unit while still operating purely on decoded pixels.

7. Key derivation and deterministic random stream

Generate a fresh 16-byte random salt for each scramble. Encode the passphrase as UTF-8 and derive 64 bytes with scrypt using N=32768, r=8, p=1. First 32 bytes are the permutation key; reserve the remaining 32 bytes for metadata key-check/domain separation and later experiments. Store salt and KDF parameters, never the passphrase or derived key.

Define a cross-language deterministic random stream as HMAC-SHA256(permutation_key, "jpeg-obscura/v0.5/permutation" || uint64_be(counter)) for counters 0,1,…, concatenating digests. Implement unbiased randbelow(n) with rejection sampling; do not use modulo reduction or Python's version-dependent random.shuffle.

Build permutation p with Fisher–Yates over block indices 0…n−1, iterating i from n−1 down to 1 and swapping i with randbelow(i+1). Specify the move convention once: output block slot p[i] receives original block i. Restoration uses the exact inverse mapping. Unit tests must lock this convention with published small vectors.

8. Scramble algorithm

  1. Read and validate the JPEG; transpose orientation; normalise the pixel mode.
  2. Normalise the requested rectangle to the effective full-block region.
  3. Generate salt, derive keys, and generate the Fisher–Yates permutation.
  4. Copy the source pixel array. Extract each region block from the unchanged source array, then write it to its destination slot in a separate destination array. Never permute in place.
  5. Encode the destination as JPEG using the chosen quality/subsampling policy.
  6. Insert the v0.5 metadata segment and atomically publish the completed output.

Atomic publish means write a complete temporary file in the output directory, flush/close it, then replace the target only after all processing succeeds. On failure, remove only that known temporary file and leave any pre-existing target untouched.

9. Restore algorithm

  1. Read embedded metadata, or a supplied sidecar/explicit overrides if metadata is unavailable; validate schema and image dimensions.
  2. Derive keys from the supplied passphrase and stored salt/KDF parameters. Verify the key-check before changing pixels; report “wrong passphrase or damaged metadata” without writing output.
  3. Decode and orient exactly as recorded. Reject dimension or orientation mismatches in v0.5.
  4. Regenerate the same permutation and apply its inverse using separate source and destination arrays.
  5. Encode the restored pixel array as JPEG and atomically publish it. Copy/update metadata to record that this is a restored derivative, or omit scrambling metadata by default so a second accidental restore is rejected.

10. Metadata format

Embed a custom APP15 JPEG segment immediately after SOI. Payload: ASCII magic JOSC; one-byte envelope version 0x01; four-byte big-endian JSON length; UTF-8 canonical JSON. Standard decoders ignore unknown APP segments. Also support writing the same JSON as a sidecar for debugging and recovery from tools that strip APP metadata.

{
  "schema": "jpeg-obscura/v0.5",
  "operation": "scrambled",
  "image_width": 1920,
  "image_height": 1080,
  "requested_region": [101, 77, 603, 411],
  "effective_region": [96, 72, 608, 416],
  "block_size": 8,
  "block_count": 3952,
  "kdf": {"name": "scrypt", "n": 32768, "r": 8, "p": 1, "salt_b64u": "..."},
  "permutation": {"name": "fisher-yates-hmac-sha256-v1"},
  "orientation_normalised": true,
  "key_check_b64u": "..."
}

Compute key_check as the first 16 bytes of HMAC-SHA256(check_key, canonical_json_without_key_check). This detects wrong keys and accidental metadata damage; it is not an authenticity guarantee against an attacker and does not make weak passphrases safe. Reject duplicate JOSC segments, unknown required fields, impossible dimensions, excessive KDF parameters, or metadata larger than a conservative limit such as 16 KiB.

11. JPEG encoding policy

For predictable v0.5 behaviour, default to Pillow quality 95 and 4:4:4 subsampling (subsampling=0), with optimisation optional. A keep mode may use Pillow's best-effort quality="keep" and subsampling="keep" for JPEG input, but it is not a lossless copy and must not be the only tested path. Record the actual policy used in metadata for diagnostics.

Do not promise that pixels outside the region remain identical. Measure outside-region error and report it in tests, but exact preservation is deferred to coefficient-domain versions.

12. Package structure

src/jpeg_obscura/
  cli.py          argument parsing, prompts, exit codes
  image.py        orientation, mode conversion, JPEG I/O
  geometry.py     rectangle validation and block enumeration
  kdf.py          scrypt and key separation
  stream.py       HMAC counter stream and unbiased randbelow
  permutation.py  Fisher–Yates and inverse mapping
  transform.py    block extraction/copy for scramble and restore
  metadata.py     APP15 and sidecar encode/decode/validation
  errors.py       typed user-facing failures

tests/
  test_geometry.py
  test_stream_vectors.py
  test_permutation.py
  test_transform.py
  test_metadata.py
  test_roundtrip.py
  test_cli.py

Core functions accept arrays/bytes and explicit parameter objects; they do not print, prompt, or read global state. CLI owns user interaction. Use NumPy for block copying if adopted, but keep the transform semantics simple enough to test against a small pure-Python reference implementation.

13. Errors and exit behaviour

ConditionBehaviour
Invalid/empty region or fewer than two blocksExit 2; explain requested and effective geometry.
No JOSC metadata on restoreExit 3; suggest --metadata or explicit parameters.
Wrong passphrase or damaged metadataExit 4; write no output.
Unsupported schema/version or unsafe KDF valuesExit 5; write no output.
Input decode or output encode failureExit 6; preserve target and report the underlying file error concisely.
Output exists without --forceExit 7; do not modify it.

14. Test plan

15. Acceptance criteria

16. Implementation sequence

  1. Geometry and pure in-memory block transform, with exact property tests.
  2. KDF, deterministic stream, Fisher–Yates vectors, and inverse mapping.
  3. Pillow load/save and end-to-end image fixtures.
  4. APP15 metadata codec, validation, sidecar support, and key-check.
  5. CLI, atomic output behaviour, inspect command, and user-facing errors.
  6. Quality measurements and a short results record deciding whether B=8 remains the default before starting v1 diffusion.

17. Decisions deferred to v1+

v1 adds keyed value diffusion because permutation alone does not meet the hard-to-decode requirement. v1.5 moves to DCT coefficients to localise re-encoding damage. v2 handles rigorous coefficient-category constraints, chroma/MCU alignment, and exact outside-region preservation. Resize-robust coordinates and recovery after metadata-stripping platforms remain separate research questions and must not silently expand v0.5.

Status

Detailed design completed 2026-08-31. No implementation has started. The first coding milestone is the pure geometry/permutation transform with exact in-memory round-trip tests.

version 1  ·  created 2026-08-31  ·  updated 2026-08-31  ·  tags ideas, image-processing, jpeg, crypto, design