-
Notifications
You must be signed in to change notification settings - Fork 269
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
base: main
Are you sure you want to change the base?
Conversation
9a19fcf
to
9cbfbfb
Compare
Codecov ReportAttention: Patch coverage is
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. |
d83e187
to
414f03d
Compare
I swapped the Rust team reviewer to @bnjbvr since I know he has questions about including session ID in the timeline item. |
There was a problem hiding this 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.
crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs
Outdated
Show resolved
Hide resolved
@@ -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>( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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) { |
There was a problem hiding this comment.
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;
};
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this 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).
crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs
Outdated
Show resolved
Hide resolved
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 |
There was a problem hiding this comment.
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 None
s:
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in restructured version
#[serde(default)] | ||
pub session_id: String, |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in restructured version
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 { |
There was a problem hiding this comment.
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; };
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
This will allow us to use it to find messages that need their encryption info updating because the session changed.
a9331cf
to
7ea45d7
Compare
f3618bc
to
d61ceca
Compare
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. |
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 |
The duplicate messages problem was not caused by this change. I will log it separately. |
Test failure should be fixed by #4755 |
Logged here: #4758 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM otherwise
async fn get_encryption_info( | ||
&self, | ||
session_id: &str, | ||
sender: &UserId, | ||
) -> Option<EncryptionInfo> { | ||
self.get_encryption_info(session_id, sender).await | ||
} |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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?
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 | ||
} |
There was a problem hiding this comment.
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
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() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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() { |
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 theirEncryptionInfo
, in case it has improved.