What's actually inside the QR codes.
Last updated: 2026-05-01
transfqr is a one-way protocol: the sender's screen flickers QR frames; the receiver's camera grabs whichever ones it can. There's no back-channel — no Bluetooth handshake, no WebRTC negotiation, no acknowledgements going the other way. The trick that makes this work is fountain coding. Any K + ε received frames reconstruct the file, no matter which ones you missed.
Goals
- One-way. Sender screen → receiver camera. No back-channel.
- Loss-tolerant. Any K + ε received frames decode the file. No ACKs.
- Self-describing. A receiver joining mid-stream can sync without restart.
- Integrity-checked. SHA-256 hash, baked into the meta frame, must match the decoded payload before the receiver hands it off.
Frame envelope (18-byte header)
Every QR code carries one frame. The frame is a binary blob, base45-encoded (RFC 9285) so the QR encoder picks the dense alphanumeric mode — ~60% more bytes per QR than the byte-mode base64 alternative. All integers little-endian.
| Bytes | Field | Type | Notes |
|---|---|---|---|
| 0..1 | magic | u16 | 0x5451 ('TQ') |
| 2 | version | u8 | currently 1 |
| 3 | packetType | u8 | 0x01 = meta, 0x02 = data |
| 4..7 | fileId | u32 | random per session; receiver discards on mismatch |
| 8..11 | totalChunks | u32 | K = number of source blocks |
| 12..13 | blockSize | u16 | bytes per source block |
| 14..17 | seed | u32 | LT PRNG seed (data only; meta sets to 0) |
| 18.. | payload | — | type-specific |
Packet types
Meta (0x01)
Sent periodically by the sender (every 10th frame) so a receiver joining mid-stream can bootstrap. The meta packet carries:
fileNameLen u8
fileName utf-8 bytes
mimeLen u8
mimeType utf-8 bytes
originalSize u64 (8 bytes LE)
sha256 32 bytes (post-decompression checksum)
flags u8 (bit 0 = compressed) Data (0x02)
LT-encoded data symbol. Payload is exactly blockSize bytes. The seed in the header tells the receiver which subset of source blocks were XOR'd to produce this symbol. Same seed → same combination on both sides; the LT degree distribution makes belief propagation feasible.
Sender loop
- Compress source bytes (DEFLATE via pako) if size > threshold.
- Compute SHA-256 of the (zero-padded) compressed payload.
- Split into K blocks of blockSize.
- Build an LT (Luby Transform) encoder over those K blocks.
- Loop indefinitely: every 10th tick render a meta packet; otherwise pick a fresh random seed, encode an LT symbol, render the resulting bytes as a QR code.
- Stop on user cancel.
Receiver loop
- Open the camera, scan QR codes continuously.
- For each decoded frame: validate magic + version, lock onto the first fileId seen, discard frames with a different one.
- If meta and we haven't seen meta yet: store fileName, mime, size, sha256, flags. If data: feed (seed, payload) into the LT decoder.
- Track progress as recovered / K.
- When the decoder completes: reconstruct, verify SHA-256, decompress if the compressed flag is set, hand the file off to the OS (save / share intent).
QR rendering
Frames are rendered with error correction level L (~7% redundancy) — the fountain code already supplies massive end-to-end redundancy, so spending more QR bits on ECC just shrinks the per-frame payload for no gain. Quiet zone: 1 module. Sender pacing: ~12-13 frames per second (80ms tick); the receiver camera drops some, the fountain code recovers them.
What this protocol is not
- Not encrypted. v1 frames are visible to anyone with a camera pointed at your screen. If you need confidentiality, encrypt the file before sending. v3 of the protocol will add an optional passphrase-derived session key.
- Not authenticated. Anyone can craft a frame that claims to be from you. Practically: who is going to spoof QR codes onto your screen during a transfer? Future versions can add signatures if a real threat model emerges.
- Not high-bandwidth. 768 bytes per frame × 12.5 fps ≈ 9 KB/s effective throughput. A 1 MB file takes ~110 seconds; 5 MB about 9 minutes. This is fine for documents, photos, voice memos. It's not for movies. There's a 50 MB hard cap.
Roadmap
- v2: RaptorQ erasure coding (RFC 6330) replaces LT — lower decode failure rate at marginal capacity, especially for small K.
- v3: Optional end-to-end encryption — a passphrase derives a session key; meta and payload encrypted before LT-encoding.
Have an idea or spotted a flaw? protocol@transfqr.com.