Skip to main content

arminius_server_client/handlers/
user_keys.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Implements APIs for client key upload from the matrix spec
6
7#![allow(
8	clippy::allow_attributes,
9	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`. But an exception to this lint will mistakenly report that it's unfulfilled when used with expect, even though it is not."
10)]
11
12mod tests;
13
14use std::{collections::BTreeMap, time::Duration};
15
16use arminius_lib_authentication::SessionInfo;
17use arminius_lib_component::{
18	Component, ServerState,
19	error::{
20		BadJson, InputJsonError, InternalServerError, MatrixApiError, ResultExt as _,
21		create_custom_matrix_error,
22	},
23	ruma_requests::ArminiusResponse,
24};
25use arminius_lib_db::{Database as _, TransactedDb};
26use arminius_lib_db_keys::{
27	FallbackKeysQueries as _, OneTimeKeysQueries as _, UserKeysQueries as _,
28};
29use arminius_lib_db_signatures::LocalSignatureQueries as _;
30use arminius_lib_macros::{FromDbError, FromNatsError};
31use arminius_lib_model::user_keys::DbKeyType;
32use arminius_lib_user_keys::{
33	FederatedKeyFailureReason,
34	identity_keys::{FederatedIdentityKeysResponse, RawUserKeys},
35	one_time_keys::FederatedOTKResponse,
36	signatures::{SignatureVerificationError, verify_signatures},
37	traits::SigningKey as _,
38};
39use arminius_server_federation_output::clients::FederationNatsClient;
40use axum::{Router, extract::State, routing::post};
41use reqwest::StatusCode;
42use ruma::{
43	DeviceId, OwnedServerName, UserId,
44	api::{
45		client::keys::{claim_keys, get_keys, upload_keys},
46		error::{Error as MatrixError, ErrorKind},
47	},
48	encryption::{DeviceKeys, OneTimeKey},
49	serde::{Base64, base64::Standard},
50};
51use serde_json::json;
52use tokio_util::task::JoinMap;
53
54use crate::client_request::AuthenticatedClientRequest;
55
56/// Thrown when signature verification fails.
57#[derive(Debug, thiserror::Error)]
58#[error("Invalid signature: {0}")]
59pub struct SignatureError(String);
60
61// Manually implemented, because `ErrorKind` does not agree with custom error
62// codes.
63impl MatrixApiError for SignatureError {
64	fn create_matrix_error(&self) -> MatrixError {
65		create_custom_matrix_error(
66			StatusCode::BAD_REQUEST,
67			"M_INVALID_SIGNATURE",
68			&self.to_string(),
69		)
70	}
71}
72
73/// Converts a [`SignatureVerificationError`] into a [`UserKeysError`] so that
74/// it can be returned as Matrix error.
75impl From<SignatureVerificationError> for UserKeysError {
76	fn from(signature_error: SignatureVerificationError) -> Self {
77		match signature_error {
78			SignatureVerificationError::BadJson(bad_json) => {
79				Self::InputJsonError(InputJsonError::BadJson(bad_json))
80			}
81			other_error => Self::SignatureVerification(SignatureError(other_error.to_string())),
82		}
83	}
84}
85
86#[allow(
87	missing_docs,
88	clippy::missing_docs_in_private_items,
89	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
90)]
91#[derive(Debug, thiserror::Error, MatrixApiError, FromDbError, FromNatsError)]
92enum UserKeysError {
93	#[error(transparent)]
94	InputJsonError(#[from] InputJsonError),
95
96	/// The requested action was impossible because we do not have the user's
97	/// device keys.
98	#[error("Missing device keys")]
99	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::MissingParam)]
100	MissingDeviceKeys,
101
102	#[error(transparent)]
103	SignatureVerification(#[from] SignatureError),
104
105	#[error(transparent)]
106	InternalServerError(#[from] InternalServerError),
107}
108
109/// Given the user and device IDs of the authenticated request made, performs
110/// various checks on a [`DeviceKeys`].
111///
112/// # Errors
113///
114/// This function returns a [`BadJson`] error for any of the following cases:
115/// - The device key is missing or has a wrongly formatted field.
116/// - The user ID or device ID does not match that of the authenticated user/device making the
117///   request.
118fn check_device_key(
119	expected_user_id: &UserId,
120	expected_device_id: &DeviceId,
121	keys: &DeviceKeys,
122) -> Result<(), InputJsonError> {
123	if keys.keys.keys().any(|key_id| key_id.key_name() != expected_device_id) {
124		return Err(InputJsonError::BadJson(BadJson(
125			"Device keys contains keys for another device".to_owned(),
126		)));
127	}
128
129	if keys.user_id != expected_user_id {
130		return Err(InputJsonError::BadJson(BadJson(
131			"Field `user_id` does not match in device key".to_owned(),
132		)));
133	}
134
135	if keys.device_id != expected_device_id {
136		return Err(InputJsonError::BadJson(BadJson(
137			"Field `device_id` does not match the requester".to_owned(),
138		)));
139	}
140
141	Ok(())
142}
143
144/// Parameter used to tell `check_onetime_key()` what to check.
145#[allow(
146	clippy::missing_docs_in_private_items,
147	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
148)]
149#[derive(Debug, PartialEq, Clone, Copy)]
150enum ShouldBeFallback {
151	Yes,
152	No,
153}
154
155/// Performs various checks on a [`OneTimeKey`].
156///
157/// # Errors
158///
159/// This function returns a [`BadJson`] error for any of the following cases:
160/// - The `fallback` field is missing or not equal to `true` when the key is supposed to be a
161///   fallback key.
162/// - The `fallback` field is present and set to `true` when the key is supposed to be a one-time
163///   key.
164fn check_onetime_key(
165	should_be_fallback: ShouldBeFallback,
166	otk: &ruma::encryption::SignedKey,
167) -> Result<(), InputJsonError> {
168	if (should_be_fallback == ShouldBeFallback::Yes && !otk.fallback)
169		|| (should_be_fallback == ShouldBeFallback::No && otk.fallback)
170	{
171		return Err(InputJsonError::BadJson(BadJson(
172			"One-time key's fallback field does not match".to_owned(),
173		)));
174	}
175
176	Ok(())
177}
178
179/// Handles a new device-keys upload by:
180/// - Checking its correctness using [`check_device_key`]
181/// - Check its signature is correct
182/// - Extracts its signature(s)
183/// - Store both the keys and the signatures
184///
185/// This function returns the keymap extracted from the given [DeviceKeys].
186async fn handle_new_device_key(
187	transaction: &mut TransactedDb<'_>,
188	session: &SessionInfo,
189	new_keys: &mut DeviceKeys,
190) -> Result<(), UserKeysError> {
191	check_device_key(&session.user_id, &session.device_id, new_keys)?;
192
193	let new_keys_id = new_keys.key_id().map_err(InputJsonError::BadJson)?;
194	verify_signatures(
195		vec![&new_keys.keymap().map_err(InputJsonError::BadJson)?],
196		new_keys,
197		// Device keys MUST be signed by themselves
198		Some(&BTreeMap::from([(session.user_id.clone(), vec![new_keys_id.clone()])])),
199	)?;
200
201	// SAFETY: We just checked keys were at least signed by themselves.
202	#[expect(
203		clippy::expect_used,
204		reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
205	)]
206	let signatures = new_keys
207		.signatures
208		.remove(&session.user_id)
209		.expect("We just checked keys were at least signed by themselves");
210
211	let device_keys_nid =
212		transaction.upsert_device_keys(&session.user_id, &session.device_id, new_keys).await?;
213
214	for (signing_key_id, signature) in signatures {
215		transaction
216			.upsert_local_signature(
217				&session.user_id,
218				// Device keys can either be signed by themselves,
219				if signing_key_id == new_keys_id {
220					DbKeyType::Device
221				} else {
222					// or by Self-Signing keys
223					DbKeyType::SelfSigning
224				},
225				&signing_key_id,
226				device_keys_nid,
227				Base64::<Standard>::parse(signature)
228					.unrecoverable_error("Failed to parse base64")?
229					.as_bytes(),
230			)
231			.await?;
232	}
233
234	Ok(())
235}
236
237/// Handler for `POST /_matrix/client/v3/keys/upload` (See
238/// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3keysupload>).
239///
240/// Endpoint used by users to upload keys.
241async fn upload_keys_handler(
242	mut state: State<ServerState>,
243	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
244		upload_keys::v3::Request,
245	>,
246) -> Result<ArminiusResponse<upload_keys::v3::Response>, UserKeysError> {
247	let mut transaction = state.database.begin_transaction().await?;
248
249	// Uploaded device keys appear in the `device_lists.changed` field of
250	// `/sync` responses of other users, so we need to notify the sync endpoint
251	// about them after committing. One-time/fallback keys are not synced.
252	let uploaded_device_keys = request.device_keys.is_some();
253
254	let device_keys = match request.device_keys {
255		Some(new_device_keys) => {
256			let mut new_device_keys =
257				new_device_keys.deserialize().map_err(InputJsonError::from)?;
258			handle_new_device_key(&mut transaction, &claims, &mut new_device_keys).await?;
259
260			Some(new_device_keys)
261		}
262		None => transaction
263			.fetch_device_keys_for_users(&BTreeMap::from([(
264				claims.user_id.clone(),
265				vec![claims.device_id.clone()],
266			)]))
267			.await?
268			.remove(&claims.user_id)
269			.and_then(|mut user_keys| user_keys.remove(&claims.device_id))
270			.map(|db_key| db_key.key),
271	};
272
273	if !request.one_time_keys.is_empty() || !request.fallback_keys.is_empty() {
274		// If we don't have any device key at this point we won't be able to
275		// verify the signatures of the new one-time and/or fallback keys so
276		// we have to error out.
277		let Some(device_keys) = device_keys else {
278			return Err(UserKeysError::MissingDeviceKeys);
279		};
280
281		let keymap = device_keys.keymap().unrecoverable_error("Failed to get keymap")?;
282
283		// The user's device that doing the request must have signed the other
284		// keys.
285		let required_signers = BTreeMap::from([(
286			claims.user_id.clone(),
287			vec![device_keys.key_id().map_err(InputJsonError::BadJson)?],
288		)]);
289
290		for (key_id, raw_otk) in request.one_time_keys {
291			let one_time_key = raw_otk.deserialize().map_err(InputJsonError::from)?;
292			let OneTimeKey::SignedKey(otk) = &one_time_key else {
293				return Err(InputJsonError::BadJson(BadJson(
294					"Expected one-time keys to be signed keys".to_owned(),
295				))
296				.into());
297			};
298
299			check_onetime_key(ShouldBeFallback::No, otk)?;
300			verify_signatures(vec![&keymap], otk, Some(&required_signers))?;
301
302			transaction
303				.try_insert_one_time_key(
304					&key_id.algorithm(),
305					key_id.key_name(),
306					&claims.user_id,
307					&claims.device_id,
308					&one_time_key,
309				)
310				.await?;
311		}
312
313		for (key_id, raw_fallback_key) in request.fallback_keys {
314			let fallback_key = raw_fallback_key.deserialize().map_err(InputJsonError::from)?;
315			let OneTimeKey::SignedKey(otk) = &fallback_key else {
316				return Err(InputJsonError::BadJson(BadJson(
317					"Expected fallback keys to be signed keys".to_owned(),
318				))
319				.into());
320			};
321
322			check_onetime_key(ShouldBeFallback::Yes, otk)?;
323			verify_signatures(vec![&keymap], otk, Some(&required_signers))?;
324
325			// NOTE: This will overwrite any existing fallback key with the same
326			// algorithm.
327			transaction
328				.upsert_fallback_key(
329					&key_id.algorithm(),
330					key_id.key_name(),
331					&claims.user_id,
332					&claims.device_id,
333					&fallback_key,
334				)
335				.await?;
336		}
337	}
338
339	let one_time_key_counts =
340		transaction.fetch_one_time_keys_counts(&claims.user_id, &claims.device_id).await?;
341
342	transaction.commit().await?;
343
344	if uploaded_device_keys {
345		// Notify possibly listening clients that a user updated their device
346		// keys
347		state.nats_connection.notify_new_sync_data().await?;
348	}
349
350	Ok(upload_keys::v3::Response::new(one_time_key_counts).into())
351}
352
353/// Handler for `POST /_matrix/client/v3/keys/query` (See
354/// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3keysquery>).
355///
356/// Endpoint used to query device and user identity keys.
357async fn get_keys_handler(
358	mut state: State<ServerState>,
359	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<get_keys::v3::Request>,
360) -> Result<ArminiusResponse<get_keys::v3::Response>, UserKeysError> {
361	let (req_local_keys, req_foreign_keys) = request.device_keys.into_iter().fold(
362		(BTreeMap::new(), BTreeMap::<OwnedServerName, BTreeMap<_, _>>::new()),
363		|(mut local_keys, mut foreign_keys), (user_id, user_keys)| {
364			let server_name = user_id.server_name();
365
366			if server_name == state.config.server_name {
367				local_keys.insert(user_id, user_keys);
368			} else {
369				foreign_keys.entry(server_name.to_owned()).or_default().insert(user_id, user_keys);
370			}
371
372			(local_keys, foreign_keys)
373		},
374	);
375
376	let mut response = get_keys::v3::Response::new();
377
378	// PERF: Querying local and foreign keys could be done in parallel.
379
380	{
381		// This endpoint expects us to create entries for every requested user,
382		// even if we did not find their key, so we do this first.
383		for user_id in req_local_keys.keys().cloned() {
384			response.device_keys.entry(user_id).or_default();
385		}
386
387		let mut local_keys: RawUserKeys = arminius_lib_user_keys::identity_keys::get_local_keys(
388			&mut state.database,
389			Some(&claims.user_id),
390			req_local_keys,
391		)
392		.await?
393		.try_into()?;
394
395		response.device_keys.append(&mut local_keys.device_keys);
396
397		response.master_keys.append(&mut local_keys.master_keys);
398		response.self_signing_keys.append(&mut local_keys.self_signing_keys);
399		response.user_signing_keys.append(&mut local_keys.user_signing_keys);
400	}
401
402	{
403		// Recommended default timeout is 10 seconds, see
404		// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3keysquery>.
405		let timeout = request.timeout.unwrap_or(Duration::from_secs(10));
406
407		let mut tasks = JoinMap::new();
408
409		for (server_name, requested_keys) in req_foreign_keys {
410			let client = FederationNatsClient::new(state.nats_connection.client.clone());
411			tasks.spawn(
412				server_name.clone(),
413				tokio::time::timeout(timeout, async move {
414					client.get_federated_user_keys(server_name, requested_keys, Some(timeout)).await
415				}),
416			);
417		}
418
419		while let Some((server_name, result)) = tasks.join_next().await {
420			let result = result.unrecoverable_error("Failed to join spawned task")?;
421
422			// Check if the request timed-out
423			let Ok(federation_response) = result else {
424				response.failures.insert(
425					server_name.to_string(),
426					json!({
427						"reason": FederatedKeyFailureReason::TimedOut.to_string()
428					}),
429				);
430
431				continue;
432			};
433
434			let federation_response =
435				federation_response.unrecoverable_error("Fetching foreign keys failed")?;
436
437			match federation_response {
438				FederatedIdentityKeysResponse::Success(mut federated_keys) => {
439					response.device_keys.append(&mut federated_keys.device_keys);
440					response.master_keys.append(&mut federated_keys.master_keys);
441					response.self_signing_keys.append(&mut federated_keys.self_signing_keys);
442				}
443				FederatedIdentityKeysResponse::Failure(failure) => {
444					response.failures.insert(
445						failure.server_name.to_string(),
446						json!({
447							"reason": failure.reason.to_string()
448						}),
449					);
450				}
451			}
452		}
453	}
454
455	Ok(response.into())
456}
457
458/// Handler for `POST /_matrix/client/v3/keys/claim` (See
459/// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3keysclaim>).
460///
461/// Endpoint that allows users to claim one-time keys.
462async fn claim_keys_handler(
463	mut state: State<ServerState>,
464	AuthenticatedClientRequest(_, request): AuthenticatedClientRequest<claim_keys::v3::Request>,
465) -> Result<ArminiusResponse<claim_keys::v3::Response>, UserKeysError> {
466	let (req_local_keys, req_foreign_keys): (Vec<_>, Vec<_>) = request
467		.one_time_keys
468		.into_iter()
469		.partition(|(user_id, _)| user_id.server_name() == state.config.server_name);
470
471	let req_local_keys = req_local_keys.into_iter().collect();
472	let req_foreign_keys = req_foreign_keys.into_iter().fold(
473		BTreeMap::new(),
474		|mut keys: BTreeMap<OwnedServerName, BTreeMap<_, _>>, (user_id, user_keys)| {
475			keys.entry(user_id.server_name().to_owned()).or_default().insert(user_id, user_keys);
476			keys
477		},
478	);
479
480	let mut response = claim_keys::v3::Response::new(BTreeMap::new());
481
482	// PERF: Querying local and foreign keys could be done in parallel.
483
484	{
485		response.one_time_keys.append(
486			&mut arminius_lib_user_keys::one_time_keys::claim_local_keys(
487				&mut state.database,
488				req_local_keys,
489			)
490			.await?,
491		);
492	}
493
494	{
495		// Recommended default timeout is 10 seconds, see
496		// <https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3keysclaim>.
497		let timeout = request.timeout.unwrap_or(Duration::from_secs(10));
498
499		let mut join_map = JoinMap::new();
500		for (server_name, requested_keys) in req_foreign_keys {
501			let client = FederationNatsClient::new(state.nats_connection.client.clone());
502			join_map.spawn(
503				server_name.clone(),
504				tokio::time::timeout(timeout, async move {
505					client
506						.get_federated_otks(server_name.clone(), requested_keys, Some(timeout))
507						.await
508				}),
509			);
510		}
511
512		while let Some((server_name, result)) = join_map.join_next().await {
513			let result = result.unrecoverable_error("Failed to join spawned task")?;
514
515			// Check if the request timed-out
516			let Ok(federation_response) = result else {
517				response.failures.insert(
518					server_name.to_string(),
519					json!({
520						"reason": format!("Server {server_name} failed to respond before the timeout")
521					}),
522				);
523
524				continue;
525			};
526
527			let federation_response =
528				federation_response.unrecoverable_error("Fetching foreign keys failed")?;
529
530			match federation_response {
531				FederatedOTKResponse::Success(mut federated_otks) => {
532					response.one_time_keys.append(&mut federated_otks.one_time_keys);
533				}
534				FederatedOTKResponse::Failure(failure) => {
535					response.failures.insert(
536						failure.server_name.to_string(),
537						json!({
538							"reason": failure.reason
539						}),
540					);
541				}
542			}
543		}
544	}
545
546	Ok(response.into())
547}
548
549/// Component implementing endpoints for user keys
550#[derive(Debug)]
551pub struct UserKeysComponent;
552
553impl Component for UserKeysComponent {
554	fn router() -> Router<ServerState> {
555		Router::new()
556			.route("/_matrix/client/v3/keys/upload", post(upload_keys_handler))
557			.route("/_matrix/client/v3/keys/query", post(get_keys_handler))
558			.route("/_matrix/client/v3/keys/claim", post(claim_keys_handler))
559	}
560}