Skip to main content

tuwunel_api_client/keys/
claim_keys.rs

1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::{StreamExt, future::join};
5use ruma::{
6	OneTimeKeyAlgorithm, OwnedDeviceId, OwnedOneTimeKeyId, OwnedUserId, ServerName, UserId,
7	api::{
8		client::keys::claim_keys,
9		federation::keys::claim_keys::v1::{
10			Request as FederationRequest, Response as FederationResponse,
11		},
12	},
13	encryption::OneTimeKey,
14	serde::Raw,
15};
16use tuwunel_core::{
17	Result,
18	utils::{IterStream, stream::BroadbandExt},
19};
20use tuwunel_service::{
21	Services,
22	federation::feds::{OutcomeExt, fanout_with},
23};
24
25use super::{FailureMap, federation_failures, federation_opts};
26use crate::Ruma;
27
28#[derive(Default)]
29struct Claims {
30	one_time_keys: OneTimeKeyMap,
31	failures: FailureMap,
32}
33
34type RequestClaims = BTreeMap<OwnedUserId, Algorithms>;
35type ServerClaims<'a> = BTreeMap<&'a ServerName, RequestClaims>;
36type LocalClaim<'a> = (&'a UserId, &'a Algorithms);
37type Algorithms = BTreeMap<OwnedDeviceId, OneTimeKeyAlgorithm>;
38type OneTimeKeys = BTreeMap<OwnedOneTimeKeyId, Raw<OneTimeKey>>;
39type OneTimeKeyMap = BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, OneTimeKeys>>;
40
41/// # `POST /_matrix/client/r0/keys/claim`
42///
43/// Claims one-time keys
44pub(crate) async fn claim_keys_route(
45	State(services): State<crate::State>,
46	body: Ruma<claim_keys::v3::Request>,
47) -> Result<claim_keys::v3::Response> {
48	claim_keys_helper(&services, &body.one_time_keys).await
49}
50
51/// Claims one-time keys for the requested users and devices.
52///
53/// Local claims and federated claims are combined into one response.
54pub async fn claim_keys_helper(
55	services: &Services,
56	one_time_keys_input: &RequestClaims,
57) -> Result<claim_keys::v3::Response> {
58	let (local_users, remote_users): (Vec<_>, Vec<_>) = one_time_keys_input
59		.iter()
60		.map(|(uid, map)| (uid.as_ref(), map))
61		.partition(|(user_id, _)| services.globals.user_is_local(user_id));
62
63	let server: ServerClaims<'_> =
64		remote_users
65			.into_iter()
66			.fold(BTreeMap::new(), |mut acc, (user_id, map)| {
67				acc.entry(user_id.server_name())
68					.or_default()
69					.insert(user_id.to_owned(), map.clone());
70				acc
71			});
72
73	let local = collect_local_one_time_keys(services, &local_users);
74	let federation = collect_federation_one_time_keys(services, server);
75
76	let (local, federation) = join(local, federation).await;
77	let merged = local.merge(federation);
78
79	Ok(claim_keys::v3::Response {
80		failures: merged.failures,
81		one_time_keys: merged.one_time_keys,
82	})
83}
84
85async fn collect_local_one_time_keys(services: &Services, users: &[LocalClaim<'_>]) -> Claims {
86	let take_one_time_key = async |(user_id, device_id, algorithm)| {
87		let key = services
88			.users
89			.take_one_time_key(user_id, device_id, algorithm)
90			.await
91			.ok();
92
93		// MSC2732: serve the fallback key when the one-time pool is empty.
94		let key = match key {
95			| Some(key) => Some(key),
96			| None => services
97				.users
98				.take_fallback_key(user_id, device_id, algorithm)
99				.await
100				.ok(),
101		};
102
103		key.map(|key| (device_id.to_owned(), [key].into()))
104	};
105
106	let one_time_keys = users
107		.iter()
108		.copied()
109		.stream()
110		.broad_filter_map(async |(user_id, requested)| {
111			let device_keys: BTreeMap<_, _> = requested
112				.iter()
113				.stream()
114				.map(|(device_id, algorithm)| (user_id, device_id.as_ref(), algorithm))
115				.filter_map(take_one_time_key)
116				.collect()
117				.await;
118
119			// Omit a depleted user entirely; Synapse returns no entry, not an empty map.
120			(!device_keys.is_empty()).then(|| (user_id.to_owned(), device_keys))
121		})
122		.collect()
123		.await;
124
125	Claims { one_time_keys, ..Default::default() }
126}
127
128async fn collect_federation_one_time_keys(
129	services: &Services,
130	server: ServerClaims<'_>,
131) -> Claims {
132	let requests = server
133		.into_iter()
134		.stream()
135		.map(|(server, one_time_keys)| (server.to_owned(), FederationRequest { one_time_keys }));
136
137	let outcomes = fanout_with(
138		requests,
139		async |server, request| {
140			let response = services
141				.federation
142				.execute_keys(&server, request)
143				.await?;
144
145			Ok(retain_origin_users(&server, response))
146		},
147		federation_opts(services),
148	);
149
150	let (claims, faults) = outcomes
151		.merge(Claims::default(), |mut claims, response: FederationResponse| {
152			claims
153				.one_time_keys
154				.extend(response.one_time_keys);
155
156			claims
157		})
158		.await;
159
160	Claims {
161		failures: federation_failures("claim_keys", faults).collect(),
162		..claims
163	}
164}
165
166/// Drops the one-time keys of users who do not belong to the server that
167/// answered.
168///
169/// A server is only authoritative for its own users; any other entry, local
170/// users included, would otherwise replace that user's keys in the response.
171fn retain_origin_users(
172	origin: &ServerName,
173	mut response: FederationResponse,
174) -> FederationResponse {
175	response
176		.one_time_keys
177		.retain(|user, _| user.server_name().eq(origin));
178
179	response
180}
181
182impl Claims {
183	fn merge(mut self, other: Self) -> Self {
184		self.one_time_keys.extend(other.one_time_keys);
185		self.failures.extend(other.failures);
186		self
187	}
188}
189
190#[cfg(test)]
191mod tests {
192	use ruma::{server_name, user_id};
193
194	use super::*;
195
196	#[test]
197	fn drops_one_time_keys_of_users_on_other_servers() {
198		let remote = user_id!("@bob:remote.example");
199		let users = [remote, user_id!("@alice:local.example"), user_id!("@carol:third.example")];
200		let one_time_keys = users.map(|user| (user.to_owned(), BTreeMap::new()));
201		let response = FederationResponse::new(one_time_keys.into());
202		let response = retain_origin_users(server_name!("remote.example"), response);
203
204		assert!(response.one_time_keys.keys().eq([remote]));
205	}
206}