Blobs
The blob store holds encrypted files a host cannot read, addressed by a handle it cannot guess. Everything a recipient needs to find and decrypt a file travels inside MLS, so holding a blob_handle is itself evidence of group membership.
/// Everything needed to locate, fetch, and decrypt a file. Travels inside MLS,/// so only group members ever learn a blob's location or key.struct FileRef { // File size in bytes size: u64,
// Hint only. Never validated or enforced by a server mime_type: String,
// Blake3 of unencrypted content. Verified after reassembly plaintext_hash: [u8; 32],
// Stable identity, used by FileAction to target this file file_id: FileId,
// Server hosting the ciphertext. Chosen by the uploader, which is why it // is stated rather than derived. Normally the uploader's home server host: String,
// Unguessable storage key. Acts as the fetch capability: it travels only // inside MLS, so holding it evidences group membership blob_handle: [u8; 32],
// How to decrypt the blob key: FileKey,}
/// Type-safe file identifierstruct FileId { // DeviceId of the device that uploaded this file uploader: DeviceId,
// Local counter maintained by uploader id: u64,}
/// Files come from two contexts, and only one of them has a groupenum FileKey { // Group-scoped. Derived from the MLS epoch secret, so the same file sent // to two groups yields different ciphertext and the host cannot correlate // them. Members who joined later cannot derive it, matching MLS forward // secrecy Epoch { epoch: u64 },
// Identity-scoped. A standalone random key carried inline, for media with // no group context, such as a profile picture shared via InfoPackage Direct([u8; 32]),}file_id MUST NOT be used as the storage key. It contains uploader: DeviceId, and a hosting server that indexed by it would learn which device owns every blob. This is the exact leak that blind KeyPackage handles exist to prevent. blob_handle is random and unrelated to identity.
UploadBlob
Section titled “UploadBlob”Stores an encrypted file and returns its handle.
- Input:
BlobUpload { ciphertext, max_uses, ttl_secs } - Output:
BlobReceipt { blob_handle, revocation_token, expires_at } - Auth: PrivacyPass
- Idempotent: No
- Errors:
TOKEN_INVALID,TOKEN_REPLAYED,BUDGET_EXHAUSTED,BLOB_TOO_LARGE,MAX_USES_OUT_OF_RANGE
Requirements
Section titled “Requirements”- Upload MUST spend one token, regardless of size
- Servers MUST reject an upload exceeding their published maximum blob size
- Servers MUST generate
blob_handlewith a CSPRNG - Servers MUST NOT index a blob by anything derived from
file_idordevice_id - Servers MUST treat contents as opaque and MUST NOT inspect or transcode them
- Servers MUST return a
revocation_tokenand MUST NOT accept one supplied by the client - Servers MUST enforce a TTL, defaulting to 30 days
- Servers SHOULD enforce
max_uses, bounded atMAX_BLOB_USES
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/blobsAuthorization: PrivateToken token="<base64url>"Content-Type: application/octet-streamFetchBlob
Section titled “FetchBlob”Retrieves a blob by handle
- Input: None - the handle is the path
- Output: ciphertext
- Auth: Capability handle
- Idempotent: Yes
- Errors:
BLOB_NOT_FOUND,BLOB_EXPIRED,BLOB_USES_EXHAUSTED
Requirements
Section titled “Requirements”- Fetch MUST NOT require a token
- Servers MUST NOT log the fetcher’s identity or source address against a handle
- Servers MUST count a fetch against
max_uses - Recipients MUST verify
plaintext_hashafter decryption and discard on mismatch
Why Fetch is Unmetered
Section titled “Why Fetch is Unmetered”Every other data-carrying operation costs a token. This one cannot, for the same reason tokens never cross the federation boundary: they redeem only at the issuing server, so a recipient fetching a file from a peer’s host holds nothing that host would accept.
Authorization comes instead from possession of the blob_handle, which travels only inside MLS. Hosts bound abuse with a TTL and max_uses rather than with tokens.
HTTP Binding
Section titled “HTTP Binding”GET /api/v1/blobs/{blob_handle}DeleteBlob
Section titled “DeleteBlob”Removes a blob before its TTL expires.
- Input: None - the revocation_token is in a header
- Output: None
- Auth: Revocation token
- Idempotent: Yes
- Errors:
BLOB_NOT_FOUND,REVOCATION_TOKEN_INVALID
Requirements
Section titled “Requirements”- Servers MUST compare revocation tokens in constant time
- Servers MUST return
REVOCATION_TOKEN_INVALIDrather thanBLOB_NOT_FOUNDwhen the blob exists but the token is wrong - The revocation_token MUST NOT appear in a
FileRef - Deletion spends no token - it frees resources
The token stays out of the FileRef deliberately. A FileRef reaches every group member, and any of them could otherwise delete everyone else’s files.
HTTP Binding
Section titled “HTTP Binding”DELETE /api/v1/blobs/{blob_handle}X-Revocation-Token: <32 bytes, base64url>ReportBlob
Section titled “ReportBlob”Flags a blob for operator attention.
- Input:
BlobReport { reason } - Output: None
- Auth: Capability handle
- Idempotent: Yes
- Errors:
BLOB_NOT_FOUND
Requirements
Section titled “Requirements”- Reports MUST be advisory. Hosts MUST NOT auto-delete on report.
- Hosts SHOULD require operator review, or a threshold of independent reports, before removal
- Hosts MUST NOT log reporter identity or source address against a handle
- Reporting spends no token
A reporter is necessarily somene who could decrypt the blob, since only group members learn a handle. That is the most a host can establish without breaking the encryption model: not that the content is bad, but that someone with legitimate access objected.
Auto-deletion on a single report would be a griefing vector - every group member holds the handle for every file shared in that group.
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/blobs/{blob_handle}/reportLifetime
Section titled “Lifetime”A blob is gone when the first of these happens: its TTL expires, max_uses is reached, or the uploader deletes it. Recipients still holding a FileRef get BLOB_NOT_FOUND or BLOB_EXPIRED thereafter.
- Recipients SHOULD cache decrypted files locally rather than relying on availability
- Uploaders MAY re-upload an expired file and announce
FileAction::Relocated, preservingfile_id