From 065ee565f29399e6dae812b22918ad18bfbfbc08 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Thu, 25 Sep 2025 18:04:50 +0100 Subject: [PATCH 1/8] Initial draft of simplified encrypted state events. --- proposals/4362-simplified-encrypted-state.md | 172 +++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 proposals/4362-simplified-encrypted-state.md diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md new file mode 100644 index 00000000000..5c9d3d06058 --- /dev/null +++ b/proposals/4362-simplified-encrypted-state.md @@ -0,0 +1,172 @@ +# MSC4362: Simplified Encrypted State Events + + + +This proposal builds upon the earlier MSC3414, aiming to provide a simplified approach to encrypted +state events in Matrix. Currently, all room state is unencrypted and accessible to everyone in the +room, and occasionally people outside the room (such as via the public room directory, invite state, +or peekable rooms). Most events in room state could be encrypted to provide confidentiality, which +is what this MSC seeks to achieve more straightforwardly. Some parts, however, cannot be encrypted +to maintain a functioning protocol. + +## Proposal + + + +Under this proposal, all room state events can be encrypted, except events critical to maintain the +protocol. Those critical events are: + +- `m.room.create` +- `m.room.member` +- `m.room.join_rules` +- `m.room.power_levels` +- `m.room.third_party_invite` +- `m.room.history_visibility` +- `m.room.guest_access` +- `m.room.encryption` + +An encrypted state event looks very similar to a regular encrypted room message: the `type` becomes +`m.room.encrypted` and the `content` is the same shape as a regular `m.room.encrypted` event. The +`state_key` for encrypted state events is constructed from the plaintext `type` and `state_key` +fields, formatted as `{type}:{state_key}`, preserving the uniqueness of the `type`-`state_key` +mapping required for the server to perform state resolution. + +To track whether a room has state encryption enabled, and to preserve compatibility with older +clients that cannot work with encrypted state events, a new boolean field `encrypt_state_events` is +introduced to the content of `m.room.encryption`, which determines if clients should send state +encrypted events. + +Clients are expected to decrypt all room state on reception and validate the packed state key +matches the decrypted type and state key. This ensures malicious clients cannot send state events +that masquerade as message events and vice versa. + +This MSC relies on the room key sharing mechanism outlined in +[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268), which enables clients to +decrypt historical state events. + +## Potential issues + + + +At present, MSC4268 +[does not require invitees to download the key bundle upon receiving an invite](https://github.com/matrix-org/matrix-spec-proposals/blob/rav/proposal/encrypted_history_sharing/proposals/4268-encrypted-history-sharing.md#actions-as-a-receiving-client); +instead, the key bundle is only fetched when the user joins the room, which could lead to problems +displaying the room name, topic, and avatar to invitees. One way to address this is to always +download the room key bundle on invite, but as MSC4268 notes, this introduces a potential +denial-of-service (DoS) attack vector. + +If the client does not receive the keys needed to decrypt state events, the room may become +unusable, as information such as the room's name, topic, avatar, and other metadata will be +inaccessible. Additionally, if there are state events sent both before and after state encryption is +enabled, existing clients might display the unencrypted, outdated state. + +Encrypting certain state events would prevent servers from displaying meaningful information about +rooms, as the room directory relies on being able to read these events. Rooms with encrypted +metadata could either appear as blank, generic, or broken entries in the public room list, or could +be omitted entirely, impeding room discovery. A similar issue arises with the space room list: if +room metadata is encrypted, clients and servers will be unable to display meaningful information +about child rooms within a space. It may be necessary to introduce an unencrypted state event, +`m.space.child_info`, that stores plaintext copies of a child room's avatar, name, and topic, which +can then be used over the encrypted metadata. + +The `:` delimiter may not be suitable in all cases. Additionally, string packing introduces size +limitations, as the combined length of the packed string cannot exceed the 255-byte maximum for a +state key. This effectively reduces the available space for both event types and state keys. + +## Alternatives + + + +A number of alternatives to string-packing the plaintext `type` and `state_key` are possible: + +- Preserving the values of `type` and `state_key`; +- Introducing an adjacent `true_type` field; +- Hashing `type` and `state_key` with HMAC. + +### Preserved Fields + +Rather than string-packing the `type` and `state_key` together, we could preserve these values on +the encrypted event, but still encrypt the event content. This provides the same (lack of) +confidentiality as the approach laid out in this MSC while avoiding string packing. However, this +approach would introduce a difference between the encryption of message events and state events, +which may be undesirable. + +### Adjacent Type Field + +In a similar manner to preserved fields, we could introduce a new `true_type` field to the events +`content`, which holds the plaintext type of the state event. This would require modifying the +server to utilise this field over the value of the `type` field, which may be undesirable. + +### HMAC-hashed `state_key`s + +This is the _ideal solution_, as it hides the state key and type from the server entirely; however, +there are some considerable downsides. We have two choices: + +- Use a static key generated on room creation to encrypt all state events for the duration of the + room's existence; +- Rotate the key periodically, perhaps deriving it from the current Megolm session key. + +The former case lacks post-compromise confidentiality (PCS), which, although quite hard to pull off +as an attacker, makes this approach undesirable. This approach is also vulnerable to frequency +analysis through comparison between the distribution of state key hashes and a known distribution of +public `type`-`state_key` pairs. + +The latter option has issues too: rotating the key breaks the server's ability to track room state, +since two events with identical state keys will produce encrypted events with different hashed state +keys when using different (HMAC) keys. The server will treat each as unique and send both to +clients. This would require clients to perform state resolution locally (to decide which of two +clashing events to accept), which in turn would require them to consume and understand the room DAG. +This approach may also be vulnerable to frequency analysis, but, based on some naive calculations, +the probability a malicious server is able to infer the hash to `type`-`state_key` mapping correctly +becomes increasingly unlikely as the number of state events encrypted by any given key decreases. + +## Security considerations + +This proposal relies on the security of the Olm/Megolm primitives, and an attack against them could +be a viable method to derive partial or complete knowledge of the encrypted content. + +Confidential information **should not** be stored in the `type` and `state_key` fields, since both +are present in plaintext. + +## Unstable prefix + + + +The current implementation uses an `io.element` vendor prefix for the `encrypt_state_events` flag +(i.e. `io.element.msc3414.encrypt_state_events`) for compatibility. + +## Dependencies + +This MSC builds on +[MSC3414](https://github.com/matrix-org/matrix-spec-propsals/tree/main/proposals/3414-encrypted-state-events.md) +and depends on [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268), neither of +which have been accepted into the spec at the time of writing. From ba98a30395b8d66f5cebaf102c9abd2ad8828d6c Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Fri, 26 Sep 2025 11:23:20 +0100 Subject: [PATCH 2/8] Clarify server administrators can read unencrypted state events. --- proposals/4362-simplified-encrypted-state.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index 5c9d3d06058..b7a83cec693 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -17,9 +17,10 @@ proposal improve Matrix?" - the answer could reveal a small impact, and that is This proposal builds upon the earlier MSC3414, aiming to provide a simplified approach to encrypted state events in Matrix. Currently, all room state is unencrypted and accessible to everyone in the room, and occasionally people outside the room (such as via the public room directory, invite state, -or peekable rooms). Most events in room state could be encrypted to provide confidentiality, which -is what this MSC seeks to achieve more straightforwardly. Some parts, however, cannot be encrypted -to maintain a functioning protocol. +or peekable rooms). The server also has access to these state events in order to perform state +resolution, and so is visible to server administrators. Most events in room state could be encrypted +to provide confidentiality, which is what this MSC seeks to achieve more straightforwardly. Some +parts, however, cannot be encrypted to maintain a functioning protocol. ## Proposal From 6b69056f98344a997d971768de398a0f32dec340 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 12 Nov 2025 13:17:14 +0000 Subject: [PATCH 3/8] Reword intro and Limitations --- proposals/4362-simplified-encrypted-state.md | 66 +++++++++++--------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index b7a83cec693..8a48eb33e04 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -14,13 +14,27 @@ would be a good thing to list here._ _If you're having troubles coming up with a description, a good question to ask is "how does this proposal improve Matrix?" - the answer could reveal a small impact, and that is okay._--> -This proposal builds upon the earlier MSC3414, aiming to provide a simplified approach to encrypted -state events in Matrix. Currently, all room state is unencrypted and accessible to everyone in the -room, and occasionally people outside the room (such as via the public room directory, invite state, -or peekable rooms). The server also has access to these state events in order to perform state -resolution, and so is visible to server administrators. Most events in room state could be encrypted -to provide confidentiality, which is what this MSC seeks to achieve more straightforwardly. Some -parts, however, cannot be encrypted to maintain a functioning protocol. +Currently, all state events are unencrypted. This allows the homeserver to read state event content +in order to do its job in implementing the Matrix protocol: processing room membership and power +levels, and performing state resolution. A side effect of homeservers being able to read state event +content is that anyone with access to the homeserver's data (such as an administrator or a +successful attacker) can also read these events. + +The set of events that are actually needed by the homeserver is quite small, so we propose +encrypting everything else. This provides a significant reduction in the amount of visible metadata, +at the cost of some user inconvenience (because users need decryption keys to see state information +like room names). + +[MSC3414](https://github.com/matrix-org/matrix-spec-proposals/pull/3414) has similar goals to this +proposal, but it specifies a concrete mechanism for hiding encrypted event types, and resolving +state where it cannot be fully resolved by the server. We think this approach could be problematic, +and may effectively require us to implement full state resolution on the client. Here, we simply +propose the "easy" part: encrypting state events without hiding their types from the server. + +The intent is to allow real-world usage of encrypted state, accepting the limitations imposed +because state is hidden from users in situations where they might want it, without requiring us to +draw conclusions on the trickiest parts (sharing historical state, resolving state the server can't +identify, and exposing room names and topics). ## Proposal @@ -62,33 +76,29 @@ This MSC relies on the room key sharing mechanism outlined in [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268), which enables clients to decrypt historical state events. +## Limitations + +### Room names and topics are not visible from outside + +The name and topic of a room with encrypted state will not be visible without access to the keys +used to encrypt them. Without additional proposals, this will make it impossible to provide a room +directory entry, list the room inside a space, or display room details when invited. + +### State sent before joining the room is inaccessible + +Upon joining a room with encrypted state, new users will not be able to decrypt room state, making +the room name, topic and other information (e.g. ongoing whiteboard sessions or call) inaccessible. + +This limitation does not apply if +[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) is available and the room +settings allow sharing the relevant events. + ## Potential issues -At present, MSC4268 -[does not require invitees to download the key bundle upon receiving an invite](https://github.com/matrix-org/matrix-spec-proposals/blob/rav/proposal/encrypted_history_sharing/proposals/4268-encrypted-history-sharing.md#actions-as-a-receiving-client); -instead, the key bundle is only fetched when the user joins the room, which could lead to problems -displaying the room name, topic, and avatar to invitees. One way to address this is to always -download the room key bundle on invite, but as MSC4268 notes, this introduces a potential -denial-of-service (DoS) attack vector. - -If the client does not receive the keys needed to decrypt state events, the room may become -unusable, as information such as the room's name, topic, avatar, and other metadata will be -inaccessible. Additionally, if there are state events sent both before and after state encryption is -enabled, existing clients might display the unencrypted, outdated state. - -Encrypting certain state events would prevent servers from displaying meaningful information about -rooms, as the room directory relies on being able to read these events. Rooms with encrypted -metadata could either appear as blank, generic, or broken entries in the public room list, or could -be omitted entirely, impeding room discovery. A similar issue arises with the space room list: if -room metadata is encrypted, clients and servers will be unable to display meaningful information -about child rooms within a space. It may be necessary to introduce an unencrypted state event, -`m.space.child_info`, that stores plaintext copies of a child room's avatar, name, and topic, which -can then be used over the encrypted metadata. - The `:` delimiter may not be suitable in all cases. Additionally, string packing introduces size limitations, as the combined length of the packed string cannot exceed the 255-byte maximum for a state key. This effectively reduces the available space for both event types and state keys. From 51a14935325a9ca6b79640e894ab2f1234658a9c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 12 Nov 2025 13:19:47 +0000 Subject: [PATCH 4/8] Update unstable prefix to our own MSC number --- proposals/4362-simplified-encrypted-state.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index 8a48eb33e04..35cbf0f1954 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -172,8 +172,9 @@ This section should be used to document things such as what endpoints and names the feature is in development, the name of the unstable feature flag to use to detect support for the feature, or what migration steps are needed to switch to newer versions of the proposal._--> -The current implementation uses an `io.element` vendor prefix for the `encrypt_state_events` flag -(i.e. `io.element.msc3414.encrypt_state_events`) for compatibility. +| Name | Stable prefix | Unstable prefix | +| - | - | - | +| Property in `m.room.encryption` event | `encrypt_state_events` | `io.element.msc4362.encrypt_state_events` | ## Dependencies From 1757d1a89952f6c52ff05fa893c7a927eed7cac8 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 12 Nov 2025 13:20:35 +0000 Subject: [PATCH 5/8] Reword unstable names --- proposals/4362-simplified-encrypted-state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index 35cbf0f1954..e62f5da5119 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -172,7 +172,7 @@ This section should be used to document things such as what endpoints and names the feature is in development, the name of the unstable feature flag to use to detect support for the feature, or what migration steps are needed to switch to newer versions of the proposal._--> -| Name | Stable prefix | Unstable prefix | +| Name | Stable name | Unstable name | | - | - | - | | Property in `m.room.encryption` event | `encrypt_state_events` | `io.element.msc4362.encrypt_state_events` | From 2270db62f30d6f2d6c54b4f280bc312f78d5d1d6 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 12 Nov 2025 13:22:04 +0000 Subject: [PATCH 6/8] Reword dependencies --- proposals/4362-simplified-encrypted-state.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index e62f5da5119..0106d44d393 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -178,7 +178,8 @@ the feature, or what migration steps are needed to switch to newer versions of t ## Dependencies -This MSC builds on -[MSC3414](https://github.com/matrix-org/matrix-spec-propsals/tree/main/proposals/3414-encrypted-state-events.md) -and depends on [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268), neither of -which have been accepted into the spec at the time of writing. +This proposal is a more limited alternative to +[MSC3414](https://github.com/matrix-org/matrix-spec-propsals/tree/main/proposals/3414-encrypted-state-events.md). + +The limitations of this proposal are improved somewhat if +[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) is available. From ff483c5162dc5d842690a8dd5ba7c453ceadd0a3 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Wed, 12 Nov 2025 14:57:59 +0000 Subject: [PATCH 7/8] Add examples for enabling encrypted state events, send, receive, verify. --- proposals/4362-simplified-encrypted-state.md | 120 ++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index 0106d44d393..5a0bffa7746 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -76,6 +76,122 @@ This MSC relies on the room key sharing mechanism outlined in [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268), which enables clients to decrypt historical state events. +## Worked examples + +### Enabling state event encryption + +To enable state event encryption in a room, clients must include the `encrypt_state_events` flag set +to `true` in the `content` of the `m.room.encryption` state event: + +```json +{ + "room_id": "!room:example.org", + "type": "m.room.encryption", + "state_key": "", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "encrypt_state_events": true + } +} +``` + +Once this event is present in the room state, clients that support encrypted state events will begin +encrypting eligible state events according to this proposal, and will also attempt to decrypt any +encrypted state events they receive. Clients that do not support this feature will continue to send +and interpret state events unencrypted. + +### Sending an encrypted state event + +To encrypt an `m.room.name` state event, the client first constructs the "packed state key" by +concatenating the event type and the state key, separated by a colon (`:`), following the template +`:`. For `m.room.name` events, the `state_key` is typically an empty string, +so the packed state key becomes `m.room.name:`. + +Next, the client prepares the plaintext payload to be encrypted. This payload contains the original +event content and state key: + +```json +{ + "room_id": "!room:example.org", + "type": "m.room.name", + "state_key": "", + "content": { + "name": "Example" + } +} +``` + +The client then encrypts this payload using the room's group encryption session (e.g., Megolm), +producing an encrypted payload. The resulting state event that is sent to the server has: + +- The `type` field set to `m.room.encrypted` +- The `state_key` field set to the packed state key (`m.room.name:` in this example) +- The `content` field containing the encrypted payload, structured as in a normal encrypted message + +The final event sent to the room looks like this: + +```json +{ + "room_id": "!room:example.org", + "type": "m.room.encrypted", + "state_key": "m.room.name:", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "", + "device_id": "", + "sender_key": "", + "session_id": "" + } +} +``` + +Clients receiving this event will use the packed state key to determine which state event it +represents, decrypt the payload, and verify that the decrypted `type` and `state_key` match the +packed state key. + +### Reception and packed state key validation + +Suppose a client receives the following encrypted state event: + +```json +{ + "room_id": "!room:example.org", + "type": "m.room.encrypted", + "state_key": "m.room.topic:", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "", + "device_id": "", + "sender_key": "", + "session_id": "" + } +} +``` + +After decryption, the client obtains the following plaintext: + +```json +{ + "room_id": "!room:example.org", + "type": "m.room.topic", + "state_key": "", + "content": { + "topic": "Encrypted topics are cool!" + } +} +``` + +The client must validate the following: + +- The outer event's `state_key` must be present if and only if the inner (decrypted) event's + `state_key` is present. That is, both should either be present or both absent. +- The outer event's `state_key` must be able to be split into a `(type, state_key)` pair using the + `type:state_key` format (for example, `("m.room.topic", "")` in this case). +- The `(type, state_key)` pair obtained from unpacking the outer event's `state_key` must exactly + match the `type` and `state_key` fields found in the decrypted (inner) event. + +If any of these checks fail, the event should be considered invalid and ignored. + ## Limitations ### Room names and topics are not visible from outside @@ -172,8 +288,8 @@ This section should be used to document things such as what endpoints and names the feature is in development, the name of the unstable feature flag to use to detect support for the feature, or what migration steps are needed to switch to newer versions of the proposal._--> -| Name | Stable name | Unstable name | -| - | - | - | +| Name | Stable name | Unstable name | +| ------------------------------------- | ---------------------- | ----------------------------------------- | | Property in `m.room.encryption` event | `encrypt_state_events` | `io.element.msc4362.encrypt_state_events` | ## Dependencies From a67c5aa7d08fa5bc8b3cba815141d4eed2c8c454 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Thu, 13 Nov 2025 11:33:18 +0000 Subject: [PATCH 8/8] docs: Clarify preference for encrypted state events. --- proposals/4362-simplified-encrypted-state.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/proposals/4362-simplified-encrypted-state.md b/proposals/4362-simplified-encrypted-state.md index 5a0bffa7746..3c98067d4ab 100644 --- a/proposals/4362-simplified-encrypted-state.md +++ b/proposals/4362-simplified-encrypted-state.md @@ -61,7 +61,9 @@ An encrypted state event looks very similar to a regular encrypted room message: `m.room.encrypted` and the `content` is the same shape as a regular `m.room.encrypted` event. The `state_key` for encrypted state events is constructed from the plaintext `type` and `state_key` fields, formatted as `{type}:{state_key}`, preserving the uniqueness of the `type`-`state_key` -mapping required for the server to perform state resolution. +mapping required for the server to perform state resolution. In rooms where both encrypted and +unencrypted versions of the same state event exist (that is, for the same `(type, state_key)` pair), +clients **must** use the encrypted version and ignore the unencrypted one. To track whether a room has state encryption enabled, and to preserve compatibility with older clients that cannot work with encrypted state events, a new boolean field `encrypt_state_events` is @@ -104,8 +106,8 @@ and interpret state events unencrypted. To encrypt an `m.room.name` state event, the client first constructs the "packed state key" by concatenating the event type and the state key, separated by a colon (`:`), following the template -`:`. For `m.room.name` events, the `state_key` is typically an empty string, -so the packed state key becomes `m.room.name:`. +`:`. For `m.room.name` events, the `state_key` is typically an empty string, so the +packed state key becomes `m.room.name:`. Next, the client prepares the plaintext payload to be encrypted. This payload contains the original event content and state key: