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.client_api_error_kind() else {
267 return Ok(());
270 };
271
272 if ErrorKind::NotFound == *err {
273 olm_machine.store().clear_room_pending_key_bundle(room.room_id()).await?;
276 }
277
278 return Ok(());
279 }
280 };
281
282 match serde_json::from_slice(&bundle_content) {
283 Ok(bundle) => {
284 olm_machine
285 .store()
286 .receive_room_key_bundle(
287 &bundle_info,
288 bundle,
289 |_, _| {},
291 )
292 .await?;
293 }
294 Err(err) => {
295 warn!("Failed to deserialize room key bundle: {err}");
296 }
297 }
298
299 olm_machine.store().clear_room_pending_key_bundle(room.room_id()).await?;
308
309 Ok(())
310}
311
312#[cfg(all(test, not(target_family = "wasm")))]
313mod test {
314 use matrix_sdk_base::crypto::store::types::RoomKeyBundleInfo;
315 use matrix_sdk_test::{
316 InvitedRoomBuilder, JoinedRoomBuilder, async_test, event_factory::EventFactory,
317 };
318 use ruma::{room_id, user_id};
319 use vodozemac::Curve25519PublicKey;
320
321 use crate::{room::shared_room_history, test_utils::mocks::MatrixMockServer};
322
323 #[async_test]
326 async fn test_should_accept_bundle() {
327 let server = MatrixMockServer::new().await;
328
329 let alice_user_id = user_id!("@alice:localhost");
330 let bob_user_id = user_id!("@bob:localhost");
331 let joined_room_id = room_id!("!joined:localhost");
332 let invited_rom_id = room_id!("!invited:localhost");
333
334 let client = server
335 .client_builder()
336 .logged_in_with_token("ABCD".to_owned(), alice_user_id.into(), "DEVICEID".into())
337 .build()
338 .await;
339
340 let event_factory = EventFactory::new().room(invited_rom_id);
341 let bob_member_event = event_factory.member(bob_user_id);
342 let alice_member_event = event_factory.member(bob_user_id).invited(alice_user_id);
343
344 server
345 .mock_sync()
346 .ok_and_run(&client, |builder| {
347 builder.add_joined_room(JoinedRoomBuilder::new(joined_room_id)).add_invited_room(
348 InvitedRoomBuilder::new(invited_rom_id)
349 .add_state_event(bob_member_event)
350 .add_state_event(alice_member_event),
351 );
352 })
353 .await;
354
355 let room =
356 client.get_room(joined_room_id).expect("We should have access to our joined room now");
357
358 assert!(
359 client
360 .base_client()
361 .get_pending_key_bundle_details_for_room(room.room_id())
362 .await
363 .unwrap()
364 .is_none(),
365 "We shouldn't have any invite acceptance details if we didn't join the room on this Client"
366 );
367
368 let bundle_info = RoomKeyBundleInfo {
369 sender: bob_user_id.to_owned(),
370 sender_key: Curve25519PublicKey::from_bytes([0u8; 32]),
371 room_id: joined_room_id.to_owned(),
372 };
373
374 assert!(
375 !shared_room_history::should_accept_key_bundle(&room, &bundle_info).await,
376 "We should not accept a bundle if we did not join the room from this Client"
377 );
378
379 let invited_room =
380 client.get_room(invited_rom_id).expect("We should have access to our invited room now");
381
382 assert!(
383 !shared_room_history::should_accept_key_bundle(&invited_room, &bundle_info).await,
384 "We should not accept a bundle if we didn't join the room."
385 );
386
387 server.mock_room_join(invited_rom_id).ok().mock_once().mount().await;
388
389 let room = client
390 .join_room_by_id(invited_rom_id)
391 .await
392 .expect("We should be able to join the invited room");
393
394 let details = client
395 .base_client()
396 .get_pending_key_bundle_details_for_room(room.room_id())
397 .await
398 .unwrap()
399 .expect("We should have stored the invite acceptance details");
400 assert_eq!(details.inviter, bob_user_id, "We should have recorded that Bob has invited us");
401
402 assert!(
403 shared_room_history::should_accept_key_bundle(&room, &bundle_info).await,
404 "We should accept a bundle if we just joined the room and did so from this very Client object"
405 );
406 }
407}