atagen

malice

public
Star0
Watch0
a802a95814e66203129e1b60b219ed262094be91
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
MACLTOK_MEDIA_STREAM.mdMarkdown77 lines3.6 KB

macltok request, what a host needs to stream large media itself

Problem

ClientHandle::media(&MediaId, MediaSize) answers with Media { bytes: Arc<[u8]>, .. }, the whole file, decrypted and hash-checked, capped by EngineConfig::max_media_bytes. That is right for avatars, thumbnails and images. It is wrong for a 20 minute video.

  • In a browser the whole file sits in wasm linear memory, which never shrinks and tops out at 4 GB on wasm32. A few long videos and the worker is carrying hundreds of MB it can never give back.
  • Nothing plays until the last byte has arrived and the hash has been checked, and seeking has to wait for the same.

malice’s on-device build serves media through a service worker so <video> and <audio> get real URLs and Range requests. For large media it wants to answer each Range itself, fetching the matching ciphertext range from the homeserver’s authenticated media endpoint and decrypting it in the service worker with WebCrypto AES-CTR (the counter block is the IV plus offset / 16, so any 16-byte aligned range decrypts on its own). Nothing is buffered and the browser caches only ciphertext. The hash can only be checked on a whole read, so a partial read plays unverified bytes. malice accepts that.

What it can’t get today is what the request needs. MediaRegistration and MediaSourceInfo (normalise/media.rs) are pub(crate), and there is no way to read the access token.

Request

Two read-only accessors on ClientHandle. Suggested shapes, adjust freely.

/// where a registered piece of media lives and how to decrypt it, for a host that fetches it
/// itself. `None` for an unknown id, same as `media` answering `MediaUnknown`
pub fn media_source(&self, media_id: &MediaId, size: MediaSize) -> Option<MediaSourceDetails>;

#[non_exhaustive]
pub struct MediaSourceDetails {
    pub uri: OwnedMxcUri,
    pub mimetype: Option<String>,
    pub declared_size: Option<u64>,
    /// `Some` for an encrypted attachment
    pub encryption: Option<AttachmentEncryption>,
}

#[non_exhaustive]
pub struct AttachmentEncryption {
    /// the JWK `k`, unpadded url-safe base64, AES-256-CTR
    pub key: String,
    /// unpadded base64, the initial counter block
    pub iv: String,
    /// unpadded base64 SHA-256 of the ciphertext, for a host that does read it whole
    pub sha256: String,
}

/// the current access token and the homeserver it belongs to. it changes on every refresh, so
/// ask again after a 401 rather than keeping it
pub fn media_credentials(&self) -> Option<MediaCredentials>;

#[non_exhaustive]
pub struct MediaCredentials {
    pub homeserver: String,
    pub access_token: String,
}

Thumbnail sizes with no uploader thumbnail are server-side resizes, which are never encrypted, so media_source for MediaSize::Thumbnail can answer the uploader’s thumbnail when there is one and the full source otherwise, whichever media would have used.

The token accessor is the sensitive one. It hands a caller the bearer token, which session_record already does in serialized form, so it doesn’t widen what a host can reach. It is worth a doc line saying the token should stay in memory and only go to the homeserver’s media endpoints.

Also worth considering

Streaming decryption inside macltok, media_stream(&MediaId, range) -> impl Stream<Item = Bytes>, would let every host stream without reimplementing AES-CTR and Range handling, and would keep the token inside macltok. matrix-sdk 0.18 decrypts attachments whole (Media::get_media_content), so it would mean reimplementing the attachment decryptor over the raw download. malice will do it host-side for now, so this is not blocking.