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 11 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
Fixes https://github.com/element-hq/crypto-internal/issues/398

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 85.71429% with 11 lines in your changes missing coverage. Please review.

Project coverage is 86.30%. Comparing base (6c9b1ef) to head (94b7776).

Files with missing lines Patch % Lines
...es/matrix-sdk-common/src/deserialized_responses.rs 0.00% 6 Missing ⚠️
...i/src/timeline/controller/decryption_retry_task.rs 94.82% 3 Missing ⚠️
...trix-sdk-ui/src/timeline/event_item/content/mod.rs 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4644      +/-   ##
==========================================
+ Coverage   86.26%   86.30%   +0.03%     
==========================================
  Files         290      290              
  Lines       34286    34346      +60     
==========================================
+ Hits        29578    29641      +63     
+ Misses       4708     4705       -3     

☔ 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 4 times, most recently 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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Restructured in the latest version.

@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 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

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed in the latest version, but thank you - I borrowed this style for f3618bc , which is lovely.

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in restructured version

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"

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in restructured version

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; };

Copy link
Member Author

Choose a reason for hiding this comment

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

I spent quite a while on trying to make this nicer in the new version - hopefully it's better but if you have more ideas let me know.


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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, we need to separate the immutable reference to items from the mutable reference. Hopefully the new version is a little nicer.


// 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

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. This PR is going to need to be rebased on top of #4715 . Marking as draft for now.

Copy link
Member

Choose a reason for hiding this comment

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

removing my review request for now, then

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, this is now rebased on top of #4715 . I will test it today and if it works, re-ask you both for review.

@andybalaam andybalaam force-pushed the andybalaam/redecrypt-non-utds branch from a9331cf to 7ea45d7 Compare February 28, 2025 15:01
@andybalaam andybalaam force-pushed the andybalaam/redecrypt-non-utds branch from f3618bc to d61ceca Compare February 28, 2025 15:09
@andybalaam
Copy link
Member Author

I have confirmed that this fixes element-hq/element-meta#2697 but last time I tried it, it introduced a problem when testing https://github.com/element-hq/crypto-internal/issues/398 so trying that out before I ask for review.

@andybalaam
Copy link
Member Author

I'm still getting duplicate messages when I try out the testcase from https://github.com/element-hq/crypto-internal/issues/398 with this PR, so I've asked for help from the EX team

@andybalaam
Copy link
Member Author

I'm still getting duplicate messages when I try out the testcase from element-hq/crypto-internal#398 with this PR, so I've asked for help from the EX team

The duplicate messages problem was not caused by this change. I will log it separately.

@andybalaam andybalaam requested review from bnjbvr and richvdh March 5, 2025 13:24
@andybalaam andybalaam marked this pull request as ready for review March 5, 2025 13:24
@andybalaam
Copy link
Member Author

Test failure should be fixed by #4755

@andybalaam
Copy link
Member Author

The duplicate messages problem was not caused by this change. I will log it separately.

Logged here: #4758

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.

LGTM otherwise

Comment on lines +285 to +291
async fn get_encryption_info(
&self,
session_id: &str,
sender: &UserId,
) -> Option<EncryptionInfo> {
self.get_encryption_info(session_id, sender).await
}
Copy link
Member

Choose a reason for hiding this comment

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

a naive reading of this is that it is a tight recursion. I suspect I'm missing something, but a comment to explain what would help.

@@ -551,6 +551,14 @@ impl EncryptedMessage {
_ => Self::Unknown,
}
}

pub(crate) fn session_id(&self) -> Option<&str> {
Copy link
Member

Choose a reason for hiding this comment

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

could maybe do with a doc-comment?

Comment on lines +187 to +193
if let Some(session_id) = session_id {
// Should we retry this session ID?
should_retry(session_id)
} else {
// No session ID: don't retry this event
false
}
Copy link
Member

Choose a reason for hiding this comment

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

I'd probably write this as

Suggested change
if let Some(session_id) = session_id {
// Should we retry this session ID?
should_retry(session_id)
} else {
// No session ID: don't retry this event
false
}
session_id.is_some_and(should_retry)

but ymmv

send_message(&room_for_alice, "secret message").await;
bob.sync_once(SyncSettings::new()).await.expect("should not fail to sync");
let event_id = send_message(&room_for_alice, "secret message").await;
while timeline.item_by_event_id(&event_id).await.is_none() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
while timeline.item_by_event_id(&event_id).await.is_none() {
// Wait for the event to appear
while timeline.item_by_event_id(&event_id).await.is_none() {

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