Skip to main content

tuwunel_api/client/
backup.rs

1use std::cmp::Ordering;
2
3use axum::extract::State;
4use futures::{FutureExt, future::try_join};
5use ruma::{
6	UInt, UserId,
7	api::client::backup::{
8		add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,
9		create_backup_version, delete_backup_keys, delete_backup_keys_for_room,
10		delete_backup_keys_for_session, delete_backup_version, get_backup_info, get_backup_keys,
11		get_backup_keys_for_room, get_backup_keys_for_session, get_latest_backup_info,
12		update_backup_version,
13	},
14	serde::Raw,
15};
16use serde::Deserialize;
17use serde_json::value::RawValue as RawJsonValue;
18use tuwunel_core::{Err, Result, err};
19use tuwunel_service::Services;
20
21use crate::Ruma;
22
23/// Overrides ruma's internal `AlgorithmWithData` shape required by the GET
24/// `/room_keys/version[/{version}]` response serializer. Validating against
25/// this will not raise a serialization error (HTTP 500) when responding.
26#[derive(Deserialize)]
27#[expect(unused)]
28struct AlgorithmShape {
29	algorithm: Box<RawJsonValue>,
30	auth_data: Box<RawJsonValue>,
31}
32
33fn validate_algorithm_shape<T>(raw: &Raw<T>) -> Result {
34	raw.deserialize_as_unchecked::<AlgorithmShape>()
35		.map_err(Into::into)
36		.map(drop)
37}
38
39/// # `POST /_matrix/client/r0/room_keys/version`
40///
41/// Creates a new backup.
42pub(crate) async fn create_backup_version_route(
43	State(services): State<crate::State>,
44	body: Ruma<create_backup_version::v3::Request>,
45) -> Result<create_backup_version::v3::Response> {
46	validate_algorithm_shape(&body.algorithm)
47		.map_err(|e| err!(Request(BadJson("Invalid backup metadata: {e}"))))?;
48
49	let version = services
50		.key_backups
51		.create_backup(body.sender_user(), &body.algorithm)?;
52
53	Ok(create_backup_version::v3::Response { version })
54}
55
56/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
57///
58/// Update information about an existing backup. Only `auth_data` can be
59/// modified.
60pub(crate) async fn update_backup_version_route(
61	State(services): State<crate::State>,
62	body: Ruma<update_backup_version::v3::Request>,
63) -> Result<update_backup_version::v3::Response> {
64	validate_algorithm_shape(&body.algorithm)
65		.map_err(|e| err!(Request(BadJson("Invalid backup metadata: {e}"))))?;
66
67	services
68		.key_backups
69		.update_backup(body.sender_user(), &body.version, &body.algorithm)
70		.await?;
71
72	Ok(update_backup_version::v3::Response {})
73}
74
75/// # `GET /_matrix/client/r0/room_keys/version`
76///
77/// Get information about the latest backup version.
78pub(crate) async fn get_latest_backup_info_route(
79	State(services): State<crate::State>,
80	body: Ruma<get_latest_backup_info::v3::Request>,
81) -> Result<get_latest_backup_info::v3::Response> {
82	let (version, algorithm) = services
83		.key_backups
84		.get_latest_backup(body.sender_user())
85		.await
86		.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
87
88	validate_algorithm_shape(&algorithm)
89		.map_err(|e| err!(Request(NotFound("Key backup does not exist: {e}"))))?;
90
91	let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await?;
92
93	Ok(get_latest_backup_info::v3::Response { algorithm, count, etag, version })
94}
95
96/// # `GET /_matrix/client/v3/room_keys/version/{version}`
97///
98/// Get information about an existing backup.
99pub(crate) async fn get_backup_info_route(
100	State(services): State<crate::State>,
101	body: Ruma<get_backup_info::v3::Request>,
102) -> Result<get_backup_info::v3::Response> {
103	let algorithm = services
104		.key_backups
105		.get_backup(body.sender_user(), &body.version)
106		.await
107		.map_err(|_| {
108			err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
109		})?;
110
111	validate_algorithm_shape(&algorithm).map_err(|e| {
112		err!(Request(NotFound(
113			"Key backup does not exist at version {:?}: {e}",
114			body.version
115		)))
116	})?;
117
118	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
119
120	Ok(get_backup_info::v3::Response {
121		algorithm,
122		count,
123		etag,
124		version: body.version.clone(),
125	})
126}
127
128/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
129///
130/// Delete an existing key backup.
131///
132/// - Deletes both information about the backup, as well as all key data related
133///   to the backup
134pub(crate) async fn delete_backup_version_route(
135	State(services): State<crate::State>,
136	body: Ruma<delete_backup_version::v3::Request>,
137) -> Result<delete_backup_version::v3::Response> {
138	services
139		.key_backups
140		.delete_backup(body.sender_user(), &body.version)
141		.await;
142
143	Ok(delete_backup_version::v3::Response {})
144}
145
146/// # `PUT /_matrix/client/r0/room_keys/keys`
147///
148/// Add the received backup keys to the database.
149///
150/// - Only manipulating the most recently created version of the backup is
151///   allowed
152/// - Adds the keys to the backup
153/// - Returns the new number of keys in this backup and the etag
154pub(crate) async fn add_backup_keys_route(
155	State(services): State<crate::State>,
156	body: Ruma<add_backup_keys::v3::Request>,
157) -> Result<add_backup_keys::v3::Response> {
158	if services
159		.key_backups
160		.get_latest_backup_version(body.sender_user())
161		.await
162		.is_ok_and(|version| version != body.version)
163	{
164		return Err!(Request(InvalidParam(
165			"You may only manipulate the most recently created version of the backup."
166		)));
167	}
168
169	for (room_id, room) in &body.rooms {
170		for (session_id, key_data) in &room.sessions {
171			services
172				.key_backups
173				.add_key(body.sender_user(), &body.version, room_id, session_id, key_data)
174				.await?;
175		}
176	}
177
178	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
179
180	Ok(add_backup_keys::v3::Response { count, etag })
181}
182
183/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
184///
185/// Add the received backup keys to the database.
186///
187/// - Only manipulating the most recently created version of the backup is
188///   allowed
189/// - Adds the keys to the backup
190/// - Returns the new number of keys in this backup and the etag
191pub(crate) async fn add_backup_keys_for_room_route(
192	State(services): State<crate::State>,
193	body: Ruma<add_backup_keys_for_room::v3::Request>,
194) -> Result<add_backup_keys_for_room::v3::Response> {
195	if services
196		.key_backups
197		.get_latest_backup_version(body.sender_user())
198		.await
199		.is_ok_and(|version| version != body.version)
200	{
201		return Err!(Request(InvalidParam(
202			"You may only manipulate the most recently created version of the backup."
203		)));
204	}
205
206	for (session_id, key_data) in &body.sessions {
207		services
208			.key_backups
209			.add_key(body.sender_user(), &body.version, &body.room_id, session_id, key_data)
210			.await?;
211	}
212
213	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
214
215	Ok(add_backup_keys_for_room::v3::Response { count, etag })
216}
217
218/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
219///
220/// Add the received backup key to the database.
221///
222/// - Only manipulating the most recently created version of the backup is
223///   allowed
224/// - Adds the keys to the backup
225/// - Returns the new number of keys in this backup and the etag
226pub(crate) async fn add_backup_keys_for_session_route(
227	State(services): State<crate::State>,
228	body: Ruma<add_backup_keys_for_session::v3::Request>,
229) -> Result<add_backup_keys_for_session::v3::Response> {
230	if services
231		.key_backups
232		.get_latest_backup_version(body.sender_user())
233		.await
234		.is_ok_and(|version| version != body.version)
235	{
236		return Err!(Request(InvalidParam(
237			"You may only manipulate the most recently created version of the backup."
238		)));
239	}
240
241	// Check if we already have a better key
242	let mut ok_to_replace = true;
243	if let Some(old_key) = &services
244		.key_backups
245		.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
246		.await
247		.ok()
248	{
249		let old_is_verified = old_key
250			.get_field::<bool>("is_verified")?
251			.unwrap_or_default();
252
253		let new_is_verified = body
254			.session_data
255			.get_field::<bool>("is_verified")?
256			.ok_or_else(|| err!(Request(BadJson("`is_verified` field should exist"))))?;
257
258		// Prefer key that `is_verified`
259		if old_is_verified != new_is_verified {
260			if old_is_verified {
261				ok_to_replace = false;
262			}
263		} else {
264			// If both have same `is_verified`, prefer the one with lower
265			// `first_message_index`
266			let old_first_message_index = old_key
267				.get_field::<UInt>("first_message_index")?
268				.unwrap_or(UInt::MAX);
269
270			let new_first_message_index = body
271				.session_data
272				.get_field::<UInt>("first_message_index")?
273				.ok_or_else(|| {
274					err!(Request(BadJson("`first_message_index` field should exist")))
275				})?;
276
277			ok_to_replace = match new_first_message_index.cmp(&old_first_message_index) {
278				| Ordering::Less => true,
279				| Ordering::Greater => false,
280				| Ordering::Equal => {
281					// If both have same `first_message_index`, prefer the one with lower
282					// `forwarded_count`
283					let old_forwarded_count = old_key
284						.get_field::<UInt>("forwarded_count")?
285						.unwrap_or(UInt::MAX);
286
287					let new_forwarded_count = body
288						.session_data
289						.get_field::<UInt>("forwarded_count")?
290						.ok_or_else(|| {
291							err!(Request(BadJson("`forwarded_count` field should exist")))
292						})?;
293
294					new_forwarded_count < old_forwarded_count
295				},
296			};
297		}
298	}
299
300	if ok_to_replace {
301		services
302			.key_backups
303			.add_key(
304				body.sender_user(),
305				&body.version,
306				&body.room_id,
307				&body.session_id,
308				&body.session_data,
309			)
310			.await?;
311	}
312
313	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
314
315	Ok(add_backup_keys_for_session::v3::Response { count, etag })
316}
317
318/// # `GET /_matrix/client/r0/room_keys/keys`
319///
320/// Retrieves all keys from the backup.
321pub(crate) async fn get_backup_keys_route(
322	State(services): State<crate::State>,
323	body: Ruma<get_backup_keys::v3::Request>,
324) -> Result<get_backup_keys::v3::Response> {
325	let rooms = services
326		.key_backups
327		.get_all(body.sender_user(), &body.version)
328		.await;
329
330	Ok(get_backup_keys::v3::Response { rooms })
331}
332
333/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
334///
335/// Retrieves all keys from the backup for a given room.
336pub(crate) async fn get_backup_keys_for_room_route(
337	State(services): State<crate::State>,
338	body: Ruma<get_backup_keys_for_room::v3::Request>,
339) -> Result<get_backup_keys_for_room::v3::Response> {
340	let sessions = services
341		.key_backups
342		.get_room(body.sender_user(), &body.version, &body.room_id)
343		.await;
344
345	Ok(get_backup_keys_for_room::v3::Response { sessions })
346}
347
348/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
349///
350/// Retrieves a key from the backup.
351pub(crate) async fn get_backup_keys_for_session_route(
352	State(services): State<crate::State>,
353	body: Ruma<get_backup_keys_for_session::v3::Request>,
354) -> Result<get_backup_keys_for_session::v3::Response> {
355	let key_data = services
356		.key_backups
357		.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
358		.await
359		.map_err(|_| {
360			err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
361		})?;
362
363	Ok(get_backup_keys_for_session::v3::Response { key_data })
364}
365
366/// # `DELETE /_matrix/client/r0/room_keys/keys`
367///
368/// Delete the keys from the backup.
369pub(crate) async fn delete_backup_keys_route(
370	State(services): State<crate::State>,
371	body: Ruma<delete_backup_keys::v3::Request>,
372) -> Result<delete_backup_keys::v3::Response> {
373	services
374		.key_backups
375		.delete_all_keys(body.sender_user(), &body.version)
376		.await;
377
378	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
379
380	Ok(delete_backup_keys::v3::Response { count, etag })
381}
382
383/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
384///
385/// Delete the keys from the backup for a given room.
386pub(crate) async fn delete_backup_keys_for_room_route(
387	State(services): State<crate::State>,
388	body: Ruma<delete_backup_keys_for_room::v3::Request>,
389) -> Result<delete_backup_keys_for_room::v3::Response> {
390	services
391		.key_backups
392		.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
393		.await;
394
395	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
396
397	Ok(delete_backup_keys_for_room::v3::Response { count, etag })
398}
399
400/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
401///
402/// Delete a key from the backup.
403pub(crate) async fn delete_backup_keys_for_session_route(
404	State(services): State<crate::State>,
405	body: Ruma<delete_backup_keys_for_session::v3::Request>,
406) -> Result<delete_backup_keys_for_session::v3::Response> {
407	services
408		.key_backups
409		.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
410		.await;
411
412	let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
413
414	Ok(delete_backup_keys_for_session::v3::Response { count, etag })
415}
416
417async fn get_count_etag(
418	services: &Services,
419	sender_user: &UserId,
420	version: &str,
421) -> Result<(UInt, String)> {
422	let count = services
423		.key_backups
424		.count_keys(sender_user, version)
425		.map(TryInto::try_into);
426
427	let etag = services
428		.key_backups
429		.get_etag(sender_user, version)
430		.map(Ok);
431
432	Ok(try_join(count, etag).await?)
433}