atagen

malice

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

Blame: MACLTOK_PROTOCOL_LEAKS.md

dffd63b58af4atagen2026-09-28 09:10 UTC1
# macltok request, keep Matrix protocol knowledge out of hosts
dffd63b58af4atagen2026-09-28 09:10 UTC2
dffd63b58af4atagen2026-09-28 09:10 UTC3
## Problem
dffd63b58af4atagen2026-09-28 09:10 UTC4
dffd63b58af4atagen2026-09-28 09:10 UTC5
malice should know how to represent Matrix things (show a homeserver field, print ids as strings)
dffd63b58af4atagen2026-09-28 09:10 UTC6
and nothing more. Today it carries protocol knowledge macltok should own, and some of it is written
dffd63b58af4atagen2026-09-28 09:10 UTC7
twice, once in `malice-server` and once in `malice-worker`. Every other macltok host would have to
dffd63b58af4atagen2026-09-28 09:10 UTC8
write the same code. Five requests, ordered by impact.
dffd63b58af4atagen2026-09-28 09:10 UTC9
dffd63b58af4atagen2026-09-28 09:10 UTC10
## 1. Avatars as `MediaId`, not MXC URIs
dffd63b58af4atagen2026-09-28 09:10 UTC11
dffd63b58af4atagen2026-09-28 09:10 UTC12
`RoomSummary::avatar` and the other `avatar` fields in `model.rs` are `Option<OwnedMxcUri>`, so a host
dffd63b58af4atagen2026-09-28 09:10 UTC13
fetches them with `ClientHandle::mxc_media(&MxcUri, MediaSize)`. Attachments go through the opaque
dffd63b58af4atagen2026-09-28 09:10 UTC14
`MediaId` and `ClientHandle::media`. Because of that split, malice validates raw `mxc://` strings
dffd63b58af4atagen2026-09-28 09:10 UTC15
with `ruma::MxcUri::is_valid` and sends MXC URIs over its HTTP API to the browser and back.
dffd63b58af4atagen2026-09-28 09:10 UTC16
dffd63b58af4atagen2026-09-28 09:10 UTC17
Request, register avatars like attachments and expose them as `Option<MediaId>`, or as
dffd63b58af4atagen2026-09-28 09:10 UTC18
`MediaDescriptor` if the mimetype or size is known. Then hosts fetch every image through
dffd63b58af4atagen2026-09-28 09:10 UTC19
`media(&MediaId, MediaSize)`. `mxc_media` can stay for hosts that want it, but no model field
dffd63b58af4atagen2026-09-28 09:10 UTC20
should require it.
dffd63b58af4atagen2026-09-28 09:10 UTC21
dffd63b58af4atagen2026-09-28 09:10 UTC22
`set_avatar(Option<OwnedMxcUri>)` is fine as it is if its input comes from an upload outcome
dffd63b58af4atagen2026-09-28 09:10 UTC23
and not from the host.
dffd63b58af4atagen2026-09-28 09:10 UTC24
dffd63b58af4atagen2026-09-28 09:10 UTC25
## 2. Soft-logout re-login decision
dffd63b58af4atagen2026-09-28 09:10 UTC26
dffd63b58af4atagen2026-09-28 09:10 UTC27
malice runs the same flow in `malice-server/src/state.rs` (`login`) and
dffd63b58af4atagen2026-09-28 09:10 UTC28
`malice-worker/src/lib.rs` (`Host::login`). It restores the store, and on
dffd63b58af4atagen2026-09-28 09:10 UTC29
`Restore::SoftLoggedOut(soft)` checks whether the submitted username is that user. If so it calls
dffd63b58af4atagen2026-09-28 09:10 UTC30
`soft.password_login(password)` to keep the device and its keys. Otherwise it does a fresh
dffd63b58af4atagen2026-09-28 09:10 UTC31
`PasswordLogin::new(..)`. The username check is copied in both crates.
dffd63b58af4atagen2026-09-28 09:10 UTC32
dffd63b58af4atagen2026-09-28 09:10 UTC33
```rust
dffd63b58af4atagen2026-09-28 09:10 UTC34
fn username_matches(submitted: &str, user_id: &ruma::OwnedUserId) -> bool {
dffd63b58af4atagen2026-09-28 09:10 UTC35
    let submitted = submitted.trim();
dffd63b58af4atagen2026-09-28 09:10 UTC36
    submitted == user_id.as_str() || submitted == user_id.localpart()
dffd63b58af4atagen2026-09-28 09:10 UTC37
}
dffd63b58af4atagen2026-09-28 09:10 UTC38
```
dffd63b58af4atagen2026-09-28 09:10 UTC39
dffd63b58af4atagen2026-09-28 09:10 UTC40
That check is Matrix knowledge (a localpart or a full MXID names the same account), and it is
dffd63b58af4atagen2026-09-28 09:10 UTC41
only correct when the homeserver also matches.
dffd63b58af4atagen2026-09-28 09:10 UTC42
dffd63b58af4atagen2026-09-28 09:10 UTC43
Request, either shape works.
dffd63b58af4atagen2026-09-28 09:10 UTC44
dffd63b58af4atagen2026-09-28 09:10 UTC45
```rust
dffd63b58af4atagen2026-09-28 09:10 UTC46
impl SoftLogout {
dffd63b58af4atagen2026-09-28 09:10 UTC47
    /// whether a login form's homeserver and username name this soft-logged-out account
dffd63b58af4atagen2026-09-28 09:10 UTC48
    pub fn matches(&self, homeserver: &str, username: &str) -> bool;
dffd63b58af4atagen2026-09-28 09:10 UTC49
}
dffd63b58af4atagen2026-09-28 09:10 UTC50
```
dffd63b58af4atagen2026-09-28 09:10 UTC51
dffd63b58af4atagen2026-09-28 09:10 UTC52
Or have `Session::login(PasswordLogin, sessions)` resume a soft-logged-out record in `sessions`
dffd63b58af4atagen2026-09-28 09:10 UTC53
by itself when it names the same account, so hosts never branch on it. The second shape is
dffd63b58af4atagen2026-09-28 09:10 UTC54
preferred because both malice call sites would shrink to one call.
dffd63b58af4atagen2026-09-28 09:10 UTC55
dffd63b58af4atagen2026-09-28 09:10 UTC56
## 3. Homeserver input validation, and a discovery bug it causes
dffd63b58af4atagen2026-09-28 09:10 UTC57
dffd63b58af4atagen2026-09-28 09:10 UTC58
macltok builds the client with `server_name_or_homeserver_url(login.homeserver.trim())`, which
dffd63b58af4atagen2026-09-28 09:10 UTC59
does `.well-known` discovery for a bare server name. malice runs `normalise_homeserver`
dffd63b58af4atagen2026-09-28 09:10 UTC60
(`malice-core/src/login.rs`) first, and that prefixes `https://` onto anything without a scheme.
dffd63b58af4atagen2026-09-28 09:10 UTC61
So a user typing `example.org`, whose homeserver is `matrix.example.org`, gets
dffd63b58af4atagen2026-09-28 09:10 UTC62
`https://example.org` treated as a homeserver URL and discovery never runs. malice will drop its
dffd63b58af4atagen2026-09-28 09:10 UTC63
normaliser once macltok covers what it checks.
dffd63b58af4atagen2026-09-28 09:10 UTC64
dffd63b58af4atagen2026-09-28 09:10 UTC65
- reject empty input and input containing whitespace
dffd63b58af4atagen2026-09-28 09:10 UTC66
- reject schemes other than http and https
dffd63b58af4atagen2026-09-28 09:10 UTC67
- reject a userinfo part (`user@host`) and a missing host
dffd63b58af4atagen2026-09-28 09:10 UTC68
- reject plain http unless the host opts in, and even then only for loopback hosts (`localhost`,
dffd63b58af4atagen2026-09-28 09:10 UTC69
  `127.0.0.1`, `::1`)
dffd63b58af4atagen2026-09-28 09:10 UTC70
dffd63b58af4atagen2026-09-28 09:10 UTC71
Suggested shape is a builder option plus a typed error.
dffd63b58af4atagen2026-09-28 09:10 UTC72
dffd63b58af4atagen2026-09-28 09:10 UTC73
```rust
dffd63b58af4atagen2026-09-28 09:10 UTC74
PasswordLogin::new(homeserver, username, password)
dffd63b58af4atagen2026-09-28 09:10 UTC75
    .allow_insecure_loopback(true)
dffd63b58af4atagen2026-09-28 09:10 UTC76
```
dffd63b58af4atagen2026-09-28 09:10 UTC77
dffd63b58af4atagen2026-09-28 09:10 UTC78
with a distinct `AccountError` variant (or `ErrorKind`) for invalid homeserver input, so a host can
dffd63b58af4atagen2026-09-28 09:10 UTC79
answer 400 with the message instead of a generic login failure. The same validation should apply
dffd63b58af4atagen2026-09-28 09:10 UTC80
to the OAuth login path.
dffd63b58af4atagen2026-09-28 09:10 UTC81
dffd63b58af4atagen2026-09-28 09:10 UTC82
## 4. `AccountError::Operation` hides the homeserver's answer
dffd63b58af4atagen2026-09-28 09:10 UTC83
dffd63b58af4atagen2026-09-28 09:10 UTC84
`Operation` displays as `"{context} failed"` and the useful part (the homeserver's errcode and
dffd63b58af4atagen2026-09-28 09:10 UTC85
message) is only in `source()`. malice has a `describe_error` helper that walks the source chain to
dffd63b58af4atagen2026-09-28 09:10 UTC86
build a readable message. Every host would need it.
dffd63b58af4atagen2026-09-28 09:10 UTC87
dffd63b58af4atagen2026-09-28 09:10 UTC88
Request, include the innermost meaningful cause in `Display`, or add an accessor such as
dffd63b58af4atagen2026-09-28 09:10 UTC89
`AccountError::user_message() -> String` that renders the chain.
dffd63b58af4atagen2026-09-28 09:10 UTC90
dffd63b58af4atagen2026-09-28 09:10 UTC91
## 5. Re-export `ruma`
dffd63b58af4atagen2026-09-28 09:10 UTC92
dffd63b58af4atagen2026-09-28 09:10 UTC93
macltok's public API is typed with ruma (`OwnedRoomId`, `OwnedEventId`, `OwnedUserId`,
dffd63b58af4atagen2026-09-28 09:10 UTC94
`OwnedRoomOrAliasId`, `OwnedMxcUri`), so hosts must depend on ruma at exactly macltok's version.
dffd63b58af4atagen2026-09-28 09:10 UTC95
malice pins `ruma = "0.16"` in two places to match.
dffd63b58af4atagen2026-09-28 09:10 UTC96
dffd63b58af4atagen2026-09-28 09:10 UTC97
Request, `pub use ruma;` in `lib.rs`, same as `imbl`. malice will then parse ids through
dffd63b58af4atagen2026-09-28 09:10 UTC98
`macltok::ruma` and drop its own ruma dependency.
dffd63b58af4atagen2026-09-28 09:10 UTC99
dffd63b58af4atagen2026-09-28 09:10 UTC100
## Not needed
dffd63b58af4atagen2026-09-28 09:10 UTC101
dffd63b58af4atagen2026-09-28 09:10 UTC102
`VerificationAction` already derives `Deserialize` under the `serde` feature, so malice will drop its
dffd63b58af4atagen2026-09-28 09:10 UTC103
hand-written string match on its own.