Two people
Last updated
A private conversation is an MLS group with two principals and no parent. The workspace owner cannot read it, an administrator cannot, a bot cannot, and neither can the relay. Nobody can restore it either, and that is the same fact rather than a separate caveat.
Key packages
Protocol version 2 adds frame tag 22. A device publishes MLS key packages so that somebody can add it to a conversation without the two of them ever being online together, and fetches a peer's so it can.
Key packages are cleartext by construction, because a key package is the object that bootstraps encryption and there is no earlier key to seal it under. The relay has to index them by device key to serve a fetch at all, which is a lookup it cannot perform on an envelope whose kind lives inside the ciphertext. Both facts point the same way: this belongs on the wire, where the relay can see it, and it must not be mistaken for content.
| Form | Direction | Meaning |
|---|---|---|
| publish | client to relay | Store these against the authenticated device key |
| published | relay to client | The shelf's depth afterwards |
| fetch | client to relay | Hand me at most count packages for this device |
| bundles | relay to client | Here they are, and they are gone from the shelf |
| none | relay to client | The shelf is empty. Not an error |
A fetch consumes what it hands out, in the statement that selects it. Serving one package twice would hand two different conversations the same joiner leaf key, so one-time delivery is a property of the query rather than a convention.
An empty shelf is an answer. The correct client behaviour is to wait for the peer to top up rather than to retry, because a retry loop against an empty shelf is what actually drains one.
| Limit | Value | Why |
|---|---|---|
| outstanding packages per device | 100 | Over the cap is refused and nothing is evicted: a silent discard produces an unaddable member with no error anywhere |
| packages per fetch | 8 | Higher is enumeration |
| KEYS frames per connection per minute | 30 | A roster prefetch is a startup burst, not a stream |
The welcome carries no group id
Kind 0x0022 dm.welcome is an ordinary envelope in the workspace root group, which is the only group the two devices already share. It carries a blinded tag, BLAKE3 over a domain label and the target key package reference, and the MLS Welcome itself.
It does not carry the conversation's group id. The joiner reads that from inside the Welcome. Naming it in the record would let every member of the root group learn which pair of people opened a conversation and when, which is precisely the metadata this design refuses to leak.
A tag matching nothing is ignored without comment, because asking about it would be asking the room who a message was for.
The group policy, and the row that cannot be undone
| Property | Value |
|---|---|
| principals | two, and any number of their devices |
| agents | not admitted |
| admission | explicit, so there is no self-join |
| groupinfo.publish | never, so no external commit even holding the root key |
| history.publish | never, so a later-added device reads from that point on only |
| parent | none, so the root key grants nothing and an administrator cannot read it |
| recovery.wrap | never published |
The last row is load-bearing. Losing every device on both sides loses the conversation permanently. A per-pair recovery principal was considered and rejected rather than deferred: it would be an escrow key, and an escrow key that exists is an escrow key that can be compelled.
Both sides derive the same group from the sorted pair of handles, so two people who open a conversation at the same moment converge on one conversation rather than two, with no coordination and no round trip. The derivation is over handles rather than device keys, so a person's second laptop joins the conversation they already have instead of starting another.
What is still visible, stated plainly
The relay learns that a group with two devices exists and when it is written to. Other members of the workspace learn from the root log that some welcome was published, though not by whom, to whom, or about what. The roster tells everybody who is in the workspace, which it did before.
The relay also learns that one device fetched another's key packages. The mitigation is real and partial: a client prefetches the whole roster on first use rather than at conversation-open time, so the request does not time-correlate with the intent to talk to a particular person. What is left is that the relay knows a device intends to be able to talk to its colleagues, which it could have guessed.
Nobody learns who is talking to whom, or about what.