Skip to main content

tuwunel_api_client/sync/v5/extensions/
e2ee.rs

1use 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() // size firewall
69	.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() // erase region
143			.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	// A plaintext room bursts only on the sender's own join.
187	let newly_encrypted = current_encrypted
188		.then_async(|| encrypted_at(since_shortstatehash).is_false())
189		.unwrap_or_default();
190
191	// The join check leads; the state lookup trails.
192	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() // Unpin for into_future
206			.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}