1#![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#[derive(Debug, thiserror::Error)]
60#[error("Wrong backup version")]
61struct WrongBackupVersion {
62 error_data: WrongRoomKeysVersionErrorData,
64}
65
66impl WrongBackupVersion {
67 fn new(current_version: String) -> Self {
69 Self { error_data: WrongRoomKeysVersionErrorData::new(current_version) }
70 }
71}
72
73impl 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("No current backup version")]
98 #[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
99 NoBackupFound,
100
101 #[error("Backup not found")]
103 #[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
104 BackupNotFound,
105
106 #[error("Key not found")]
108 #[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
109 KeyNotFound,
110 #[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("Invalid version string")]
119 #[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam)]
120 AlgorithmMismatch,
121}
122
123fn 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
145async 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
186async 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, ¤t_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
222async 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 ¤t_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
262async 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 ¤t_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
305async 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
329async 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
355async 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
381async 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
412async 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
441async 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
474async 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
514async 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
555async 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
601async 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
632async 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 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#[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}