atagen

malice

public
Star0
Watch0
1b586ca058592ee4ddfa70958d7c0c4ff0c90c69
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
MACLTOK_PRESHARE.mdMarkdown58 lines4.3 KB

macltok request, pre-share room keys before send

Problem

Sending into an encrypted room is slow in malice (and Element X), and under federation lag it can take over a minute. Clients on other SDKs feel instant because they do the key work before the user presses send.

matrix-sdk 0.18 does all of it inline on the send path. SendRawMessageLikeEvent::into_future (matrix-sdk-0.18.0/src/room/futures.rs:190-245) calls the private ensure_room_encryption_ready (futures.rs:523-537), which runs, in the room’s single send-queue worker and before the /send PUT

  1. sync_members() if !are_members_synced()
  2. query_keys_for_untracked_or_dirty_users(), a /keys/query that the homeserver fans out over federation
  3. preshare_room_key() (room/mod.rs:2396-2431), which claims one-time keys (/keys/claim, 10s server timeout, also federated) and then sends the megolm key to every device as to-device messages, awaiting each

Every request uses the default 30s timeout, and the send queue retries with short_retry (3 attempts, exponential backoff). A slow remote server stalls the send and everything queued behind it in that room.

For comparison, matrix-dart-sdk calls prepareOutboundGroupSession unawaited from Client.setTyping, so FluffyChat has usually finished steps 1 to 3 by the time the user hits send. matrix-js-sdk exposes CryptoApi.prepareToEncrypt(room) for the same purpose. matrix-sdk has the primitive but keeps it private, with this at room/mod.rs:2392

// TODO: expose this publicly so people can pre-share a group session if
// e.g. a user starts to type a message for a room.

What is reachable today

Checked against the 0.18 sources. Only the first step is public.

steppublic from matrix_sdk?
Room::sync_members, are_members_syncedyes
Room::query_keys_for_untracked_or_dirty_usersno, private. Encryption::request_user_identity(user_id) does a per-user /keys/query
Client::claim_one_time_keysno, pub(crate)
Room::share_room_key, Room::preshare_room_keyno, private. OlmMachine::share_room_key is pub but Client::olm_machine() is pub(crate)
ensure_room_encryption_readyno, private free fn

There is no public encrypt-without-send path either. Every caller of ensure_room_encryption_ready goes on to send.

Request

Expose a command that runs the send path’s encryption prep without sending anything, fire and forget, deduplicated per room.

impl ClientHandle {
    /// no-op for unencrypted rooms, coalesces with an in-flight prepare for the same room
    pub async fn prepare_to_send(&self, room_id: OwnedRoomId) -> Result<(), Error>;
}

malice would call it when the user starts typing in a room (alongside a typing notice) and probably when a room is opened. A later send then finds members synced, device lists fresh and the outbound session already shared, so only the /send PUT remains. Correctness is unchanged, since the send path still runs ensure_room_encryption_ready and redoes whatever went stale in between (a member joining after the prepare, say).

Suggested route, in order of preference

  1. Upstream a small matrix-rust-sdk PR making Room::preshare_room_key public, or adding pub async fn Room::prepare_encryption(&self) that wraps ensure_room_encryption_ready. The TODO invites exactly this. Until it is released macltok could carry it via a [patch.crates-io] git fork, if that fits its dependency policy.
  2. A partial warm-up using only public API while the PR is pending. sync_members() when not synced, then request_user_identity for members whose device lists are unknown, capped (members can be many and each query is a federated round trip). This removes step 1 and the /keys/query part of step 2 from the send path, which is the part federation lag hurts most. Steps 3 (/keys/claim and the to-device share) stay on the send path. It does not know which users are dirty (only tracked_users() is public), so it can over-query. Worth it only if option 1 will take a while.

malice will pair it with the existing ClientHandle::set_typing(room_id, bool) (transport.rs:357), so no new typing API is needed.

Not requested

A “send without waiting for fresh device lists” mode. It would encrypt to stale device lists, so new members and new devices miss the key and see undecryptable messages. Pre-sharing gives the same latency without that.