10.2 QSFS
File storage that has only ever seen ciphertext.
Sharded, encrypted file storage: every blob arrives sealed, splits across shards, and is erased on schedule.
- Blob AEAD ChaCha20-Poly1305
- Blob keys HKDF-SHA512
- Session floor ML-KEM-1024 + X25519
01 / THE CARRIER — LETTERED
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.
Erased with its message
When a conversation is destroyed, the destruction signal carries the file paths of every blob it referenced, and the purge listener issues a DELETE per blob in the same window. Media does not outlive its conversation, and retired FSOR epochs lock any residue.
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.
02 / THE WRITE PATH — TRACED
Trace one file from your device to the shard
03 / THE BINDING — IN SOURCE
Read the binding in the source that ships
/// 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
}
04 / THE PARAMETERS — READ FROM SOURCE
Check every parameter against the file it came from
| Parameter | Value | Source |
|---|---|---|
| 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 | purge listener issues DELETE per blob path | backend/src/services/lifecycle.rs |
05 / THE YIELD — BY STORAGE CLASS
Measure where your files actually stand.
| 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 | Erased with the message + epoch lock | 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 |
06 / THE HARD QUESTIONS
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.
What does a storage seizure yield?
Sealed blobs and their identifiers. Assembling plaintext requires the conversation keys — breaking both X25519 and ML-KEM-1024.
What happens to files when I delete a conversation?
The destruction signal lists every blob path and deletes them in the same window; retired FSOR epochs lock any residue.
Can a corrupted or swapped blob go unnoticed?
No — each blob's AAD binds its conversation, epoch, chain position, sender, file identifier, and declared media type. Substitution fails authentication.
Does file size leak what I sent?
Sizes land on the shared pad-ladder — length classes, not exact lengths.