matrix_sdk/room/
shared_room_history.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::iter;
16
17use matrix_sdk_base::{
18    crypto::types::events::room_key_bundle::RoomKeyBundleContent,
19    media::{MediaFormat, MediaRequestParameters},
20};
21use ruma::{OwnedUserId, UserId, events::room::MediaSource};
22use tracing::{info, instrument, warn};
23
24use crate::{Error, Result, Room};
25
26/// Share any shareable E2EE history in the given room with the given recipient,
27/// as per [MSC4268].
28///
29/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
30#[instrument(skip(room), fields(room_id = ?room.room_id()))]
31pub(super) async fn share_room_history(room: &Room, user_id: OwnedUserId) -> Result<()> {
32    let client = &room.client;
33
34    // 0. We can only share room history if our user has set up cross signing
35    let own_identity = match client.user_id() {
36        Some(own_user) => client.encryption().get_user_identity(own_user).await?,
37        None => None,
38    };
39
40    if own_identity.is_none() {
41        warn!("Not sharing message history as cross-signing is not set up");
42        return Ok(());
43    }
44
45    info!("Sharing message history");
46
47    let olm_machine = client.olm_machine().await;
48    let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
49
50    // 1. Construct the key bundle
51    let bundle = olm_machine.store().build_room_key_bundle(room.room_id()).await?;
52
53    if bundle.is_empty() {
54        info!("No keys to share");
55        return Ok(());
56    }
57
58    // 2. Upload to the server as an encrypted file
59    let json = serde_json::to_vec(&bundle)?;
60    let upload = client.upload_encrypted_file(&mut (json.as_slice())).await?;
61
62    info!(
63        media_url = ?upload.url,
64        shared_keys = bundle.room_keys.len(),
65        withheld_keys = bundle.withheld.len(),
66        "Uploaded encrypted key blob"
67    );
68
69    // 3. Ensure that we get a fresh list of devices for the invited user.
70    let (req_id, request) = olm_machine.query_keys_for_users(iter::once(user_id.as_ref()));
71
72    if !request.device_keys.is_empty() {
73        room.client.keys_query(&req_id, request.device_keys).await?;
74    }
75
76    // 4. Establish Olm sessions with all of the recipient's devices.
77    client.claim_one_time_keys(iter::once(user_id.as_ref())).await?;
78
79    // 5. Send to-device messages to the recipient to share the keys.
80    let content = RoomKeyBundleContent { room_id: room.room_id().to_owned(), file: upload };
81    let requests = {
82        let olm_machine = client.olm_machine().await;
83        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
84        olm_machine
85            .share_room_key_bundle_data(
86                &user_id,
87                &client.base_client().room_key_recipient_strategy,
88                content,
89            )
90            .await?
91    };
92
93    for request in requests {
94        let response = client.send_to_device(&request).await?;
95        client.mark_request_as_sent(&request.txn_id, &response).await?;
96    }
97
98    Ok(())
99}
100
101/// Having accepted an invite for the given room from the given user, attempt to
102/// find a information about a room key bundle and, if found, download the
103/// bundle and import the room keys, as per [MSC4268].
104///
105/// # Arguments
106///
107/// * `room` - The room we were invited to, for which we want to check if a room
108///   key bundle was received.
109///
110/// * `inviter` - The user who invited us to the room and is expected to have
111///   sent the room key bundle.
112///
113/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
114#[instrument(skip(room), fields(room_id = ?room.room_id(), bundle_sender))]
115pub(crate) async fn maybe_accept_key_bundle(room: &Room, inviter: &UserId) -> Result<()> {
116    // TODO: retry this if it gets interrupted or it fails.
117    // TODO: do this in the background.
118
119    let client = &room.client;
120    let olm_machine = client.olm_machine().await;
121
122    let Some(olm_machine) = olm_machine.as_ref() else {
123        warn!("Not fetching room key bundle as the Olm machine is not available");
124        return Ok(());
125    };
126
127    let Some(bundle_info) =
128        olm_machine.store().get_received_room_key_bundle_data(room.room_id(), inviter).await?
129    else {
130        // No bundle received (yet).
131        info!("No room key bundle from inviter found");
132        return Ok(());
133    };
134
135    tracing::Span::current().record("bundle_sender", bundle_info.sender_user.as_str());
136
137    // Ensure that we get a fresh list of devices for the inviter, in case we need
138    // to recalculate the `SenderData`.
139    // XXX: is this necessary, given (with exclude-insecure-devices), we should have
140    // checked that the inviter device was cross-signed when we received the
141    // to-device message?
142    let (req_id, request) =
143        olm_machine.query_keys_for_users(iter::once(bundle_info.sender_user.as_ref()));
144
145    if !request.device_keys.is_empty() {
146        room.client.keys_query(&req_id, request.device_keys).await?;
147    }
148
149    let bundle_content = client
150        .media()
151        .get_media_content(
152            &MediaRequestParameters {
153                source: MediaSource::Encrypted(Box::new(bundle_info.bundle_data.file.clone())),
154                format: MediaFormat::File,
155            },
156            false,
157        )
158        .await?;
159
160    match serde_json::from_slice(&bundle_content) {
161        Ok(bundle) => {
162            olm_machine
163                .store()
164                .receive_room_key_bundle(
165                    &bundle_info,
166                    bundle,
167                    // TODO: Use the progress listener and expose an argument for it.
168                    |_, _| {},
169                )
170                .await?;
171        }
172        Err(err) => {
173            warn!("Failed to deserialize room key bundle: {err}");
174        }
175    }
176
177    // TODO: Now that we downloaded and imported the bundle, or the bundle was
178    // invalid, we can safely remove the info about the bundle.
179    // olm_machine.store().clear_received_room_key_bundle_data(room.room_id(),
180    // user_id).await?;
181
182    Ok(())
183}