Skip to content

Commit 414f03d

Browse files
committed
fix(crypto): When a session updates, re-fetch encryption info for non-UTDs
Fixes element-hq/element-meta#2697
1 parent ff1dde9 commit 414f03d

File tree

4 files changed

+201
-26
lines changed

4 files changed

+201
-26
lines changed

crates/matrix-sdk-ui/src/timeline/controller/mod.rs

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
10621062
decryptor: impl Decryptor,
10631063
session_ids: Option<BTreeSet<String>>,
10641064
) {
1065-
let state = self.state.clone().write_owned().await;
1065+
let mut state = self.state.clone().write_owned().await;
10661066

10671067
// We should retry an event if its session is included in the list, or the list
10681068
// is None.
@@ -1074,11 +1074,25 @@ impl<P: RoomDataProvider> TimelineController<P> {
10741074
}
10751075
};
10761076

1077-
// Find which messages need retrying
1078-
let retry_indices = event_indices_to_retry_decryption(&state, &should_retry);
1077+
// Find which events need retrying
1078+
let (retry_decryption_indices, retry_info_indices) =
1079+
event_indices_to_retry_decryption(&state, &should_retry);
1080+
1081+
// Retry fetching encryption info for events that are already decrypted
1082+
let room_data_provider = self.room_data_provider.clone();
1083+
matrix_sdk::executor::spawn(async move {
1084+
state.retry_event_encryption_info(retry_info_indices, &room_data_provider).await;
1085+
});
10791086

1080-
// Retry decrypting
1081-
self.retry_event_decryption_by_index(state, retry_indices, should_retry, decryptor).await;
1087+
// Retry decrypting UTDs
1088+
let state = self.state.clone().write_owned().await;
1089+
self.retry_event_decryption_by_index(
1090+
state,
1091+
retry_decryption_indices,
1092+
should_retry,
1093+
decryptor,
1094+
)
1095+
.await;
10821096
}
10831097

10841098
/// Retry decryption of the supplied events, which are expected to be UTDs.
@@ -1438,24 +1452,40 @@ impl<P: RoomDataProvider> TimelineController<P> {
14381452
}
14391453
}
14401454

1455+
/// Decide which events should be retried, either for re-decryption, or, if they
1456+
/// are already decrypted, for re-checking their encryption info.
1457+
///
1458+
/// Returns a tuple `(retry_decryption_indices, retry_info_indices)` where
1459+
/// `retry_decryption_indices` is a list of UTDs to try decrypting, and
1460+
/// retry_info_indices is a list of already-decrypted events whose encryption
1461+
/// info we can re-fetch.
14411462
fn event_indices_to_retry_decryption(
14421463
state: &tokio::sync::OwnedRwLockWriteGuard<TimelineState>,
14431464
should_retry: impl Fn(&str) -> bool,
1444-
) -> Vec<usize> {
1445-
let retry_indices: Vec<_> = state
1446-
.items
1447-
.iter()
1448-
.enumerate()
1449-
.filter_map(|(idx, item)| match item.as_event()?.content().as_unable_to_decrypt()? {
1450-
EncryptedMessage::MegolmV1AesSha2 { session_id, .. } if should_retry(session_id) => {
1451-
Some(idx)
1465+
) -> (Vec<usize>, Vec<usize>) {
1466+
let mut retry_decryption_indices = Vec::new();
1467+
let mut retry_info_indices = Vec::new();
1468+
1469+
for (idx, item) in state.items.iter().enumerate() {
1470+
if let Some(event) = item.as_event() {
1471+
if let Some(remote_event) = event.as_remote() {
1472+
if let Some(session_id) = &remote_event.session_id {
1473+
if should_retry(session_id) {
1474+
match event.content() {
1475+
TimelineItemContent::UnableToDecrypt(_) => {
1476+
retry_decryption_indices.push(idx);
1477+
}
1478+
_ => {
1479+
retry_info_indices.push(idx);
1480+
}
1481+
}
1482+
}
1483+
}
14521484
}
1453-
EncryptedMessage::MegolmV1AesSha2 { .. }
1454-
| EncryptedMessage::OlmV1Curve25519AesSha2 { .. }
1455-
| EncryptedMessage::Unknown => None,
1456-
})
1457-
.collect();
1458-
retry_indices
1485+
}
1486+
}
1487+
1488+
(retry_decryption_indices, retry_info_indices)
14591489
}
14601490

14611491
impl TimelineController {

crates/matrix-sdk-ui/src/timeline/controller/state.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ use super::{
4646
DateDividerMode, TimelineFocusKind, TimelineMetadata, TimelineSettings,
4747
TimelineStateTransaction,
4848
};
49-
use crate::unable_to_decrypt_hook::UtdHookManager;
49+
use crate::{
50+
timeline::{event_item::EventTimelineItemKind, TimelineItemKind},
51+
unable_to_decrypt_hook::UtdHookManager,
52+
};
5053

5154
#[derive(Debug)]
5255
pub(in crate::timeline) struct TimelineState {
@@ -225,6 +228,45 @@ impl TimelineState {
225228
txn.commit();
226229
}
227230

231+
/// Try to fetch [`EncryptionInfo`] for the events with the supplied
232+
/// indices, and update them where we succeed.
233+
pub(super) async fn retry_event_encryption_info<P: RoomDataProvider>(
234+
&mut self,
235+
retry_indices: Vec<usize>,
236+
room_data_provider: &P,
237+
) {
238+
let mut new_info = Vec::new();
239+
for idx in retry_indices {
240+
let item = self.items.get(idx);
241+
if let Some(item) = item {
242+
if let Some(event) = item.as_event() {
243+
let sender = event.sender.clone();
244+
if let Some(remote) = event.as_remote() {
245+
if let Some(session_id) = &remote.session_id {
246+
let mut new_remote = remote.clone();
247+
248+
new_remote.encryption_info =
249+
room_data_provider.get_encryption_info(session_id, &sender).await;
250+
251+
let new_event =
252+
event.with_kind(EventTimelineItemKind::Remote(new_remote));
253+
254+
let new_item = item.with_kind(TimelineItemKind::Event(new_event));
255+
256+
new_info.push((idx, new_item));
257+
}
258+
}
259+
}
260+
}
261+
}
262+
263+
let mut txn = self.transaction();
264+
for (idx, item) in new_info {
265+
txn.replace(idx, item);
266+
}
267+
txn.commit();
268+
}
269+
228270
#[cfg(test)]
229271
pub(super) fn handle_read_receipts(
230272
&mut self,

crates/matrix-sdk-ui/src/timeline/tests/encryption.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ use eyeball_im::VectorDiff;
2828
use matrix_sdk::{
2929
assert_next_matches_with_timeout,
3030
crypto::{decrypt_room_key_export, types::events::UtdCause, OlmMachine},
31+
deserialized_responses::{
32+
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, VerificationLevel, VerificationState,
33+
},
3134
test_utils::test_client_builder,
3235
};
3336
use matrix_sdk_base::deserialized_responses::{TimelineEvent, UnableToDecryptReason};
@@ -38,18 +41,19 @@ use ruma::{
3841
EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement,
3942
RoomEncryptedEventContent,
4043
},
41-
room_id,
44+
owned_device_id, room_id,
4245
serde::Raw,
4346
user_id,
4447
};
4548
use serde_json::{json, value::to_raw_value};
46-
use stream_assert::assert_next_matches;
49+
use stream_assert::{assert_next_matches, assert_pending};
4750
use tokio::time::sleep;
4851

4952
use super::TestTimeline;
5053
use crate::{
5154
timeline::{
52-
tests::TestTimelineBuilder, EncryptedMessage, TimelineDetails, TimelineItemContent,
55+
tests::{TestRoomDataProvider, TestTimelineBuilder},
56+
EncryptedMessage, TimelineDetails, TimelineItemContent,
5357
},
5458
unable_to_decrypt_hook::{UnableToDecryptHook, UnableToDecryptInfo, UtdHookManager},
5559
};
@@ -550,6 +554,94 @@ async fn test_retry_message_decryption_highlighted() {
550554
assert!(event.is_highlighted());
551555
}
552556

557+
#[async_test]
558+
async fn test_retry_fetching_encryption_info() {
559+
const SESSION_ID: &str = "C25PoE+4MlNidQD0YU5ibZqHawV0zZ/up7R8vYJBYTY";
560+
let sender = user_id!("@sender:s.co");
561+
let room_id = room_id!("!room:s.co");
562+
563+
// Given when I ask the room for new encryption info for any session, it will
564+
// say "verified"
565+
let verified_encryption_info = make_encryption_info(SESSION_ID, VerificationState::Verified);
566+
let provider = TestRoomDataProvider::default().with_encryption_info(verified_encryption_info);
567+
let timeline = TestTimelineBuilder::new().provider(provider).build();
568+
let f = &timeline.factory;
569+
let mut stream = timeline.subscribe().await;
570+
571+
// But right now the timeline contains 2 events whose info says "unverified"
572+
// One is linked to SESSION_ID, the other is linked to some other session.
573+
let timeline_event_this_session: TimelineEvent = DecryptedRoomEvent {
574+
event: f.text_msg("foo").sender(sender).room(room_id).into_raw(),
575+
encryption_info: make_encryption_info(
576+
SESSION_ID,
577+
VerificationState::Unverified(VerificationLevel::UnsignedDevice),
578+
),
579+
unsigned_encryption_info: None,
580+
}
581+
.into();
582+
let timeline_event_other_session: TimelineEvent = DecryptedRoomEvent {
583+
event: f.text_msg("foo").sender(sender).room(room_id).into_raw(),
584+
encryption_info: make_encryption_info(
585+
"other_session_id",
586+
VerificationState::Unverified(VerificationLevel::UnsignedDevice),
587+
),
588+
unsigned_encryption_info: None,
589+
}
590+
.into();
591+
timeline.handle_live_event(timeline_event_this_session).await;
592+
timeline.handle_live_event(timeline_event_other_session).await;
593+
594+
// Sanity: the events come through as unverified
595+
assert_eq!(timeline.controller.items().await.len(), 3);
596+
let item_1 = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
597+
let fetched_encryption_info_1 =
598+
item_1.as_event().unwrap().as_remote().unwrap().encryption_info.as_ref().unwrap();
599+
assert_matches!(fetched_encryption_info_1.verification_state, VerificationState::Unverified(_));
600+
// (Plus a date divider is emitted - not sure why it's in the middle...)
601+
let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value);
602+
assert!(date_divider.is_date_divider());
603+
let item_2 = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
604+
let fetched_encryption_info_2 =
605+
item_2.as_event().unwrap().as_remote().unwrap().encryption_info.as_ref().unwrap();
606+
assert_matches!(fetched_encryption_info_2.verification_state, VerificationState::Unverified(_));
607+
608+
// When we retry the session with ID SESSION_ID
609+
let own_user_id = user_id!("@me:s.co");
610+
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
611+
timeline
612+
.controller
613+
.retry_event_decryption_test(
614+
room_id,
615+
olm_machine,
616+
Some(iter::once(SESSION_ID.to_owned()).collect()),
617+
)
618+
.await;
619+
620+
// Then the event in that session has been updated to be verified
621+
assert_eq!(timeline.controller.items().await.len(), 3);
622+
let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value);
623+
let event = item.as_event().unwrap();
624+
let fetched_encryption_info = event.as_remote().unwrap().encryption_info.as_ref().unwrap();
625+
assert_matches!(fetched_encryption_info.verification_state, VerificationState::Verified);
626+
627+
// But the other one is unchanged because it was for a different session - no
628+
// other updates are waiting
629+
assert_pending!(stream);
630+
}
631+
632+
fn make_encryption_info(session_id: &str, verification_state: VerificationState) -> EncryptionInfo {
633+
EncryptionInfo {
634+
sender: BOB.to_owned(),
635+
sender_device: Some(owned_device_id!("BOBDEVICE")),
636+
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
637+
curve25519_key: Default::default(),
638+
sender_claimed_keys: Default::default(),
639+
},
640+
verification_state,
641+
session_id: session_id.to_owned(),
642+
}
643+
}
644+
553645
#[async_test]
554646
async fn test_utd_cause_for_nonmember_event_is_found() {
555647
// Given a timline

crates/matrix-sdk-ui/src/timeline/tests/mod.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use imbl::vector;
2828
use indexmap::IndexMap;
2929
use matrix_sdk::{
3030
config::RequestConfig,
31-
deserialized_responses::TimelineEvent,
31+
deserialized_responses::{EncryptionInfo, TimelineEvent},
3232
event_cache::paginator::{PaginableRoom, PaginatorError},
3333
room::{EventWithContextResponse, Messages, MessagesOptions},
3434
send_queue::RoomSendQueueUpdate,
@@ -267,6 +267,9 @@ struct TestRoomDataProvider {
267267

268268
/// Events redacted with that room data providier.
269269
pub redacted: Arc<RwLock<Vec<OwnedEventId>>>,
270+
271+
/// Returned from get_encryption_info method
272+
pub encryption_info: Option<EncryptionInfo>,
270273
}
271274

272275
impl TestRoomDataProvider {
@@ -278,6 +281,14 @@ impl TestRoomDataProvider {
278281
self.fully_read_marker = Some(event_id);
279282
self
280283
}
284+
285+
pub(crate) fn with_encryption_info(
286+
mut self,
287+
encryption_info: EncryptionInfo,
288+
) -> TestRoomDataProvider {
289+
self.encryption_info = Some(encryption_info);
290+
self
291+
}
281292
}
282293

283294
impl PaginableRoom for TestRoomDataProvider {
@@ -419,7 +430,7 @@ impl RoomDataProvider for TestRoomDataProvider {
419430
&self,
420431
_session_id: &str,
421432
_sender: &UserId,
422-
) -> Option<matrix_sdk::deserialized_responses::EncryptionInfo> {
423-
None
433+
) -> Option<EncryptionInfo> {
434+
self.encryption_info.clone()
424435
}
425436
}

0 commit comments

Comments
 (0)