RJPEG: Making Photos Carry Their Own Proof
There are two problems with photos right now.
The first: anyone with a laptop can generate a photorealistic image of anything in seconds. Presidents shaking hands with aliens. Soldiers who never existed. Evidence of events that never happened. The average person has no way to tell the difference.
The second: even when a photo is real, there's no standard way for that proof to travel with it. Cameras capture images. Some cameras can even generate cryptographic proofs of authenticity. But the moment you save that photo as a JPEG — the moment it enters the world — the proof gets left behind. It's a separate file, a separate API call, a separate database entry. It's not part of the photo.
We think the proof should live inside the image itself. One file. Self-contained. Verifiable.
So we built RJPEG.
The insight
Here's something most people don't know about JPEG: every JPEG file ends with a two-byte marker called EOI — End of Image (0xFF 0xD9). When a viewer opens a JPEG, it reads from the beginning, decodes the image data, hits EOI, and stops. Everything after EOI is invisible.
This means you can append arbitrary structured data after EOI and the file is still a perfectly valid JPEG. Your phone opens it. Your browser renders it. iMessage previews it. Photoshop loads it. Nothing breaks.
RJPEG exploits this property. It appends a small, structured envelope after the JPEG's EOI marker — a place to put cryptographic proofs, thumbnails, metadata, or anything else you want to travel with the image.
No new file format to evangelize. No viewer updates to wait for. No ecosystem to bootstrap. It's just a JPEG with extra data that already gets ignored.
The format
The RJPEG binary format is intentionally simple:
[standard JPEG data ... 0xFF 0xD9] ← normal JPEG, untouched
[magic: "RJPEG\0"] ← 6 bytes
[version: u8] ← 1 byte
[section_count: u16 BE] ← 2 bytes
For each section:
[tag: 4 ASCII bytes] ← e.g. "PRUF", "THMB", "META"
[flags: u8] ← bit 0 = gzip compressed
[length: u32 BE] ← byte length of payload
[data: N bytes] ← the payload
[offset_to_magic: u64 BE] ← pointer back to start of envelope
[end_marker: "RJPG"] ← 4 bytes sentinel
That's the entire spec. A few dozen bytes of overhead for the envelope, plus whatever you put in the sections.
Sections are identified by 4-byte ASCII tags. We define four well-known ones:
| Tag | Name | Purpose |
|-----|------|---------|
| THMB | Thumbnail | A smaller version of the image (typically JPEG) |
| PRUF | Proof | Cryptographic proof bytes (ZK proofs, signatures, etc.) |
| PRFI | Proof Input | Witness data for proof verification |
| META | Metadata | Arbitrary structured data (JSON, CBOR, etc.) |
Custom 4-byte ASCII tags are welcome. The format doesn't care what you put in a section — it stores raw bytes. Consumers define the semantics.
Sections are gzip-compressed by default. For data that's already compressed (like a JPEG thumbnail), you can store it raw to avoid double-compression overhead.
Why not EXIF? Why not a new format?
EXIF and APP markers are baked into the JPEG spec, which means they have a hard 65,535-byte size limit per segment. A zero-knowledge proof can easily exceed that. A thumbnail certainly does. And APP markers have to appear at the beginning of the file, which means you'd need to rewrite the entire JPEG to add data after the fact.
RJPEG's post-EOI approach has no size limit and requires no rewriting of the original image bytes. You just append.
As for creating an entirely new file format — that's a non-starter. The JPEG ecosystem is one of the most entrenched in computing. Every device, every browser, every app, every OS on Earth understands JPEG. A new format needs years of adoption to reach even basic compatibility. RJPEG inherits JPEG's universality for free.
Using it
The reference implementation is a Rust crate:
use rjpeg::{pack, unpack, is_rjpeg, Section, METADATA, PROOF, THUMBNAIL};
// Embed sections into a JPEG
let jpeg = std::fs::read("photo.jpg")?;
let sections = vec![
Section::new(METADATA, serde_json::to_vec(&my_metadata)?),
Section::new(PROOF, proof_bytes),
Section::raw(THUMBNAIL, thumbnail_jpeg),
];
let rjpeg = pack(&jpeg, §ions)?;
std::fs::write("photo.rjpeg", &rjpeg)?;
// Read sections back
let data = std::fs::read("photo.rjpeg")?;
let rjpeg = unpack(&data)?;
let proof = rjpeg.section(PROOF).unwrap();
let original = rjpeg.jpeg(); // the untouched JPEG bytes
There's also a TypeScript implementation for browsers that uses the native DecompressionStream API — no dependencies, no WASM, just read the bytes from the end of the file:
import { unpack, isRjpeg, getThumbnail, getProofJson } from 'rjpeg';
const response = await fetch('https://example.com/photo.rjpeg');
const buffer = await response.arrayBuffer();
if (isRjpeg(buffer)) {
const rjpeg = await unpack(buffer);
const thumbnail = getThumbnail(rjpeg);
const proof = await getProofJson(rjpeg);
}
What you can build with it
RJPEG is deliberately format-agnostic. The library stores bytes; you decide what those bytes mean. Some things people could build:
- Camera attestation. A secure camera embeds a proof that the image was captured by real hardware at a real time. Anyone can verify it. This is what we're doing with Roc Camera.
- AI-detection scores. A classifier embeds its confidence score directly into the image. The score travels with the photo, not in a separate database.
- Provenance chains. Each editor in a workflow appends a signed section. The image accumulates a verifiable history of every hand it passed through.
- Rich thumbnails. Embed a pre-rendered thumbnail so gallery views can show previews without decoding the full image. No 65KB EXIF limit.
- Sensor data. Embed IMU readings, GPS traces, light spectral data — anything a sensor captured at the moment the shutter fired.
How we use it
We built RJPEG for Roc Camera, a camera that captures verifiably real photos. Every photo Roc takes is an RJPEG: the image, a cryptographic proof of authenticity, a thumbnail, and metadata — all in one file.
When you share a Roc photo, the proof comes with it. When someone wants to verify it, they don't need to call an API or check a database. The proof is right there, in the file, verifiable offline.
But RJPEG is bigger than one camera. We think every camera — phone cameras, DSLRs, security cameras, dashcams — should be able to embed provenance into the photos they capture. And any viewer, any platform, any verification tool should be able to extract and check it.
That's why we're open-sourcing both the format spec and the reference implementations.
Open source
Everything is MIT + Apache 2.0 dual-licensed:
- Rust crate: github.com/faust-machines/rjpeg —
cargo add rjpeg - TypeScript library: github.com/faust-machines/rjpeg-js —
npm install rjpeg - Format spec: included in the repository, versioned alongside the code
The format is simple enough that you could implement it in any language in an afternoon. We'd love to see implementations in Python, Go, Swift, C — wherever photos are processed.
If you're building something that touches photo authenticity, provenance, or verification — we want to hear from you. Open an issue, submit a PR, or just say hello.
This is the first post on Faust News. We're building machines that make us more human — starting with a camera that proves what's real. More soon.