QERYX COMING SOON
OBSERVATORY

10.2 QSFS

File storage that has only ever seen ciphertext.

QSFS Coming soon

Sharded, encrypted file storage: every blob arrives sealed, splits across shards, and is erased on schedule.

Send a file through it. The keys never follow.

The encryption boundary

Your file is padded to a size class and sealed on your device — one ChaCha20-Poly1305 box over the padded plaintext, under a Q-Ratchet-derived key — before a single byte leaves it. QSFS stores opaque blobs addressed by identifiers, sharded across storage paths. The keys stay with you and your recipients; the store has only ever seen ciphertext.

Seal point
on-device, before upload — server never holds a key
Blob key
Q-Ratchet-derived per file, labeled HKDF
Traffic shape
pad-ladder size classes — length buckets, not exact lengths

Binding beats swapping

Sealing is not enough; a captured blob must not be re-servable somewhere else. Each media blob's AAD binds its conversation, epoch, chain position, sender, file identifier, and declared media type — substitution, replay onto a sibling group, or re-serving an image as a video manifest fails authentication.

Deleted on its own clock

A file is not deleted with its message. The relay keeps each upload until its own expiry, 45 days after upload by default and published at /api/v1/transparency/retention; then the media reaper deletes the blob and its upload record. The sender’s delete for everyone, or a chat timer’s purge from the sender’s phone, removes it sooner; destroying the conversation does not. FSOR wraps no file, and any server-side encryption a file’s store adds is under a key the operator holds, so against the operator a file rests on its end-to-end seal alone.

Uploads that survive bad networks

The sealed ciphertext uploads in 256 KB chunks, each idempotency-keyed by its digest, so a transient failure on chunk N never wipes the bytes for chunks 0…N−1. Resume is a bookkeeping operation on ciphertext — the crypto never re-runs.

Every blob arrives sealed. QSFS has never held a key.

The carrier model

Trace one file from your device to the shard

FILE padded to class SEALED BLOB AEAD + bound AAD CHUNK 0 · 256 KB CHUNK 1 · 256 KB CHUNK N SHARDED STORE opaque paths, ciphertext only u/<shard>/<file_id>/….enc deleted at its own expiry
Pad → seal → chunk → shard. Keys never leave the clients.

Read the binding in the source

crypto-core/src/group_sender.rs
/// Build the AAD for a media-blob AEAD payload. Wire layout:
/// `GROUP_MEDIA_AAD_LABEL || 0x00 || conversation_id || 0x00 || epoch(u32) ||
///  chain_index(u32) || sender_profile_id || 0x00 || file_id || 0x00 ||
///  mime_type`
///
/// `mime_type` binding stops a blob host from re-serving an image
/// ciphertext as a video manifest (the iOS decoder picks behavior off the
/// declared mime). `file_id` binding stops a captured blob from being
/// served back under a different file_id slot. Conversation/epoch/chain
/// binding stops cross-channel replay onto a sibling group.
pub fn build_media_aad(
    conversation_id: &[u8],
    epoch: u32,
    chain_index: u32,
    sender_profile_id: &[u8],
    file_id: &[u8],
    mime_type: &[u8],
) -> Vec<u8> {
    let mut aad = Vec::with_capacity(/* exact-size preallocation elided */);
    aad.extend_from_slice(GROUP_MEDIA_AAD_LABEL);
    aad.push(0x00);
    aad.extend_from_slice(conversation_id);
    aad.push(0x00);
    aad.extend_from_slice(&epoch.to_be_bytes());
    aad.extend_from_slice(&chain_index.to_be_bytes());
    aad.extend_from_slice(sender_profile_id);
    aad.push(0x00);
    aad.extend_from_slice(file_id);
    aad.push(0x00);
    aad.extend_from_slice(mime_type);
    aad
}

Check every parameter against the file it came from

Storage parameters — values from source
ParameterValueSource
Blob seal ChaCha20-Poly1305, one box over padded plaintext FileUploadService.swift
Blob key Q-Ratchet-derived per file (labeled HKDF) crypto-core/src/group_sender.rs
AAD binding conversation, epoch, chain index, sender, file_id, mime type group_sender.rs::build_media_aad
Traffic shape pad-ladder size classes crypto-core/src/q_pad_ladder.rs
Upload chunk 256 KB, digest-idempotent, resumable FileUploadService.swift
Concurrency 4 chunks in flight FileUploadService.swift
Max file size 2 GB FileUploadService.swift
Deletion media reaper at expiry, 45 days after upload by default; the uploader's DELETE sooner backend/src/main.rs · handlers.rs

61

fields bound into every media blob's AAD

256 KB2

resumable upload chunk — ciphertext only

03

keys ever held by the storage layer

Measure where your files actually stand.

Category classes only — never named products
QERYX QSFS Classical cloud storage Email attachments
Real workloads
Provider read Ciphertext only Provider-readable or provider-keyed Plaintext at provider
Storage seizure Sealed chunks + identifiers Plaintext or provider-decryptable Plaintext
Deletion At its own expiry (45 days by default), or sooner by its sender Soft-delete; versions persist Forever, in mailboxes
Integrity AAD-bound authentication Checksums at best Not included
Quantum recording today Hybrid-derived keys Classical TLS + at-rest Plaintext

Ask what a seizure of the store reaches.

Can QERYX open my files?

No. Files are sealed on your device before upload; QSFS holds ciphertext and identifiers, not keys.

Sealed blobs and their identifiers. Assembling plaintext requires the conversation keys — breaking both X25519 and ML-KEM-1024.

They stay until their own expiry, up to 45 days after upload: destroying a conversation deletes the message rows the relay can tie to it, not the files they carried. Delete a message you sent for everyone, within two hours of sending it, to remove its file sooner.

No — each blob's AAD binds its conversation, epoch, chain position, sender, file identifier, and declared media type. Substitution fails authentication.

Sizes land on the shared pad-ladder — length classes, not exact lengths.

Security overview Read the docs