Skip to main content

arminius_server_client/handlers/
room_keys.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Implements APIs for room keys backups.
6//!
7//! NOTE: This file contains a total of 14 endpoints. We sorted them with the
8//! same ordering as in [ruma::api::client::backup].
9
10#![allow(
11	clippy::allow_attributes,
12	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`.
13	But an exception to this lint will mistakenly report that it's unfulfilled when used with
14	expect, even though it is not."
15)]
16
17mod tests;
18
19use std::collections::BTreeMap;
20
21use arminius_lib_component::{
22	Component, ServerState,
23	error::{InputJsonError, InternalServerError, MatrixApiError, ResultExt as _},
24	ruma_requests::ArminiusResponse,
25};
26use arminius_lib_db::{Database as _, TransactedDb};
27use arminius_lib_db_keys::{
28	DbRoomKeysBackup, RoomKeysQueries as _, TransactedRoomKeysQueries as _,
29};
30use axum::{
31	Router,
32	extract::State,
33	routing::{delete, post, put},
34};
35use itertools::Itertools as _;
36use reqwest::StatusCode;
37use ruma::{
38	OwnedRoomId, UInt, UserId,
39	api::{
40		client::backup::{
41			BackupAlgorithm, KeyBackupData, RoomKeyBackup, add_backup_keys,
42			add_backup_keys_for_room, add_backup_keys_for_session, create_backup_version,
43			delete_backup_keys, delete_backup_keys_for_room, delete_backup_keys_for_session,
44			delete_backup_version, get_backup_info, get_backup_keys, get_backup_keys_for_room,
45			get_backup_keys_for_session, get_latest_backup_info, update_backup_version,
46		},
47		error::{ErrorBody, ErrorKind, StandardErrorBody, WrongRoomKeysVersionErrorData},
48	},
49	serde::Raw,
50};
51
52use crate::client_request::AuthenticatedClientRequest;
53
54/// Error thrown when the version specified does not match the current
55/// backup version.
56///
57/// NOTE: Since this error is turned into a [`ErrorKind::WrongRoomKeysVersion`],
58/// we need to manually implement [`MatrixApiError`] for it.
59#[derive(Debug, thiserror::Error)]
60#[error("Wrong backup version")]
61struct WrongBackupVersion {
62	/// The current backup version of the user that should have been used.
63	error_data: WrongRoomKeysVersionErrorData,
64}
65
66impl WrongBackupVersion {
67	/// Helper to create a [`WrongBackupVersion`].
68	fn new(current_version: String) -> Self {
69		Self { error_data: WrongRoomKeysVersionErrorData::new(current_version) }
70	}
71}
72
73/// Turns a [`WrongBackupVersion`] into a [`ruma::api::error::Error`].
74impl MatrixApiError for WrongBackupVersion {
75	fn create_matrix_error(&self) -> ruma::api::error::Error {
76		ruma::api::error::Error::new(
77			StatusCode::FORBIDDEN,
78			ErrorBody::Standard(StandardErrorBody::new(
79				ErrorKind::WrongRoomKeysVersion(self.error_data.clone()),
80				"Wrong backup version".to_owned(),
81			)),
82		)
83	}
84}
85
86#[allow(
87	clippy::missing_docs_in_private_items,
88	reason = "All of the variants of the enum are documented, documenting them here would be redundant."
89)]
90#[derive(Debug, thiserror::Error, MatrixApiError, arminius_lib_macros::FromDbError)]
91enum RoomKeysError {
92	#[error(transparent)]
93	InternalServerError(#[from] InternalServerError),
94	#[error(transparent)]
95	InputJsonError(#[from] InputJsonError),
96	/// Error returned when requesting an unspecified backup but we find none.
97	#[error("No current backup version")]
98	#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
99	NoBackupFound,
100
101	/// Error returned when the specified backup version was not found.
102	#[error("Backup not found")]
103	#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
104	BackupNotFound,
105
106	/// Error returned when the specified backup's key was not found.
107	#[error("Key not found")]
108	#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
109	KeyNotFound,
110	/// Error returned when a specified backup version fails to being parsed.
111	#[error("Invalid version string")]
112	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam)]
113	InvalidBackupVersion,
114	#[error(transparent)]
115	WrongBackupVersion(#[from] WrongBackupVersion),
116	/// Error returned when given an algorithm that does not match the stored
117	/// one.
118	#[error("Invalid version string")]
119	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam)]
120	AlgorithmMismatch,
121}
122
123/// Checks whether we should replace an existing backup's data with the given
124/// new one.
125///
126/// First, if there is no existing current backup, we of course want to keep the
127/// new one.
128///
129/// Then, the rules to decide whether to keep the existing one or to replace it
130/// are:
131/// - If the keys have different values for `is_verified`, keep the one which `is_verified = true`
132/// - Otherwise if they are identical, keep the key with the lowest `first_message_index`
133/// - Otherwise if they are identical, keep the key with the lowest `forwarded_count`
134fn should_keep_new_backup(
135	current_backup: Option<&KeyBackupData>,
136	new_backup: &KeyBackupData,
137) -> bool {
138	current_backup.is_none_or(|existing_backup| {
139		new_backup.is_verified && !existing_backup.is_verified
140			|| new_backup.first_message_index < existing_backup.first_message_index
141			|| new_backup.forwarded_count < existing_backup.forwarded_count
142	})
143}
144
145/// Handles newly uploaded backup(s) and upserts the new one if they match the
146/// rules of [should_keep_new_backup].
147///
148/// Note that it will only upsert (and hence bump the `etag`) if there is new
149/// backup data to store.
150///
151/// Returns the backup's `etag` and `count` fields, taken from the newly
152/// upserted one or the existing one.
153async fn handle_new_backups<'a>(
154	transaction: &mut TransactedDb<'a>,
155	user_id: &UserId,
156	current_backup: &DbRoomKeysBackup,
157	new_backups: BTreeMap<OwnedRoomId, RoomKeyBackup>,
158) -> Result<(i64, u64), RoomKeysError> {
159	let mut to_upsert: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
160
161	for (room_id, room_backups) in new_backups {
162		for (session_id, backup_data) in room_backups.sessions {
163			let new_backup_data = backup_data.deserialize().map_err(InputJsonError::from)?;
164
165			if should_keep_new_backup(
166				current_backup.rooms.get(&room_id).and_then(|room_keys| room_keys.get(&session_id)),
167				&new_backup_data,
168			) {
169				to_upsert.entry(room_id.clone()).or_default().insert(
170					session_id,
171					serde_json::to_value(new_backup_data)
172						.unrecoverable_error("Failed to serialize backup data")?,
173				);
174			}
175		}
176	}
177
178	let (backup_etag, backup_count) = match to_upsert.is_empty() {
179		true => (current_backup.etag, current_backup.keys_count),
180		false => transaction.upsert_backup_data(user_id, current_backup.version, to_upsert).await?,
181	};
182
183	Ok((backup_etag, backup_count))
184}
185
186/// Handler for `PUT /_matrix/client/v3/room_keys/keys` (See
187/// <https://spec.matrix.org/v1.18/client-server-api/#put_matrixclientv3room_keyskeys>).
188///
189/// Store several keys in a backup.
190async fn add_backup_keys_handler(
191	mut state: State<ServerState>,
192	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
193		add_backup_keys::v3::Request,
194	>,
195) -> Result<ArminiusResponse<add_backup_keys::v3::Response>, RoomKeysError> {
196	let req_version =
197		request.version.parse::<i64>().map_err(|_e| RoomKeysError::InvalidBackupVersion)?;
198
199	let mut transaction = state.database.begin_transaction().await?;
200	let current_backup = transaction
201		.fetch_user_backup(&claims.user_id, None, None, None)
202		.await?
203		.ok_or(RoomKeysError::NoBackupFound)?;
204
205	if req_version != current_backup.version {
206		return Err(WrongBackupVersion::new(current_backup.version.to_string()).into());
207	}
208
209	let (backup_etag, backup_count) =
210		handle_new_backups(&mut transaction, &claims.user_id, &current_backup, request.rooms)
211			.await?;
212
213	transaction.commit().await?;
214
215	Ok(add_backup_keys::v3::Response::new(
216		backup_etag.to_string(),
217		UInt::new(backup_count).unrecoverable_error("Failed to instantiate UInt")?,
218	)
219	.into())
220}
221
222/// Handler for `PUT /_matrix/client/v3/room_keys/keys/{roomId}` (See
223/// <https://spec.matrix.org/v1.18/client-server-api/#put_matrixclientv3room_keyskeysroomid>).
224///
225/// Store several keys in a backup for a given room.
226async fn add_backup_keys_for_room_handler(
227	mut state: State<ServerState>,
228	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
229		add_backup_keys_for_room::v3::Request,
230	>,
231) -> Result<ArminiusResponse<add_backup_keys_for_room::v3::Response>, RoomKeysError> {
232	let req_version =
233		request.version.parse::<i64>().map_err(|_e| RoomKeysError::InvalidBackupVersion)?;
234
235	let mut transaction = state.database.begin_transaction().await?;
236	let current_backup = transaction
237		.fetch_user_backup(&claims.user_id, None, None, None)
238		.await?
239		.ok_or(RoomKeysError::NoBackupFound)?;
240
241	if req_version != current_backup.version {
242		return Err(WrongBackupVersion::new(current_backup.version.to_string()).into());
243	}
244
245	let (backup_etag, backup_count) = handle_new_backups(
246		&mut transaction,
247		&claims.user_id,
248		&current_backup,
249		BTreeMap::from([(request.room_id, RoomKeyBackup::new(request.sessions))]),
250	)
251	.await?;
252
253	transaction.commit().await?;
254
255	Ok(add_backup_keys_for_room::v3::Response::new(
256		backup_etag.to_string(),
257		UInt::new(backup_count).unrecoverable_error("Failed to instantiate UInt")?,
258	)
259	.into())
260}
261
262/// Handler for `PUT /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}`
263/// (See <https://spec.matrix.org/v1.18/client-server-api/#put_matrixclientv3room_keyskeysroomidsessionid>).
264///
265/// Store a key in a backup.
266async fn add_backup_keys_for_session_handler(
267	mut state: State<ServerState>,
268	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
269		add_backup_keys_for_session::v3::Request,
270	>,
271) -> Result<ArminiusResponse<add_backup_keys_for_session::v3::Response>, RoomKeysError> {
272	let req_version =
273		request.version.parse::<i64>().map_err(|_e| RoomKeysError::InvalidBackupVersion)?;
274
275	let mut transaction = state.database.begin_transaction().await?;
276	let current_backup = transaction
277		.fetch_user_backup(&claims.user_id, None, None, None)
278		.await?
279		.ok_or(RoomKeysError::NoBackupFound)?;
280
281	if req_version != current_backup.version {
282		return Err(WrongBackupVersion::new(current_backup.version.to_string()).into());
283	}
284
285	let (new_etag, new_count) = handle_new_backups(
286		&mut transaction,
287		&claims.user_id,
288		&current_backup,
289		BTreeMap::from([(
290			request.room_id,
291			RoomKeyBackup::new(BTreeMap::from([(request.session_id, request.session_data)])),
292		)]),
293	)
294	.await?;
295
296	transaction.commit().await?;
297
298	Ok(add_backup_keys_for_session::v3::Response::new(
299		new_etag.to_string(),
300		UInt::new(new_count).unrecoverable_error("Failed to instantiate UInt")?,
301	)
302	.into())
303}
304
305/// Handler for `POST /_matrix/client/v3/room_keys/version` (See
306/// <https://spec.matrix.org/v1.18/client-server-api/#post_matrixclientv3room_keysversion>).
307///
308/// Inserts a new backup in the database.
309async fn create_backup_version_handler(
310	mut state: State<ServerState>,
311	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
312		create_backup_version::v3::Request,
313	>,
314) -> Result<ArminiusResponse<create_backup_version::v3::Response>, RoomKeysError> {
315	let algorithm = request.algorithm.deserialize().map_err(InputJsonError::from)?;
316
317	let auth_data =
318		serde_json::to_value(algorithm).unrecoverable_error("Failed to serialize algorithm")?;
319
320	let version = state
321		.database
322		.insert_backup(&claims.user_id, &auth_data)
323		.await
324		.unrecoverable_error("Failed to insert new backup")?;
325
326	Ok(create_backup_version::v3::Response::new(version).into())
327}
328
329/// Handler for `DELETE /_matrix/client/v3/room_keys/keys` (See
330/// <https://spec.matrix.org/v1.18/client-server-api/#delete_matrixclientv3room_keyskeys>).
331///
332/// Delete the keys from a backup.
333async fn delete_backup_keys_handler(
334	mut state: State<ServerState>,
335	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
336		delete_backup_keys::v3::Request,
337	>,
338) -> Result<ArminiusResponse<delete_backup_keys::v3::Response>, RoomKeysError> {
339	let version =
340		request.version.parse::<i64>().map_err(|_e| RoomKeysError::InvalidBackupVersion)?;
341
342	let (new_etag, new_count) = state
343		.database
344		.delete_backup_keys(&claims.user_id, version, None, None)
345		.await?
346		.ok_or(RoomKeysError::BackupNotFound)?;
347
348	Ok(delete_backup_keys::v3::Response::new(
349		new_etag.to_string(),
350		UInt::new(new_count).unrecoverable_error("Creating UInt")?,
351	)
352	.into())
353}
354
355/// Handler for `DELETE /_matrix/client/v3/room_keys/keys/{roomId}` (See
356/// <https://spec.matrix.org/v1.18/client-server-api/#delete_matrixclientv3room_keyskeysroomid>).
357///
358/// Delete the keys from a backup for a given room.
359async fn delete_backup_keys_for_room_handler(
360	mut state: State<ServerState>,
361	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
362		delete_backup_keys_for_room::v3::Request,
363	>,
364) -> Result<ArminiusResponse<delete_backup_keys_for_room::v3::Response>, RoomKeysError> {
365	let version =
366		request.version.parse::<i64>().map_err(|_e| RoomKeysError::InvalidBackupVersion)?;
367
368	let (new_etag, new_count) = state
369		.database
370		.delete_backup_keys(&claims.user_id, version, Some(request.room_id), None)
371		.await?
372		.ok_or(RoomKeysError::BackupNotFound)?;
373
374	Ok(delete_backup_keys_for_room::v3::Response::new(
375		new_etag.to_string(),
376		UInt::new(new_count).unrecoverable_error("Creating UInt")?,
377	)
378	.into())
379}
380
381/// Handler for `DELETE /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}`
382/// (See <https://spec.matrix.org/v1.18/client-server-api/#delete_matrixclientv3room_keyskeysroomidsessionid>).
383///
384/// Delete a key from a backup.
385async fn delete_backup_keys_for_session_handler(
386	mut state: State<ServerState>,
387	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
388		delete_backup_keys_for_session::v3::Request,
389	>,
390) -> Result<ArminiusResponse<delete_backup_keys_for_session::v3::Response>, RoomKeysError> {
391	let version =
392		request.version.parse::<i64>().map_err(|_e| RoomKeysError::InvalidBackupVersion)?;
393
394	let (new_etag, new_count) = state
395		.database
396		.delete_backup_keys(
397			&claims.user_id,
398			version,
399			Some(request.room_id),
400			Some(request.session_id),
401		)
402		.await?
403		.ok_or(RoomKeysError::BackupNotFound)?;
404
405	Ok(delete_backup_keys_for_session::v3::Response::new(
406		new_etag.to_string(),
407		UInt::new(new_count).unrecoverable_error("Creating UInt")?,
408	)
409	.into())
410}
411
412/// Handler for `DELETE /_matrix/client/v3/room_keys/version/{version}` (See
413/// <https://spec.matrix.org/v1.18/client-server-api/#delete_matrixclientv3room_keysversionversion>).
414///
415/// Delete a specific backup.
416async fn delete_backup_version_handler(
417	mut state: State<ServerState>,
418	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
419		delete_backup_version::v3::Request,
420	>,
421) -> Result<ArminiusResponse<delete_backup_version::v3::Response>, RoomKeysError> {
422	let mut transaction = state.database.begin_transaction().await?;
423
424	let last_user_backup_version =
425		transaction.fetch_max_user_backup_version(&claims.user_id).await?;
426
427	let req_version: i64 =
428		request.version.parse().map_err(|_err| RoomKeysError::InvalidBackupVersion)?;
429
430	if req_version < 1 || req_version > last_user_backup_version {
431		return Err(RoomKeysError::BackupNotFound);
432	}
433
434	transaction.delete_user_backup(&claims.user_id, req_version).await?;
435
436	transaction.commit().await?;
437
438	Ok(delete_backup_version::v3::Response::new().into())
439}
440
441/// Handler for `GET /_matrix/client/v3/room_keys/version/{version}` (See
442/// <https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3room_keysversionversion>).
443///
444/// Gets information about a specific backup.
445async fn get_backup_info_handler(
446	mut state: State<ServerState>,
447	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
448		get_backup_info::v3::Request,
449	>,
450) -> Result<ArminiusResponse<get_backup_info::v3::Response>, RoomKeysError> {
451	let version = request.version.parse().map_err(|_err| RoomKeysError::InvalidBackupVersion)?;
452
453	let mut transaction = state.database.begin_transaction().await?;
454	let backup = transaction
455		.fetch_user_backup(&claims.user_id, Some(version), None, None)
456		.await?
457		.ok_or(RoomKeysError::NoBackupFound)?;
458	transaction.commit().await?;
459
460	let algorithm = Raw::<BackupAlgorithm>::from_json(
461		serde_json::value::to_raw_value(&backup.auth_data)
462			.unrecoverable_error("Failed to convert backup data to raw")?,
463	);
464
465	Ok(get_backup_info::v3::Response::new(
466		algorithm,
467		UInt::new(backup.keys_count).unrecoverable_error("Failed to create UInt")?,
468		backup.etag.to_string(),
469		backup.version.to_string(),
470	)
471	.into())
472}
473
474/// Handler for `GET /_matrix/client/v3/room_keys/keys` (See
475/// <https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3room_keyskeys>).
476///
477/// Retrieve the keys from a backup.
478async fn get_backup_keys_handler(
479	mut state: State<ServerState>,
480	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
481		get_backup_keys::v3::Request,
482	>,
483) -> Result<ArminiusResponse<get_backup_keys::v3::Response>, RoomKeysError> {
484	let version = request.version.parse().map_err(|_err| RoomKeysError::InvalidBackupVersion)?;
485
486	let mut transaction = state.database.begin_transaction().await?;
487	let backup = transaction
488		.fetch_user_backup(&claims.user_id, Some(version), None, None)
489		.await?
490		.ok_or(RoomKeysError::NoBackupFound)?;
491	transaction.commit().await?;
492
493	let backups = backup
494		.rooms
495		.into_iter()
496		.map(|(room_id, room_backups)| {
497			let sessions = room_backups
498				.into_iter()
499				.map(|(session_id, backup_data)| {
500					let raw =
501						Raw::new(&backup_data).unrecoverable_error("Failed to convert to raw")?;
502
503					Ok((session_id, raw))
504				})
505				.collect::<Result<_, InternalServerError>>()?;
506
507			Ok((room_id, RoomKeyBackup::new(sessions)))
508		})
509		.collect::<Result<_, InternalServerError>>()?;
510
511	Ok(get_backup_keys::v3::Response::new(backups).into())
512}
513
514/// Handler for `GET /_matrix/client/v3/room_keys/keys/{roomId}` (See
515/// <https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3room_keyskeysroomid>).
516///
517/// Retrieve the keys from a backup for a given room.
518async fn get_backup_keys_for_room_handler(
519	mut state: State<ServerState>,
520	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
521		get_backup_keys_for_room::v3::Request,
522	>,
523) -> Result<ArminiusResponse<get_backup_keys_for_room::v3::Response>, RoomKeysError> {
524	let version = request.version.parse().map_err(|_err| RoomKeysError::InvalidBackupVersion)?;
525
526	let mut transaction = state.database.begin_transaction().await?;
527	let backup = transaction
528		.fetch_user_backup(&claims.user_id, Some(version), Some(request.room_id), None)
529		.await?
530		.ok_or(RoomKeysError::NoBackupFound)?;
531	transaction.commit().await?;
532
533	let backups = backup
534		.rooms
535		.into_values()
536		.at_most_one()
537		.unrecoverable_error("Query should have filtered a single room")?
538		.map(|room_backups| {
539			room_backups
540				.into_iter()
541				.map(|(session_id, backup_data)| {
542					let raw =
543						Raw::new(&backup_data).unrecoverable_error("Failed to convert to raw")?;
544
545					Ok((session_id, raw))
546				})
547				.collect::<Result<_, InternalServerError>>()
548		})
549		.transpose()?
550		.unwrap_or(BTreeMap::new());
551
552	Ok(get_backup_keys_for_room::v3::Response::new(backups).into())
553}
554
555/// Handler for `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}`
556/// (See <https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3room_keyskeysroomidsessionid>).
557///
558/// Retrieve a key from a backup.
559async fn get_backup_keys_for_session_handler(
560	mut state: State<ServerState>,
561	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
562		get_backup_keys_for_session::v3::Request,
563	>,
564) -> Result<ArminiusResponse<get_backup_keys_for_session::v3::Response>, RoomKeysError> {
565	let version = request.version.parse().map_err(|_err| RoomKeysError::InvalidBackupVersion)?;
566
567	let mut transaction = state.database.begin_transaction().await?;
568	let backup = transaction
569		.fetch_user_backup(
570			&claims.user_id,
571			Some(version),
572			Some(request.room_id),
573			Some(request.session_id),
574		)
575		.await?
576		.ok_or(RoomKeysError::NoBackupFound)?;
577	transaction.commit().await?;
578
579	let backup = backup
580		.rooms
581		.into_values()
582		.at_most_one()
583		.unrecoverable_error("Query should have filtered a single room")?
584		.map(|room_backups| -> Result<_, InternalServerError> {
585			room_backups
586				.into_values()
587				.at_most_one()
588				.unrecoverable_error("Query should have filtered a single session")?
589				.map(|backup_data| -> Result<_, InternalServerError> {
590					Raw::new(&backup_data).unrecoverable_error("Failed to convert to raw")
591				})
592				.transpose()
593		})
594		.transpose()?
595		.flatten()
596		.ok_or(RoomKeysError::KeyNotFound)?;
597
598	Ok(get_backup_keys_for_session::v3::Response::new(backup).into())
599}
600
601/// Handler for `GET /_matrix/client/v3/room_keys/version` (See
602/// <https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3room_keysversion>).
603///
604/// Gets the latest room keys backup information.
605async fn get_latest_backup_info_handler(
606	mut state: State<ServerState>,
607	AuthenticatedClientRequest(claims, _request): AuthenticatedClientRequest<
608		get_latest_backup_info::v3::Request,
609	>,
610) -> Result<ArminiusResponse<get_latest_backup_info::v3::Response>, RoomKeysError> {
611	let mut transaction = state.database.begin_transaction().await?;
612	let backup_info = transaction
613		.fetch_user_backup(&claims.user_id, None, None, None)
614		.await?
615		.ok_or(RoomKeysError::NoBackupFound)?;
616	transaction.commit().await?;
617
618	let algorithm = Raw::<BackupAlgorithm>::from_json(
619		serde_json::value::to_raw_value(&backup_info.auth_data)
620			.unrecoverable_error("Failed to convert backup data to raw")?,
621	);
622
623	Ok(get_latest_backup_info::v3::Response::new(
624		algorithm,
625		UInt::new(backup_info.keys_count).unrecoverable_error("Failed to create UInt")?,
626		backup_info.etag.to_string(),
627		backup_info.version.to_string(),
628	)
629	.into())
630}
631
632/// Handler for `PUT /_matrix/client/v3/room_keys/version/{version}` (See
633/// <https://spec.matrix.org/v1.18/client-server-api/#put_matrixclientv3room_keysversionversion>).
634///
635/// Update information about an existing backup.
636///
637/// NOTE: Only `auth_data` can be modified.
638async fn update_backup_version_handler(
639	mut state: State<ServerState>,
640	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
641		update_backup_version::v3::Request,
642	>,
643) -> Result<ArminiusResponse<update_backup_version::v3::Response>, RoomKeysError> {
644	let version = request.version.parse().map_err(|_err| RoomKeysError::InvalidBackupVersion)?;
645
646	// Fetch the backup to make sure it exists and the fields other than
647	// `auth_data` are identical.
648	let mut transaction = state.database.begin_transaction().await?;
649	let backup_info = transaction
650		.fetch_user_backup(&claims.user_id, Some(version), None, None)
651		.await?
652		.ok_or(RoomKeysError::NoBackupFound)?;
653
654	let db_algorithm: BackupAlgorithm = serde_json::from_value(backup_info.auth_data)
655		.unrecoverable_error("Invalid auth_data in database")?;
656
657	let req_algorithm = request.algorithm.deserialize().map_err(InputJsonError::from)?;
658
659	if db_algorithm.algorithm() != req_algorithm.algorithm() {
660		return Err(RoomKeysError::AlgorithmMismatch);
661	}
662
663	let new_auth_data =
664		serde_json::to_value(req_algorithm).unrecoverable_error("Failed to serialize algorithm")?;
665	transaction.update_user_backup(&claims.user_id, version, &new_auth_data).await?;
666
667	transaction.commit().await?;
668
669	Ok(update_backup_version::v3::Response::new().into())
670}
671
672/// Component implementing the room keys backups
673#[derive(Debug)]
674pub struct RoomKeysComponent;
675
676impl Component for RoomKeysComponent {
677	fn router() -> Router<ServerState> {
678		Router::new()
679			.route(
680				"/_matrix/client/v3/room_keys/version",
681				post(create_backup_version_handler).get(get_latest_backup_info_handler),
682			)
683			.route(
684				"/_matrix/client/v3/room_keys/keys",
685				put(add_backup_keys_handler)
686					.delete(delete_backup_keys_handler)
687					.get(get_backup_keys_handler),
688			)
689			.route(
690				"/_matrix/client/v3/room_keys/keys/{roomId}",
691				put(add_backup_keys_for_room_handler)
692					.delete(delete_backup_keys_for_room_handler)
693					.get(get_backup_keys_for_room_handler),
694			)
695			.route(
696				"/_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}",
697				put(add_backup_keys_for_session_handler)
698					.delete(delete_backup_keys_for_session_handler)
699					.get(get_backup_keys_for_session_handler),
700			)
701			.route(
702				"/_matrix/client/v3/room_keys/version/{version}",
703				delete(delete_backup_version_handler)
704					.get(get_backup_info_handler)
705					.put(update_backup_version_handler),
706			)
707	}
708}