atagen

malice

public
Star0
Watch0
00075327fc57931e14ac095900712973885c5a8e
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: MACLTOK_PRESHARE.md

973f8fbdaf80atagen2026-09-28 09:10 UTC1
# macltok request, pre-share room keys before send
973f8fbdaf80atagen2026-09-28 09:10 UTC2
973f8fbdaf80atagen2026-09-28 09:10 UTC3
## Problem
973f8fbdaf80atagen2026-09-28 09:10 UTC4
973f8fbdaf80atagen2026-09-28 09:10 UTC5
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.
973f8fbdaf80atagen2026-09-28 09:10 UTC6
973f8fbdaf80atagen2026-09-28 09:10 UTC7
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
973f8fbdaf80atagen2026-09-28 09:10 UTC8
973f8fbdaf80atagen2026-09-28 09:10 UTC9
1. `sync_members()` if `!are_members_synced()`
973f8fbdaf80atagen2026-09-28 09:10 UTC10
2. `query_keys_for_untracked_or_dirty_users()`, a `/keys/query` that the homeserver fans out over federation
973f8fbdaf80atagen2026-09-28 09:10 UTC11
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
973f8fbdaf80atagen2026-09-28 09:10 UTC12
973f8fbdaf80atagen2026-09-28 09:10 UTC13
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.
973f8fbdaf80atagen2026-09-28 09:10 UTC14
973f8fbdaf80atagen2026-09-28 09:10 UTC15
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`
973f8fbdaf80atagen2026-09-28 09:10 UTC16
973f8fbdaf80atagen2026-09-28 09:10 UTC17
```rust
973f8fbdaf80atagen2026-09-28 09:10 UTC18
// TODO: expose this publicly so people can pre-share a group session if
973f8fbdaf80atagen2026-09-28 09:10 UTC19
// e.g. a user starts to type a message for a room.
973f8fbdaf80atagen2026-09-28 09:10 UTC20
```
973f8fbdaf80atagen2026-09-28 09:10 UTC21
973f8fbdaf80atagen2026-09-28 09:10 UTC22
## What is reachable today
973f8fbdaf80atagen2026-09-28 09:10 UTC23
973f8fbdaf80atagen2026-09-28 09:10 UTC24
Checked against the 0.18 sources. Only the first step is public.
973f8fbdaf80atagen2026-09-28 09:10 UTC25
973f8fbdaf80atagen2026-09-28 09:10 UTC26
| step | public from `matrix_sdk`? |
973f8fbdaf80atagen2026-09-28 09:10 UTC27
|---|---|
973f8fbdaf80atagen2026-09-28 09:10 UTC28
| `Room::sync_members`, `are_members_synced` | yes |
973f8fbdaf80atagen2026-09-28 09:10 UTC29
| `Room::query_keys_for_untracked_or_dirty_users` | no, private. `Encryption::request_user_identity(user_id)` does a per-user `/keys/query` |
973f8fbdaf80atagen2026-09-28 09:10 UTC30
| `Client::claim_one_time_keys` | no, `pub(crate)` |
973f8fbdaf80atagen2026-09-28 09:10 UTC31
| `Room::share_room_key`, `Room::preshare_room_key` | no, private. `OlmMachine::share_room_key` is pub but `Client::olm_machine()` is `pub(crate)` |
973f8fbdaf80atagen2026-09-28 09:10 UTC32
| `ensure_room_encryption_ready` | no, private free fn |
973f8fbdaf80atagen2026-09-28 09:10 UTC33
973f8fbdaf80atagen2026-09-28 09:10 UTC34
There is no public encrypt-without-send path either. Every caller of `ensure_room_encryption_ready` goes on to send.
973f8fbdaf80atagen2026-09-28 09:10 UTC35
973f8fbdaf80atagen2026-09-28 09:10 UTC36
## Request
973f8fbdaf80atagen2026-09-28 09:10 UTC37
973f8fbdaf80atagen2026-09-28 09:10 UTC38
Expose a command that runs the send path's encryption prep without sending anything, fire and forget, deduplicated per room.
973f8fbdaf80atagen2026-09-28 09:10 UTC39
973f8fbdaf80atagen2026-09-28 09:10 UTC40
```rust
973f8fbdaf80atagen2026-09-28 09:10 UTC41
impl ClientHandle {
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    /// no-op for unencrypted rooms, coalesces with an in-flight prepare for the same room
973f8fbdaf80atagen2026-09-28 09:10 UTC43
    pub async fn prepare_to_send(&self, room_id: OwnedRoomId) -> Result<(), Error>;
973f8fbdaf80atagen2026-09-28 09:10 UTC44
}
973f8fbdaf80atagen2026-09-28 09:10 UTC45
```
973f8fbdaf80atagen2026-09-28 09:10 UTC46
973f8fbdaf80atagen2026-09-28 09:10 UTC47
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).
973f8fbdaf80atagen2026-09-28 09:10 UTC48
973f8fbdaf80atagen2026-09-28 09:10 UTC49
Suggested route, in order of preference
973f8fbdaf80atagen2026-09-28 09:10 UTC50
973f8fbdaf80atagen2026-09-28 09:10 UTC51
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.
973f8fbdaf80atagen2026-09-28 09:10 UTC52
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.
973f8fbdaf80atagen2026-09-28 09:10 UTC53
973f8fbdaf80atagen2026-09-28 09:10 UTC54
malice will pair it with the existing `ClientHandle::set_typing(room_id, bool)` (`transport.rs:357`), so no new typing API is needed.
973f8fbdaf80atagen2026-09-28 09:10 UTC55
973f8fbdaf80atagen2026-09-28 09:10 UTC56
## Not requested
973f8fbdaf80atagen2026-09-28 09:10 UTC57
973f8fbdaf80atagen2026-09-28 09:10 UTC58
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.