tuwunel_api_client/sync/v5/extensions/
e2ee.rs1use std::collections::HashSet;
2
3use futures::{
4 FutureExt, StreamExt, TryFutureExt,
5 future::{join, join4},
6 stream::once,
7};
8use ruma::{
9 OwnedUserId, RoomId,
10 api::client::sync::sync_events::{DeviceLists, v5::response::E2EE},
11 events::{
12 StateEventType, TimelineEventType,
13 room::member::{MembershipState, RoomMemberEventContent},
14 },
15};
16use tuwunel_core::{
17 Result, error, pair_of,
18 utils::{
19 BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
20 future::{OptionFutureExt, OptionStream, ReadyBoolExt},
21 stream::BroadbandExt,
22 },
23};
24use tuwunel_matrix::{Event, pdu::PduCount};
25use tuwunel_service::sync::Connection;
26
27use super::{SyncInfo, joined_since, share_encrypted_room};
28
29#[tracing::instrument(name = "e2ee", level = "trace", skip_all)]
30pub(super) async fn collect(sync_info: SyncInfo<'_>, conn: &Connection) -> Result<(E2EE, bool)> {
31 let SyncInfo { services, sender_user, sender_device, .. } = sync_info;
32 let Some(sender_device) = sender_device else {
33 return Ok(Default::default());
34 };
35
36 let device_lists = sync_info
37 .previous_connection_pos
38 .is_some()
39 .then_async(|| device_lists(sync_info, conn))
40 .map(Option::unwrap_or_default);
41
42 let keys_changed = services
43 .users
44 .last_one_time_keys_update(sender_user)
45 .map(|since| since > conn.globalsince);
46
47 let device_one_time_keys_count = services
48 .users
49 .count_one_time_keys(sender_user, sender_device);
50
51 let device_unused_fallback_key_types = services
52 .users
53 .unused_fallback_key_algorithms(sender_user, sender_device)
54 .collect::<Vec<_>>()
55 .map(Some);
56
57 let (
58 device_lists,
59 keys_changed,
60 device_one_time_keys_count,
61 device_unused_fallback_key_types,
62 ) = join4(
63 device_lists,
64 keys_changed,
65 device_one_time_keys_count,
66 device_unused_fallback_key_types,
67 )
68 .boxed() .await;
70
71 let response = E2EE {
72 device_lists,
73 device_one_time_keys_count,
74 device_unused_fallback_key_types,
75 };
76
77 Ok((response, keys_changed))
78}
79
80#[tracing::instrument(level = "trace", skip_all)]
81async fn device_lists(sync_info: SyncInfo<'_>, conn: &Connection) -> DeviceLists {
82 let SyncInfo { services, sender_user, .. } = sync_info;
83 let keys_changed = services
84 .users
85 .keys_changed(sender_user, conn.globalsince, Some(conn.next_batch))
86 .map(ToOwned::to_owned)
87 .collect::<HashSet<_>>()
88 .map(|changed| (changed, HashSet::new()));
89
90 let (changed, left) = (HashSet::new(), HashSet::new());
91 let (changed, left) = services
92 .state_cache
93 .rooms_joined(sender_user)
94 .map(ToOwned::to_owned)
95 .broad_filter_map(async |room_id| collect_room(sync_info, conn, &room_id).await.ok())
96 .chain(once(keys_changed))
97 .ready_fold((changed, left), |(mut changed, mut left), room| {
98 changed.extend(room.0);
99 left.extend(room.1);
100 (changed, left)
101 })
102 .await;
103
104 let left = left
105 .into_iter()
106 .stream()
107 .broad_filter_map(async |user_id| {
108 share_encrypted_room(services, sender_user, &user_id)
109 .await
110 .is_false()
111 .then_some(user_id)
112 })
113 .collect()
114 .await;
115
116 DeviceLists {
117 changed: changed.into_iter().collect(),
118 left,
119 }
120}
121
122#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
123async fn collect_room(
124 SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
125 conn: &Connection,
126 room_id: &RoomId,
127) -> Result<pair_of!(HashSet<OwnedUserId>)> {
128 let current_shortstatehash = services
129 .state
130 .get_room_shortstatehash(room_id)
131 .inspect_err(|e| error!("Room {room_id} has no state: {e}"));
132
133 let room_keys_changed = services
134 .users
135 .room_keys_changed(room_id, conn.globalsince, Some(conn.next_batch))
136 .map(|(user_id, _)| user_id)
137 .map(ToOwned::to_owned)
138 .collect::<HashSet<_>>();
139
140 let (current_shortstatehash, device_list_changed) =
141 join(current_shortstatehash, room_keys_changed)
142 .boxed() .await;
144
145 let lists = (device_list_changed, HashSet::new());
146 let Ok(current_shortstatehash) = current_shortstatehash else {
147 return Ok(lists);
148 };
149
150 if current_shortstatehash <= conn.globalsince {
151 return Ok(lists);
152 }
153
154 let Ok(since_shortstatehash) = services
155 .timeline
156 .prev_shortstatehash(room_id, PduCount::Normal(conn.globalsince).saturating_add(1))
157 .await
158 else {
159 return Ok(lists);
160 };
161
162 if since_shortstatehash == current_shortstatehash {
163 return Ok(lists);
164 }
165
166 let encrypted_at = |shortstatehash| {
167 services
168 .state_accessor
169 .state_get_shortid(shortstatehash, &StateEventType::RoomEncryption, "")
170 .is_ok()
171 };
172
173 let current_encrypted = encrypted_at(current_shortstatehash).await;
174
175 if !current_encrypted
176 && services
177 .config
178 .encryption
179 .device_key_update_encrypted_rooms_only
180 {
181 return Ok(lists);
182 }
183
184 let joined_since_last_sync = joined_since(services, room_id, sender_user, conn.globalsince);
185
186 let newly_encrypted = current_encrypted
188 .then_async(|| encrypted_at(since_shortstatehash).is_false())
189 .unwrap_or_default();
190
191 let members_burst = joined_since_last_sync
193 .is_false()
194 .and(newly_encrypted.is_false())
195 .is_false()
196 .await;
197
198 let joined_members_burst = members_burst.then_async(|| {
199 services
200 .state_cache
201 .room_members(room_id)
202 .ready_filter(|&user_id| user_id != sender_user)
203 .map(ToOwned::to_owned)
204 .map(|user_id| (MembershipState::Join, user_id))
205 .boxed() .into_future()
207 });
208
209 services
210 .state_accessor
211 .state_added((since_shortstatehash, current_shortstatehash))
212 .broad_filter_map(async |(_shortstatekey, shorteventid)| {
213 services
214 .timeline
215 .get_pdu_from_shorteventid(shorteventid)
216 .ok()
217 .await
218 })
219 .ready_filter(|event| *event.kind() == TimelineEventType::RoomMember)
220 .ready_filter(|event| {
221 event
222 .state_key()
223 .is_some_and(|state_key| state_key != sender_user)
224 })
225 .ready_filter_map(|event| {
226 let content: RoomMemberEventContent = event.get_content().ok()?;
227 let user_id: OwnedUserId = event.state_key()?.parse().ok()?;
228
229 Some((content.membership, user_id))
230 })
231 .chain(joined_members_burst.stream())
232 .ready_fold(lists, |(mut changed, mut left), (membership, user_id)| {
233 use MembershipState::*;
234
235 match membership {
236 | Join => changed.insert(user_id),
237 | Leave => left.insert(user_id),
238 | _ => false,
239 };
240
241 (changed, left)
242 })
243 .map(Ok)
244 .await
245}