matrix_sdk/room/
shared_room_history.rs1use std::{collections::HashSet, iter, time::Duration};
16
17use matrix_sdk_base::{
18 RoomState,
19 crypto::{
20 store::types::{Changes, RoomKeyBundleInfo, RoomPendingKeyBundleDetails},
21 types::events::room_key_bundle::RoomKeyBundleContent,
22 },
23 media::{MediaFormat, MediaRequestParameters},
24};
25use ruma::{
26 OwnedUserId, UserId,
27 api::error::ErrorKind,
28 events::room::{MediaSource, history_visibility::HistoryVisibility},
29};
30use tracing::{debug, info, instrument, warn};
31
32use crate::{Error, Result, Room};
33
34#[instrument(skip(room), fields(room_id = ?room.room_id()))]
39pub(super) async fn share_room_history(room: &Room, user_id: OwnedUserId) -> Result<()> {
40 let client = &room.client;
41
42 let own_identity = match client.user_id() {
44 Some(own_user) => client.encryption().get_user_identity(own_user).await?,
45 None => None,
46 };
47
48 if own_identity.is_none() {
49 warn!("Not sharing message history as cross-signing is not set up");
50 return Ok(());
51 }
52
53 if matches!(
57 room.history_visibility_or_default(),
58 HistoryVisibility::Joined | HistoryVisibility::Invited
59 ) {
60 debug!("Not sharing message history as the room history visibility is currently unshared");
61 return Ok(());
62 }
63
64 info!("Sharing message history");
65
66 let olm_machine = client.olm_machine().await;
67 let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
68
69 if !olm_machine.store().has_downloaded_all_room_keys(room.room_id()).await? {
71 debug!("Downloading room keys for room");
72 client.encryption().backups().download_room_keys_for_room(room.room_id()).await?;
73 olm_machine
74 .store()
75 .save_changes(Changes {
76 room_key_backups_fully_downloaded: HashSet::from_iter([room.room_id().to_owned()]),
77 ..Default::default()
78 })
79 .await?;
80 }
81
82 let bundle = olm_machine.store().build_room_key_bundle(room.room_id()).await?;
84
85 if bundle.is_empty() {
86 info!("No keys to share");
87 return Ok(());
88 }
89
90 let json = serde_json::to_vec(&bundle)?;
92 let upload = client.upload_encrypted_file(&mut (json.as_slice())).await?;
93
94 info!(
95 media_url = ?upload.url,
96 shared_keys = bundle.room_keys.len(),
97 withheld_keys = bundle.withheld.len(),
98 "Uploaded encrypted key blob"
99 );
100
101 let (req_id, request) = olm_machine.query_keys_for_users(iter::once(user_id.as_ref()));
103
104 if !request.device_keys.is_empty() {
105 room.client.keys_query(&req_id, request.device_keys).await?;
106 }
107
108 client.claim_one_time_keys(iter::once(user_id.as_ref())).await?;
110
111 let content = RoomKeyBundleContent { room_id: room.room_id().to_owned(), file: upload };
113 let requests = {
114 let olm_machine = client.olm_machine().await;
115 let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
116 olm_machine
117 .share_room_key_bundle_data(
118 &user_id,
119 &client.base_client().room_key_recipient_strategy,
120 content,
121 )
122 .await?
123 };
124
125 for request in requests {
126 let response = client.send_to_device(&request).await?;
127 client.mark_request_as_sent(&request.txn_id, &response).await?;
128 }
129
130 Ok(())
131}
132
133pub(crate) async fn should_accept_key_bundle(room: &Room, bundle_info: &RoomKeyBundleInfo) -> bool {
149 let Ok(Some(details)) =
152 room.client.base_client().get_pending_key_bundle_details_for_room(room.room_id()).await
153 else {
154 debug!("Not accepting key bundle as there are no recorded invite acceptance details");
155 return false;
156 };
157
158 if !should_process_room_pending_key_bundle_details(&details) {
159 return false;
160 }
161
162 let state = room.state();
163 let bundle_sender = &bundle_info.sender;
164
165 match state {
166 RoomState::Joined => bundle_sender == &details.inviter,
167 RoomState::Left | RoomState::Invited | RoomState::Knocked | RoomState::Banned => false,
168 }
169}
170
171pub(crate) fn should_process_room_pending_key_bundle_details(
189 details: &RoomPendingKeyBundleDetails,
190) -> bool {
191 const DAY: Duration = Duration::from_secs(24 * 60 * 60);
194
195 details
196 .invite_accepted_at
197 .to_system_time()
198 .and_then(|t| t.elapsed().ok())
199 .map(|elapsed_since_join| elapsed_since_join < DAY)
200 .unwrap_or(false)
201}
202
203#[instrument(skip(room), fields(room_id = ?room.room_id(), bundle_sender))]
217pub(crate) async fn maybe_accept_key_bundle(room: &Room, inviter: &UserId) -> Result<()> {
218 let client = &room.client;
222 let olm_machine = client.olm_machine().await;
223
224 let Some(olm_machine) = olm_machine.as_ref() else {
225 warn!("Not fetching room key bundle as the Olm machine is not available");
226 return Ok(());
227 };
228
229 let Some(bundle_info) =
230 olm_machine.store().get_received_room_key_bundle_data(room.room_id(), inviter).await?
231 else {
232 info!("No room key bundle from inviter found");
234 return Ok(());
235 };
236
237 tracing::Span::current().record("bundle_sender", bundle_info.sender_user.as_str());
238
239 let (req_id, request) =
245 olm_machine.query_keys_for_users(iter::once(bundle_info.sender_user.as_ref()));
246
247 if !request.device_keys.is_empty() {
248 room.client.keys_query(&req_id, request.device_keys).await?;
249 }
250
251 let bundle_content = match client
252 .media()
253 .get_media_content(
254 &MediaRequestParameters {
255 source: MediaSource::Encrypted(Box::new(bundle_info.bundle_data.file.clone())),
256 format: MediaFormat::File,
257 },
258 false,
259 )
260 .await
261 {
262 Ok(bundle_content) => bundle_content,
263 Err(err) => {
264 let Some(err) = err
267 .as_ruma_api_error()
268 .and_then(|e| e.as_client_api_error())
269 .and_then(|e| e.error_kind())
270 else {
271 return Ok(());
274 };
275
276 if ErrorKind::NotFound == *err {
277 olm_machine.store().clear_room_pending_key_bundle(room.room_id()).await?;
280 }
281
282 return Ok(());
283 }
284 };
285
286 match serde_json::from_slice(&bundle_content) {
287 Ok(bundle) => {
288 olm_machine
289 .store()
290 .receive_room_key_bundle(
291 &bundle_info,
292 bundle,
293 |_, _| {},
295 )
296 .await?;
297 }
298 Err(err) => {
299 warn!("Failed to deserialize room key bundle: {err}");
300 }
301 }
302
303 olm_machine.store().clear_room_pending_key_bundle(room.room_id()).await?;
312
313 Ok(())
314}
315
316#[cfg(all(test, not(target_family = "wasm")))]
317mod test {
318 use matrix_sdk_base::crypto::store::types::RoomKeyBundleInfo;
319 use matrix_sdk_test::{
320 InvitedRoomBuilder, JoinedRoomBuilder, async_test, event_factory::EventFactory,
321 };
322 use ruma::{room_id, user_id};
323 use vodozemac::Curve25519PublicKey;
324
325 use crate::{room::shared_room_history, test_utils::mocks::MatrixMockServer};
326
327 #[async_test]
330 async fn test_should_accept_bundle() {
331 let server = MatrixMockServer::new().await;
332
333 let alice_user_id = user_id!("@alice:localhost");
334 let bob_user_id = user_id!("@bob:localhost");
335 let joined_room_id = room_id!("!joined:localhost");
336 let invited_rom_id = room_id!("!invited:localhost");
337
338 let client = server
339 .client_builder()
340 .logged_in_with_token("ABCD".to_owned(), alice_user_id.into(), "DEVICEID".into())
341 .build()
342 .await;
343
344 let event_factory = EventFactory::new().room(invited_rom_id);
345 let bob_member_event = event_factory.member(bob_user_id);
346 let alice_member_event = event_factory.member(bob_user_id).invited(alice_user_id);
347
348 server
349 .mock_sync()
350 .ok_and_run(&client, |builder| {
351 builder.add_joined_room(JoinedRoomBuilder::new(joined_room_id)).add_invited_room(
352 InvitedRoomBuilder::new(invited_rom_id)
353 .add_state_event(bob_member_event)
354 .add_state_event(alice_member_event),
355 );
356 })
357 .await;
358
359 let room =
360 client.get_room(joined_room_id).expect("We should have access to our joined room now");
361
362 assert!(
363 client
364 .base_client()
365 .get_pending_key_bundle_details_for_room(room.room_id())
366 .await
367 .unwrap()
368 .is_none(),
369 "We shouldn't have any invite acceptance details if we didn't join the room on this Client"
370 );
371
372 let bundle_info = RoomKeyBundleInfo {
373 sender: bob_user_id.to_owned(),
374 sender_key: Curve25519PublicKey::from_bytes([0u8; 32]),
375 room_id: joined_room_id.to_owned(),
376 };
377
378 assert!(
379 !shared_room_history::should_accept_key_bundle(&room, &bundle_info).await,
380 "We should not accept a bundle if we did not join the room from this Client"
381 );
382
383 let invited_room =
384 client.get_room(invited_rom_id).expect("We should have access to our invited room now");
385
386 assert!(
387 !shared_room_history::should_accept_key_bundle(&invited_room, &bundle_info).await,
388 "We should not accept a bundle if we didn't join the room."
389 );
390
391 server.mock_room_join(invited_rom_id).ok().mock_once().mount().await;
392
393 let room = client
394 .join_room_by_id(invited_rom_id)
395 .await
396 .expect("We should be able to join the invited room");
397
398 let details = client
399 .base_client()
400 .get_pending_key_bundle_details_for_room(room.room_id())
401 .await
402 .unwrap()
403 .expect("We should have stored the invite acceptance details");
404 assert_eq!(details.inviter, bob_user_id, "We should have recorded that Bob has invited us");
405
406 assert!(
407 shared_room_history::should_accept_key_bundle(&room, &bundle_info).await,
408 "We should accept a bundle if we just joined the room and did so from this very Client object"
409 );
410 }
411}