Media Pipeline

Media Processing & Storage

Deep metadata stripping, perceptual hashing, encryption, and storage.

After ZK proofs are generated and identity is established, the image must be prepared for storage. This section covers the media processing pipeline: metadata stripping, perceptual hashing, client-side encryption, upload, and the encrypted gallery that lets users manage their verified media.

Metadata stripping

Metadata stripping happens in two stages. The first stage runs during proof generation in the split worker. The second stage runs before upload, removing everything the first stage left behind.

Stage 1 — split worker strip

The split worker removes the C2PA/JUMBF manifest (JPEG APP11 marker) from the image after extracting the data needed for proof generation. The rest of the metadata (EXIF, ICC, IPTC) is left intact at this stage because the worker only needs to remove the cryptographic proof data.

Stage 2 — deep strip

Before upload, the deep stripper removes all remaining metadata at the binary level. It parses the file format and strips every non-essential segment:

FormatRemovedPreserved
JPEGAPP0-APP15 (EXIF, XMP, ICC, C2PA, IPTC), COM (comments)SOI, EOI, DQT, DHT, SOF, SOS → compressed pixel data
PNGtEXt, iTXt, zTXt, eXIf, iCCP, tIME, caBX, caLiIHDR, PLTE, IDAT, IEND, tRNS, gAMA, sRGB, pHYs

The stripper produces a report listing every removed segment with its type, description, and byte size. After stripping, the file contains only the data needed to render the image — pixel data and structural markers. No GPS coordinates, device serial numbers, camera model, timestamps, author names, or edit history survive.

Note
PNG stripping preserves gAMA, sRGB, and pHYs chunks because these affect how the image renders (gamma correction, color space, and DPI). Removing them would subtly alter the image appearance. JPEG ICC profiles (APP2) are stripped because JPEG rendering is less sensitive to color profile loss.

Content hashes

The system maintains two distinct hashes for every image, serving different purposes:

HashInputFormatUsed for
contentHashOriginal file (with all metadata)SHA-256 (64 hex chars)Verification page lookup, user reference
circuitContentHashStripped file (metadata removed)SHA-256 reduced to BN254 fieldZK circuit public input, selective disclosure proofs

The distinction matters because the C2PA manifest's content hash covers the original file, while the ZK circuit operates on the stripped file. These produce different SHA-256 values. The circuit-compatible hash is computed by the split worker: SHA-256 of the stripped bytes, reduced modulo the BN254 field order. Both hashes are stored in the encrypted media index.

Two-hash pipeline
Original file (with C2PA + EXIF)
  → SHA-256 → contentHash (for verification page)

Original file
  → Strip C2PA (split worker)
  → SHA-256
  → mod BN254_FIELD_ORDER
  → circuitContentHash (for ZK proofs)

Perceptual hashing

After stripping, the image is perceptually hashed using pdq-wasm, a WebAssembly binding for Meta's PDQ (Perceptual Distance Quality) algorithm. PDQ produces a 256-bit hash that is resilient to minor image modifications — crops, resizes, compression changes, and color adjustments produce similar hashes.

  • Computation. The PDQ WASM module processes the image Blob directly, producing a 64-character hex string. For RAW camera files, the embedded JPEG preview is extracted first.
  • Purpose. PDQ hashes enable near-duplicate detection. The backend stores hashes and can match against known exploitative content databases. PDQ is non-reversible — the image cannot be reconstructed from its hash.
  • Non-blocking. PDQ computation failures do not block the upload. The hash is optional metadata that enhances content moderation capabilities.

For video, two additional fingerprinting methods are used: vPDQ (per-keyframe PDQ hashes, up to 32 keyframes) and TMK (Temporal Media Fingerprint, ~263 KB), which captures temporal patterns for cross-codec video matching. These are covered in the Video Verification section.

Client-side encryption

Every file is encrypted with AES-256-GCM before leaving the browser. The Apertrue backend stores only ciphertext.

Key lifecycle

  1. Generation. A fresh AES-256-GCM key is generated per upload session via crypto.subtle.generateKey. The session ID is a UUID prefixed with "upload-".
  2. Per-file IV. Each file gets a fresh random 96-bit IV. IV reuse with the same AES-GCM key would break confidentiality, so a new IV is generated for every file in the batch.
  3. Encryption. The plaintext bytes are encrypted with a 128-bit authentication tag. The output is the 12-byte IV prepended to the ciphertext — a self-contained encrypted blob.
  4. Key storage. The session key is exported as a JWK and stored in IndexedDB. If a wrapping key is available (from passkey PRF), the JWK is wrapped with AES-GCM before storage — encryption at rest for the photo keys themselves.
  5. GDPR erasure. Deleting the encryption key from IndexedDB makes the ciphertext permanently unrecoverable. This is cryptographic erasure — no need to locate and delete every copy of the ciphertext.

Upload flow

End-to-end upload pipeline: Select files → C2PA extraction → Privacy config → ZK proving → Deep strip → Resize → PDQ hash → AES-256-GCM encrypt → Upload to backend → Cache locally → Update gallery index

The upload pipeline processes each file through these stages:

  1. File selection and C2PA extraction. The user selects files. Each file is checked for a C2PA manifest. If present, the manifest is parsed and the trust tier is determined. A temporary entry is created in the media index.
  2. Privacy configuration. The user selects disclosure levels for each metadata category (location, time, device, etc.). These choices become circuit inputs for range proofs.
  3. ZK proof generation. C2PA-verified files enter the split worker pool. Workers generate ProofA and ProofB, then aggregation produces the root commitment. The worker also strips C2PA data and computes the circuit content hash.
  4. Deep metadata strip. The stripped image from the worker undergoes a second pass removing all remaining metadata (EXIF, ICC, IPTC, comments).
  5. Resize. The image is resized to a maximum dimension of 1920 pixels and encoded as JPEG at 85% quality. This produces the display version that will be stored.
  6. Perceptual hash. PDQ hash is computed from the image for content moderation matching.
  7. Encryption. The resized, stripped image is encrypted with AES-256-GCM using a per-session key. The output is a self-contained encrypted blob (IV + ciphertext).
  8. Upload. The encrypted blob is sent to the backend as a multipart form upload, along with verification status, disclosure preferences, proof identifiers, and the PDQ hash. The backend stores the blob in R2 and returns a CDN URL.
  9. Local caching. The encrypted blob is cached in IndexedDB by CDN URL for offline gallery playback — avoiding re-fetching from CDN on the next view.
  10. Gallery index update. The media index entry is updated with the backend media ID, CDN URL, encryption session ID, content hashes, ZK commitments, disclosure preferences, and all provenance metadata.

The encrypted media index

The media index is the user's private gallery — an encrypted JSON document stored in IndexedDB that tracks every verified upload. It contains everything the user needs to view, manage, and prove their media.

Gallery key derivation

The gallery encryption key is derived deterministically from the Aztec secret:

Gallery key derivation
gallery_field = Poseidon2([aztec_secret, GALLERY_DOMAIN_TAG])
gallery_key = SHA-256(hex(gallery_field))
// Imported as AES-256-GCM CryptoKey

Because the derivation is deterministic, the same Aztec secret always produces the same gallery key — enabling persistent access across browser sessions and synced devices.

Index contents

Each entry in the media index stores:

  • Backend references. Media ID, CDN URL, encryption session ID (for key lookup), thumbnail URL.
  • Content hashes. Both contentHash (original) and circuitContentHash (stripped, BN254 field) for verification and selective disclosure.
  • ZK commitments. Root commitment, image commitment, Merkle path, link commit, location commitment, time commitment, actions hash — everything needed for future selective disclosure proofs.
  • Original metadata. GPS coordinates, capture date, device model, lens, exposure settings — stored locally so the user can optionally disclose them later via commitment opening.
  • Disclosure preferences. The user's privacy choices for each metadata category, including any identity disclosures (organisation domain, nationality, verified human status).
  • Blockchain state. Aztec transaction hash, root commitment, contract address, aggregation ID — linking the local entry to the on-chain record.

The entire index is encrypted at rest with AES-256-GCM. Every read decrypts the full index; every write re-encrypts and stores the updated JSON. Stale entries (stuck in "proving" or "uploading" for more than 30 minutes) are automatically cleaned up.

Storage architecture

LayerWhatWhereEncryption
Encrypted blobImage/video ciphertextCloudflare R2 (CDN-delivered)AES-256-GCM (client key)
Media metadataVerification status, PDQ hash, provenancePostgreSQL (backend)None (public metadata)
Encryption keysPer-session AES-256 JWKIndexedDB (browser)Optional PRF wrapping
Media indexFull gallery with commitmentsIndexedDB (browser)AES-256-GCM (gallery key)
Blob cacheEncrypted blob by CDN URLIndexedDB (browser)Same as R2 (still encrypted)

The backend stores encrypted blobs and public metadata (verification status, PDQ hash, disclosure preferences). The browser stores decryption keys and the full gallery index. Compromising the backend yields only ciphertext and perceptual hashes — no plaintext images, no private metadata, no GPS coordinates.

Viewing encrypted media

When a user views a photo from their gallery:

  1. The browser reads the media index entry to find the CDN URL and encryption session ID.
  2. If the encrypted blob is in the local cache (IndexedDB), it is loaded directly. Otherwise, the blob is fetched from the CDN.
  3. The encryption key is retrieved from IndexedDB using the session ID.
  4. The first 12 bytes of the blob are extracted as the IV. The remainder is decrypted with AES-256-GCM.
  5. The plaintext image bytes are displayed in the browser.

Thumbnails may be stored as data URLs in the media index for instant gallery rendering without decrypting the full image. Video thumbnails are generated client-side by seeking to the 1-second mark and capturing a canvas frame.

Offline playback
The local blob cache stores encrypted copies of recently viewed media. Combined with the encrypted media index (which contains thumbnails and metadata), users can browse their gallery offline. Decryption happens locally using keys from IndexedDB — no network access needed after initial caching.

The next section covers video verification — how C2PA manifests are extracted from video containers, how keyframes are fingerprinted, and how the verification pipeline adapts for moving images.