Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(crypto): Redecrypt non-UTD messages to remove no-longer-relevant warning shields #4644

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

andybalaam
Copy link
Member

@andybalaam andybalaam commented Feb 7, 2025

Fixes element-hq/element-meta#2697

I'm sorry it's a big change. I've tried to break it into decent commits, and I did a couple of preparatory PRs to make it less painful, but it's still a bit to get your head around.

The basic idea is that when a session is updated and we call retry_event_decryption, we don't only look at UTDs any more - now we also look at decrypted events, and re-request their EncryptionInfo, in case it has improved.

@andybalaam andybalaam changed the title refactor(crypto): Store the session ID of a UTD in RemoteEventTimelineItem fix(crypto): Redecrypt non-UTD messages to remove no-longer-relevant warning shields Feb 7, 2025
@andybalaam andybalaam force-pushed the andybalaam/redecrypt-non-utds branch from 9a19fcf to 9cbfbfb Compare February 7, 2025 14:17
Copy link

codecov bot commented Feb 7, 2025

Codecov Report

Attention: Patch coverage is 75.71429% with 17 lines in your changes missing coverage. Please review.

Project coverage is 85.81%. Comparing base (9de6d28) to head (a9331cf).
Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
...es/matrix-sdk-common/src/deserialized_responses.rs 0.00% 6 Missing ⚠️
crates/matrix-sdk/src/room/mod.rs 0.00% 4 Missing ⚠️
...rates/matrix-sdk-ui/src/timeline/controller/mod.rs 92.85% 2 Missing ⚠️
...trix-sdk-ui/src/timeline/event_item/content/mod.rs 60.00% 2 Missing ⚠️
crates/matrix-sdk-ui/src/timeline/traits.rs 0.00% 2 Missing ⚠️
...tes/matrix-sdk-ui/src/timeline/controller/state.rs 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4644      +/-   ##
==========================================
- Coverage   85.82%   85.81%   -0.01%     
==========================================
  Files         292      292              
  Lines       33761    33822      +61     
==========================================
+ Hits        28974    29024      +50     
- Misses       4787     4798      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@andybalaam andybalaam force-pushed the andybalaam/redecrypt-non-utds branch 3 times, most recently from 741aa40 to d83e187 Compare February 14, 2025 11:45
…imelineItem

... meaning we attempt to redecrypt them even if they are not UTDs.
The redecryption doesn't actually work - that will be fixed in the next
commit.
…etry decryption

In later commits, we will make this cleverer, so it can calculate which
non-UTD events need updating too.
Future commits will add code to try non-UTDs too, in another function.
@andybalaam andybalaam force-pushed the andybalaam/redecrypt-non-utds branch from d83e187 to 414f03d Compare February 14, 2025 11:59
@andybalaam andybalaam marked this pull request as ready for review February 14, 2025 12:11
@andybalaam andybalaam requested review from a team as code owners February 14, 2025 12:11
@andybalaam andybalaam requested review from stefanceriu, richvdh and bnjbvr and removed request for a team and stefanceriu February 14, 2025 12:11
@andybalaam
Copy link
Member Author

I swapped the Rust team reviewer to @bnjbvr since I know he has questions about including session ID in the timeline item.

Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems generally good to me. A few comments.

@@ -225,6 +228,45 @@ impl TimelineState {
txn.commit();
}

/// Try to fetch [`EncryptionInfo`] for the events with the supplied
/// indices, and update them where we succeed.
pub(super) async fn retry_event_encryption_info<P: RoomDataProvider>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to say that having this here but retry_event_decryption_by_index in the other place feels rather asymmetric. Is there a reason they need to be different?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind, TimelineState::retry_event_encryption_info is a partner to TimelineState::retry_event_decryption, not to TimelineController::retry_event_decryption_by_index. retry_event_decryption_by_index is needed because we need to construct a complex retry_one closure to pass to retry_event_decryption whereas retry_event_encryption_info can do its work directly.

@andybalaam andybalaam marked this pull request as draft February 17, 2025 12:26
@bnjbvr bnjbvr removed their request for review February 17, 2025 13:05
Comment on lines 1470 to 1473
if let Some(event) = item.as_event() {
if let Some(remote_event) = event.as_remote() {
if let Some(session_id) = &remote_event.session_id {
if should_retry(session_id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uh, that's a lot of indent levels 😅

You could use .and_then on all the Option to chain them, as well as early continue when you're seeing something you're not expecting:

let Some(thing) = item.as_event()
  .and_then(|event| event.as_remote())
  .map(/* or and_then, not sure what session_id is */|remote_event| *remote_event.session_id) else {
continue;
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the changes I've already made, this is a little flatter, and I don't think there is a nice way to make it better?

On similar lines, I had a look at TimelineState::retry_event_encryption_info which has a similar problem, but I couldn't see a way to make that better because it has an async call all the way over to the right. Suggestions welcome.

@andybalaam andybalaam requested review from bnjbvr and richvdh February 17, 2025 15:16
@andybalaam andybalaam marked this pull request as ready for review February 17, 2025 15:16
@andybalaam
Copy link
Member Author

This is ready for re-review. I think I would recommend looking at the full diff now, since the review changes reverted some of what I did in earlier commits. I will rebase into nice commits after review.

Copy link
Member

@bnjbvr bnjbvr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few stylistic change requests below. On the one hand, thanks for not storing any extra data in the TimelineItemContent itself, on the other… you know my opinion about adding more code to the timeline re-decryption. This all should be moved somewhere else, and tested in isolation there (could be in the crypto crate, the main SDK crate, inside or outside the event cache,… any place other than the timeline would make more sense).

Comment on lines +446 to +453
/// Replace the timeline item at the supplied index with the supplied item.
pub(super) fn replace(
&mut self,
timeline_item_index: usize,
timeline_item: Arc<TimelineItem>,
) -> Arc<TimelineItem> {
self.items.replace(timeline_item_index, timeline_item)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function useful? I would have assumed that there are many other places in the code that already replaces items from a transaction, and that they didn't need this function to do this… (Admittedly, the blackboxing of the TimelineStateTransaction is a bit compromised already at this point.)

Comment on lines +1478 to +1493
if let Some(event) = item.as_event() {
if let TimelineItemContent::UnableToDecrypt(encrypted) = event.content() {
if let Some(session_id) = encrypted.session_id() {
if should_retry(session_id) {
return Some(Either::Left(idx));
}
}
} else if let Some(remote_event) = event.as_remote() {
if let Some(encryption_info) = &remote_event.encryption_info {
if should_retry(&encryption_info.session_id) {
return Some(Either::Right(idx));
}
}
}
}
None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style nit: this could be much less indented if you shortcut the Nones:

Suggested change
if let Some(event) = item.as_event() {
if let TimelineItemContent::UnableToDecrypt(encrypted) = event.content() {
if let Some(session_id) = encrypted.session_id() {
if should_retry(session_id) {
return Some(Either::Left(idx));
}
}
} else if let Some(remote_event) = event.as_remote() {
if let Some(encryption_info) = &remote_event.encryption_info {
if should_retry(&encryption_info.session_id) {
return Some(Either::Right(idx));
}
}
}
}
None
let event = item.as_event()?;
if let TimelineItemContent::UnableToDecrypt(encrypted) = event.content() {
let session_id = encrypted.session_id()?;
if should_retry(session_id) {
return Some(Either::Left(idx));
}
} else {
let remote_event = event.as_remote()?;
let encryption_info = remote_event.encryption_info.as_ref()?;
if should_retry(&encryption_info.session_id) {
return Some(Either::Right(idx));
}
}
None

if retry_indices.is_empty() {
return;
}

debug!("Retrying decryption");

let mut state = state.clone().write_owned().await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to have the caller pass the owned lock here; that shows that the lock will be taken, at the call sites.

Comment on lines +306 to +307
#[serde(default)]
pub session_id: String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be an Option instead of a string that defaults to "" when the data was reloaded from the store, and we didn't have it? Seems dangerous to have this floating knowledge that "" means "didn't have this information in the store"

Comment on lines +241 to +245
if let Some(item) = item {
if let Some(event) = item.as_event() {
let sender = event.sender.clone();
if let Some(remote) = event.as_remote() {
if let Some(encryption_info) = &remote.encryption_info {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many indent levels. Please use either a closure that returns an option, or short-circuiting with let Some(thing) = ... else { continue; };

Comment on lines +264 to +268
let mut txn = self.transaction();
for (idx, item) in new_info {
txn.replace(idx, item);
}
txn.commit();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need a transaction if no operations between the start of the transaction and the end aren't fallible.


let new_item = item.with_kind(TimelineItemKind::Event(new_event));

new_info.push((idx, new_item));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you replace the item here, instead of storing it temporarily? Or can't because iterating on the items themselves?


// Retry fetching encryption info for events that are already decrypted
let room_data_provider = self.room_data_provider.clone();
matrix_sdk::executor::spawn(async move {
Copy link
Member Author

@andybalaam andybalaam Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From @bnjbvr

anyhow, one change that should happen now, and unnoted in this review because D4mir noticed it afterwards, was that this is spawning a task that's detached; it should be attached to the timeline, one way or another:

either as a one-off, making sure there's only one at any time
or a task that listens to a channel of commands, asking for retrying decryption of a set of tasks, and running those requests linearly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

EX: Messages sent after an identity reset can sometimes be flagged as sent from insecure device
3 participants