From 68ffc62de3da55353cc9fd52cb0e775ee453c15c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:00:12 +0100 Subject: [PATCH] Specify history sharing (#2399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Commaille <76261501+zecakeh@users.noreply.github.com> Co-authored-by: Johannes Marbach Co-authored-by: Hubert Chathi --- .../client_server/newsfragments/2399.feature | 1 + .../modules/end_to_end_encryption.md | 169 +++++++++++++++++- .../definitions/key_backup_session_data.yaml | 6 + .../definitions/room_key_bundle.yaml | 109 +++++++++++ data/event-schemas/examples/m.room_key.yaml | 3 +- .../examples/m.room_key_bundle.yaml | 21 +++ .../components/room_key_withheld_content.yaml | 49 +++++ .../schema/m.room_key.withheld.yaml | 55 +----- data/event-schemas/schema/m.room_key.yaml | 13 ++ .../schema/m.room_key_bundle.yaml | 42 +++++ 10 files changed, 420 insertions(+), 48 deletions(-) create mode 100644 changelogs/client_server/newsfragments/2399.feature create mode 100644 data/api/client-server/definitions/room_key_bundle.yaml create mode 100644 data/event-schemas/examples/m.room_key_bundle.yaml create mode 100644 data/event-schemas/schema/components/room_key_withheld_content.yaml create mode 100644 data/event-schemas/schema/m.room_key_bundle.yaml diff --git a/changelogs/client_server/newsfragments/2399.feature b/changelogs/client_server/newsfragments/2399.feature new file mode 100644 index 00000000..7e707458 --- /dev/null +++ b/changelogs/client_server/newsfragments/2399.feature @@ -0,0 +1 @@ +Specify encrypted history sharing, as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). diff --git a/content/client-server-api/modules/end_to_end_encryption.md b/content/client-server-api/modules/end_to_end_encryption.md index 88a97bdd..e1780ddd 100644 --- a/content/client-server-api/modules/end_to_end_encryption.md +++ b/content/client-server-api/modules/end_to_end_encryption.md @@ -1547,6 +1547,143 @@ objects described as follows: {{% definition path="api/client-server/definitions/megolm_export_session_data" %}} +#### Sharing keys between users + +{{% added-in v="1.19" %}} + +When Alice invites Bob to an encrypted room, she might want Bob to have access +to messages that were previously sent in that room, subject to the [history +visibility](#room-history-visibility) setting of the room. + +Alice does this by constructing an [encrypted key +bundle](#construction-and-sharing-of-the-key-bundle) and sharing it with Bob +before inviting him. + +##### Shareable encryption sessions + +Only room keys which were marked as "shareable" by the creator of the +encryption session should be shared with new users. + +When a user wants to send a message in an encrypted room, their client first +checks the [history visibility](#room-history-visibility) state of the +room. If it is `shared` or `world_readable`, then when the client sends the Megolm +keys to room members via [`m.room_key`](#mroom_key) messages, it SHOULD set +`shared_history` to `true`. Otherwise, it SHOULD set `shared_history` to `false`. + +If the history visibility changes in a way that would affect the +`shared_history` flag (i.e., it changes from `joined` or `invited` to `shared` +or `world_readable`, or vice versa), then clients MUST rotate their outbound +Megolm session before sending more messages. + +Clients SHOULD show an indication to users that their encrypted messages +may be shared with future room members in this way. + +Recipients SHOULD keep a record of the `shared_history` flag for each +encryption session. The state of the flag is also saved in the +[`BackedUpSessionData`](#definition-backedupsessiondata) type in key backups, +and the [`ExportedSessionData`](#definition-exportedsessiondata) type in key +exports. + +A session is therefore considered as "shareable" if any of the following +conditions are satisfied: + + * The client created the session itself, when the `history_visibility` state + was set to `shared` or `world_readable`. + * The keys to the session were received from an [`m.room_key`](#mroom_key) + message with `shared_history` set to `true`. + * The keys to the session were loaded from key backup, and the + [`BackedUpSessionData`](#definition-backedupsessiondata) structure had + `shared_history` set to `true`. + * The keys to the session were loaded from a key export, and the + [`ExportedSessionData`](#definition-exportedsessiondata) structure had + `shared_history` set to `true`. + * The keys were received as part of a + [`RoomKeyBundle`](#definition-roomkeybundle). + +{{% boxes/note %}} +Tracking shareable sessions in this way prevents an attack where a malicious +homeserver can incorrectly flag history as shared, without telling the sender +of the messages. It also ensures that a user that sends an encrypted message does +so in the knowledge that the message may be shared with new members in the +room. +{{% /boxes/note %}} + +##### Construction and sharing of the key bundle + +Alice's client MAY choose not to share any room history (even messages sent when the +history visibity setting would allow sharing) if the current history +visibility setting does not allow sharing (i.e. if `history_visibility` is +set to `invited` or `joined`). + +Otherwise, before inviting Bob to a room, Alice's client constructs and sends a key bundle as follows: + +1. Alice's client SHOULD ensure that it has downloaded all keys relevant to the room + from [server-side key backup](#server-side-key-backups), if she is using it. + +2. Alice's client constructs a [`RoomKeyBundle`](#definition-roomkeybundle) structure, + containing the sessions she is aware of in the room. Alice MUST include + only [shareable encryption sessions](#shareable-encryption-sessions) in the + `room_keys` section of the structure; other sessions SHOULD be listed in the + `withheld` section. + +3. The client serialises the `RoomKeyBundle` as JSON. + +4. Alice's client encrypts and uploads the serialised JSON in the same way as when + [sending an encrypted attachment](#sending-encrypted-attachments). + +5. Alice's client ensures she has an up-to-date list of Bob's devices (performing a + [`/keys/query`](#post_matrixclientv3keysquery) request if necessary). + +6. For each of Bob's devices which are correctly + [cross-signed](#cross-signing), Alice's client encrypts and sends an + [`m.room_key_bundle`](#mroom_key_bundle) message. + +Alice's client MUST NOT send the `m.room.key_bundle` message to devices that have not +been correctly cross-signed by their owner, due to the risk of sharing +significant amounts of encrypted content with an attacker-controlled device. + +{{% definition path="api/client-server/definitions/room_key_bundle" %}} + +{{% event event="m.room_key_bundle" %}} + +##### Receiving a key bundle event + +When Bob's client receives an `m.room_key_bundle` event from Alice, there are two possibilities: + + * If Bob has recently accepted an invite to the room from Alice, the client + SHOULD immediately download and decrypt the key bundle and start processing + it. Note, however, that this process must be resilient to Bob's client being + restarted before the download/import completes. + + The definition of "recently" is left up to clients. (They should consider + balancing the needs of (a) a user that closes their client just after + joining a room but before the bundle is imported, against (b) the overhead + of attempting to download a key bundle on every startup. 24 hours is a + recommended time limit.) + + * Otherwise, Bob's client SHOULD store the details of the key bundle but not + download it immediately. If he later accepts an invite to the room from + Alice, his client downloads and processes the bundle at that point. + + Delaying the download in this way avoids a potential DoS vector in which an + attacker can cause the victim to download a large quantity of useless data. + +Once Bob has downloaded and decrypted the key bundle, the sessions are imported +as they would be when importing a [key export](#key-exports); however: + + * Only keys for the relevant room should be imported. Keys for other rooms + SHOULD be ignored. + + * Bob's client MUST remember who he received the keys from (Alice, in this + case), and MUST show that information to the user, since he has only that + user's word for the authenticity of those sessions. + +Client implementations should note that server implementations may delete or +expire old media that appears unused. They must therefore gracefully handle +download failures due to the key bundle having expired (typically by just +giving up on the attempt to download the bundle, though they could also warn +the user.) + #### Messaging Algorithms ##### Messaging Algorithm Names @@ -1830,7 +1967,7 @@ In order to enable end-to-end encryption in a room, clients can send an When creating a Megolm session in a room, clients must share the corresponding session key using Olm with the intended recipients, so that they can decrypt future messages encrypted using this session. An -`m.room_key` event is used to do this. Clients must also handle +[`m.room_key`](#mroom_key) event is used to do this. Clients must also handle `m.room_key` events sent by other devices in order to decrypt their messages. @@ -1845,6 +1982,36 @@ When a client is updating a Megolm session in its store, the client MUST ensure: user, or from a `m.room_key` event. * that the new session key has a lower message index than the existing session key. +When encrypting outgoing messages in a room using Megolm, clients MUST rotate +their outgoing Megolm session (i.e. discard the existing session, and create +and share a new session before sending more room messages) whenever any of the +following happens: + + * The existing session has been in use for longer than the period specified in + `rotation_period_ms` in the [`m.room.encryption`](#mroomencryption) room + state event, or an appropriate default. + + * The existing session has been used to encrypt as many messages as specified in + `rotation_period_msgs` in the [`m.room.encryption`](#mroomencryption) room + state event, or an appropriate default. + + * A user or device that was previously participating in the room, and may have + received a copy of the decryption keys for the session, is seen to leave the + room. + + {{% changed-in v="1.19" %}} Since any user that received an invite to the + room may have received a copy of the decryption keys for the session via + [history sharing](#sharing-keys-between-users), clients MUST observe changes + in state in the room, and whenever they see a user leaving the room, assume + that the departed user may have access to any existing Megolm session, and + rotate the session. Note that, in a `limited` [sync](#syncing), clients must + treat any membership event with a membership other than `join` as an + indication that the affected user may have joined and left the room. + + * {{% added-in v="1.19" %}} The [history visibility](#room-history-visibility) + state of the room changes in a way that would affect the `shared_history` + flag: see [shareable encryption sessions](#shareable-encryption-sessions). + #### Protocol definitions ##### Events diff --git a/data/api/client-server/definitions/key_backup_session_data.yaml b/data/api/client-server/definitions/key_backup_session_data.yaml index b5878471..2a38cf0f 100644 --- a/data/api/client-server/definitions/key_backup_session_data.yaml +++ b/data/api/client-server/definitions/key_backup_session_data.yaml @@ -49,6 +49,12 @@ properties: description: |- Unpadded base64-encoded session key in [session-export format](https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format). example: "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8Llf..." + shared_history: + x-addedInMatrixVersion: "1.19" + type: boolean + description: |- + Whether the session is [shareable](/client-server-api/#shareable-encryption-sessions). + example: false required: - algorithm - forwarding_curve25519_key_chain diff --git a/data/api/client-server/definitions/room_key_bundle.yaml b/data/api/client-server/definitions/room_key_bundle.yaml new file mode 100644 index 00000000..634ffd8a --- /dev/null +++ b/data/api/client-server/definitions/room_key_bundle.yaml @@ -0,0 +1,109 @@ +# Copyright 2026 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +type: object +title: RoomKeyBundle +description: | + A bundle of room keys and withheld indications, sent from one user to another, to share encrypted history. + + A single session MUST NOT appear in both the `room_keys` and `withheld` + sections. Handling such malformed bundles as a receiving client is + implementation-defined. +properties: + room_keys: + description: | + The room keys to be shared with the recipient of the bundle. + + The data type is similar to the format used for [key exports](/client-server-api/#key-export-format), + but omits `forwarding_curve25519_key_chain` and `shared_history`. + type: array + items: + type: object + title: HistoricRoomKey + description: |- + The format of a session key, when shared as part of a `RoomKeyBundle`. + properties: + algorithm: + type: string + description: |- + The end-to-end message encryption algorithm that the key is for. Must be `m.megolm.v1.aes-sha2`. + example: "m.megolm.v1.aes-sha2" + room_id: + type: string + format: mx-room-id + pattern: "^!" + description: |- + The room where the session is used. + example: "!Cuyf34gef24t:localhost" + sender_claimed_keys: + type: object + additionalProperties: + type: string + format: mx-unpadded-base64 + description: |- + A map from algorithm name (`ed25519`) to the Ed25519 signing key of + the device which initiated the session originally, according to the + creator of this key bundle. + example: { "ed25519": "aj40p+aw64yPIdsxoog8jhPu9i7l7NcFRecuOQblE3Y" } + sender_key: + type: string + format: mx-unpadded-base64 + description: |- + Unpadded base64-encoded device Curve25519 key, of the device which + initiated the session originally, according to the creator of this + key bundle. + example: "RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU" + session_id: + type: string + description: |- + The Megolm session ID. + example: "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ" + session_key: + type: string + format: mx-unpadded-base64 + description: |- + Unpadded base64-encoded session key in [session-export + format](/olm-megolm/megolm/#session-export-format). + example: "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8Llf..." + required: + - algorithm + - room_id + - sender_claimed_keys + - sender_key + - session_id + - session_key + withheld: + description: |- + The room keys that the creator of the bundle is choosing not to share + with the recipient. + + The `code` will normally be `m.history_not_shared`, to indicate that the + recipient isn't allowed to receive the key. + type: array + items: + allOf: + - $ref: "../../../event-schemas/schema/components/room_key_withheld_content.yaml" + - title: RoomKeyWithheld + example: { + "algorithm": "m.megolm.v1.aes-sha2", + "code": "m.history_not_shared", + "reason": "History not shared", + "room_id": "!Cuyf34gef24t:localhost", + "sender_key": "RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU", + "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ" + } + + + + diff --git a/data/event-schemas/examples/m.room_key.yaml b/data/event-schemas/examples/m.room_key.yaml index dba497b4..1dfe8809 100644 --- a/data/event-schemas/examples/m.room_key.yaml +++ b/data/event-schemas/examples/m.room_key.yaml @@ -5,6 +5,7 @@ "algorithm": "m.megolm.v1.aes-sha2", "room_id": "!Cuyf34gef24t:localhost", "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ", - "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..." + "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY...", + "shared_history": false } } diff --git a/data/event-schemas/examples/m.room_key_bundle.yaml b/data/event-schemas/examples/m.room_key_bundle.yaml new file mode 100644 index 00000000..adfa29b9 --- /dev/null +++ b/data/event-schemas/examples/m.room_key_bundle.yaml @@ -0,0 +1,21 @@ +{ + "type": "m.room_key_bundle", + "content": { + "room_id": "!Cuyf34gef24t:localhost", + "file": { + "v": "v2", + "url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe", + "key": { + "alg": "A256CTR", + "ext": true, + "k": "aWF6-32KGYaC3A_FEUCk1Bt0JA37zP0wrStgmdCaW-0", + "key_ops": ["encrypt","decrypt"], + "kty": "oct" + }, + "iv": "w+sE15fzSc0AAAAAAAAAAA", + "hashes": { + "sha256": "fdSLu/YkRx3Wyh3KQabP3rd6+SFiKg5lsJZQHtkSAYA" + } + } + } +} diff --git a/data/event-schemas/schema/components/room_key_withheld_content.yaml b/data/event-schemas/schema/components/room_key_withheld_content.yaml new file mode 100644 index 00000000..ec7bc136 --- /dev/null +++ b/data/event-schemas/schema/components/room_key_withheld_content.yaml @@ -0,0 +1,49 @@ +type: object +properties: + algorithm: + type: string + enum: ["m.megolm.v1.aes-sha2"] + description: |- + The encryption algorithm for the key that this event is about. + room_id: + type: string + format: mx-room-id + pattern: "^!" + description: |- + Required if `code` is not `m.no_olm`. The room for the key that + this event is about. + session_id: + type: string + description: |- + Required if `code` is not `m.no_olm`. The session ID of the key + that this event is about. + sender_key: + type: string + format: mx-unpadded-base64 + description: |- + The unpadded base64-encoded device curve25519 key of the event\'s + sender. + code: + type: string + enum: + - m.blacklisted + - m.unverified + - m.unauthorised + - m.unavailable + - m.no_olm + - m.history_not_shared + description: |- + A machine-readable code for why the key was not sent. Codes beginning + with `m.` are reserved for codes defined in the Matrix + specification. Custom codes must use the Java package naming + convention. + reason: + type: string + description: |- + A human-readable reason for why the key was not sent. The receiving + client should only use this string if it does not understand the + `code`. +required: + - algorithm + - sender_key + - code diff --git a/data/event-schemas/schema/m.room_key.withheld.yaml b/data/event-schemas/schema/m.room_key.withheld.yaml index 185aa902..96224fef 100644 --- a/data/event-schemas/schema/m.room_key.withheld.yaml +++ b/data/event-schemas/schema/m.room_key.withheld.yaml @@ -8,7 +8,7 @@ description: |- This event type is used to indicate that the sender is not sharing room keys with the recipient. It is sent as a to-device event. - Possible values for `code` include: + The following values for `code` are defined: * `m.blacklisted`: the user/device was blacklisted. * `m.unverified`: the user/device was not verified, and the sender is only @@ -19,6 +19,13 @@ description: |- * `m.unavailable`: sent in reply to a key request if the device that the key is requested from does not have the requested key. * `m.no_olm`: an olm session could not be established. + * **[Added in `v1.19`]** `m.history_not_shared`: when used as part of a + [`RoomKeyBundle` structure](/client-server-api/#definition-roomkeybundle), + indicates to the recipient of the bundle that the corresponding room key + was not marked as + [shareable](/client-server-api/#shareable-encryption-sessions) by the + sender. Clients SHOULD NOT use this `code` in a `m.room_key.withheld` + event. In most cases, this event refers to a specific room key. The one exception to this is when the sender is unable to establish an olm session with the @@ -36,51 +43,7 @@ description: |- room messages sent before it receives the message. properties: content: - properties: - algorithm: - type: string - enum: ["m.megolm.v1.aes-sha2"] - description: |- - The encryption algorithm for the key that this event is about. - room_id: - type: string - description: |- - Required if `code` is not `m.no_olm`. The room for the key that - this event is about. - session_id: - type: string - description: |- - Required if `code` is not `m.no_olm`. The session ID of the key - that this event is about. - sender_key: - type: string - description: |- - The unpadded base64-encoded device curve25519 key of the event\'s - sender. - code: - type: string - enum: - - m.blacklisted - - m.unverified - - m.unauthorised - - m.unavailable - - m.no_olm - description: |- - A machine-readable code for why the key was not sent. Codes beginning - with `m.` are reserved for codes defined in the Matrix - specification. Custom codes must use the Java package naming - convention. - reason: - type: string - description: |- - A human-readable reason for why the key was not sent. The receiving - client should only use this string if it does not understand the - `code`. - required: - - algorithm - - sender_key - - code - type: object + $ref: "components/room_key_withheld_content.yaml" type: enum: - m.room_key.withheld diff --git a/data/event-schemas/schema/m.room_key.yaml b/data/event-schemas/schema/m.room_key.yaml index c8c76bb4..696a288d 100644 --- a/data/event-schemas/schema/m.room_key.yaml +++ b/data/event-schemas/schema/m.room_key.yaml @@ -25,6 +25,19 @@ properties: session_key: type: string description: The key to be exchanged. + shared_history: + x-addedInMatrixVersion: "1.19" + type: boolean + description: | + `true` indicates that the creator of this encryption key considers that + the session is [shareable](/client-server-api/#shareable-encryption-sessions): + in other words, the sender has observed that the [room history + visibility](/client-server-api/#room-history-visibility) is set to + `shared` or `world_readable`, and that they understand and agree that + the session keys may be shared with newly-invited users in future. + + Absence, or any other value, indicates that the creator of the + session does not consider the session to be shareable. required: - algorithm - room_id diff --git a/data/event-schemas/schema/m.room_key_bundle.yaml b/data/event-schemas/schema/m.room_key_bundle.yaml new file mode 100644 index 00000000..a0e24f36 --- /dev/null +++ b/data/event-schemas/schema/m.room_key_bundle.yaml @@ -0,0 +1,42 @@ +--- +$schema: https://json-schema.org/draft/2020-12/schema + +allOf: + - $ref: core-event-schema/event.yaml + +description: |- + This event type is used to share a bundle of room keys when inviting a new user to the room. + + It is encrypted as an + [`m.room.encrypted`](/client-server-api/#mroomencrypted) [to-device + event](/client-server-api/#send-to-device-messaging) using + [Olm](/client-server-api/#molmv1curve25519-aes-sha2). + + The `sender_device_keys` property in the [Olm + plaintext](/client-server-api/#definition-olmpayload) MUST be + populated. Recipients SHOULD ignore `m.room_key_bundle` messages which omit + them. +properties: + content: + properties: + room_id: + type: string + format: mx-room-id + pattern: "^!" + description: The room to which the keys in the key bundle relate. + file: + description: |- + An [EncryptedFile](/client-server-api/#definition-encryptedfile) + structure. The location and decryption keys for the encrypted + [`RoomKeyBundle`](/client-server-api/#definition-roomkeybundle). + title: EncryptedFile + type: object + required: + - room_id + - file + type: object + type: + enum: + - m.room_key_bundle + type: string +type: object