Skip to main content

arminius_lib_db_keys/
user_keys.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Queries related to the `user_keys` table.
6//!
7//! This table contains both device keys and cross-signing keys.
8
9mod tests;
10
11use std::collections::{BTreeMap, HashMap};
12
13use arminius_lib_db::{Database, DatabaseError, ResultExt as _};
14use arminius_lib_model::{types::StreamId, user_keys::DbKeyType};
15use ruma::{
16	CrossSigningOrDeviceSigningKeyId, DeviceId, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
17	UInt, UserId,
18	encryption::{CrossSigningKey, DeviceKeys},
19};
20
21/// Entry in the `user_keys` table.
22///
23/// Since each of the keys can either be device keys, or cross-signing keys,
24/// which have different structures, we use this enum to return records that are
25/// either one of the two.
26#[derive(Debug)]
27pub enum UserKeyRow {
28	/// Contains cross-signing keys, see [`CrossSigningKeyRow`].
29	CrossSigningKey(CrossSigningKeyRow),
30	/// Contains device keys, see [`DeviceKeysRow`].
31	DeviceKeys(DeviceKeysRow),
32}
33
34/// [`CrossSigningKey`] from the database, along their `NID`.
35#[derive(Debug)]
36pub struct CrossSigningKeyRow {
37	/// The surrogate key of the keys object.
38	pub nid: i64,
39	/// The actual cross-signing keys.
40	pub key: CrossSigningKey,
41}
42
43/// [`DeviceKeys`] from the database, along their `NID`.
44#[derive(Debug)]
45pub struct DeviceKeysRow {
46	/// The surrogate key of the keys object.
47	pub nid: i64,
48	/// The actual device keys.
49	pub key: DeviceKeys,
50}
51
52/// A user who had their keys changed since the last sync request.
53#[derive(Debug)]
54pub struct ChangedUserKeys {
55	/// The stream ID of the changed user key. Defaults to 0.
56	pub stream_id: StreamId,
57	/// The Matrix user ID of that user.
58	pub user_id: OwnedUserId,
59}
60
61/// A lightweight reference to a `user_keys` row, carrying only the fields
62/// needed to resolve key type and identity during a cross-signing signature
63/// import.
64#[derive(Debug, Clone)]
65pub struct UserKeyRef {
66	/// The surrogate key of the `user_keys` row.
67	pub nid: i64,
68	/// The key type of the `user_keys` row.
69	pub key_type: DbKeyType,
70}
71
72/// Extracts the single key id from a cross-signing key's `keys` map, to be
73/// stored in the `user_keys.key_id` column.
74///
75/// A well-formed cross-signing key has exactly one entry in `keys` (per the
76/// Matrix spec, the object "must have exactly one property"); anything else is
77/// rejected as invalid data rather than silently picking one.
78fn single_cross_signing_key_id(key: &CrossSigningKey) -> Result<&str, DatabaseError> {
79	let mut ids = key.keys.keys();
80	match (ids.next(), ids.next()) {
81		(Some(id), None) => Ok(id.as_str()),
82		_ => Err(DatabaseError::InvalidData(format!(
83			"cross-signing key must have exactly one entry in its 'keys' map, got {}",
84			key.keys.len()
85		))),
86	}
87}
88
89/// Queries related to the `user_keys` table.
90#[expect(
91	trivial_casts,
92	reason = "The casts in `sqlx` queries are not regular rust casts, but used to infer custom types
93	from SQL queries. See
94	<https://docs.rs/sqlx/latest/sqlx/macro.query.html#type-overrides-bind-parameters-postgres-only>"
95)]
96// Queries are usually used as extension traits so suggest `import as _`.
97#[rust_analyzer::prefer_underscore_import]
98pub trait UserKeysQueries: Database {
99	/// Inserts device keys for a given user and device, and returns its NID.
100	///
101	/// # Conflict resolution
102	///
103	/// If the given `device_id` is already present in the table, its row gets
104	/// updated with the new `key_object` value **if and only if** the new
105	/// one is different.
106	#[tracing::instrument(skip(self))]
107	async fn upsert_device_keys(
108		&mut self,
109		user_id: &UserId,
110		device_id: &DeviceId,
111		key: &DeviceKeys,
112	) -> Result<i64, DatabaseError> {
113		let key_object =
114			serde_json::to_value(key).invalid_data("Failed to serialize device keys")?;
115		Ok(sqlx::query_scalar!(
116			r#"
117			WITH
118			the_user AS (
119				SELECT nid FROM users WHERE user_id = $1
120			),
121			the_device AS (
122				SELECT
123					devices.nid
124					FROM devices
125					WHERE
126						devices.user_nid = (SELECT nid FROM the_user)
127						AND devices.device_id = $2
128			),
129			upserted AS (
130				INSERT INTO user_keys (key_type, user_nid, device_nid, key_id, key_object)
131					 VALUES (
132						'device',
133						(SELECT nid FROM the_user),
134						(SELECT nid FROM the_device),
135						$3,
136						$4
137					)
138					ON CONFLICT(key_type, user_nid, device_nid) DO UPDATE
139					SET
140						key_object = EXCLUDED.key_object,
141						stream_id = EXCLUDED.stream_id
142					WHERE
143						user_keys.key_object IS DISTINCT FROM EXCLUDED.key_object
144					RETURNING nid
145			)
146			SELECT nid AS "nid!" FROM upserted
147			UNION ALL
148			SELECT nid AS "nid!"
149				FROM user_keys
150				WHERE
151					NOT EXISTS (SELECT 1 FROM upserted)
152					AND key_type = 'device'
153					AND user_keys.user_nid = (SELECT nid FROM the_user)
154					AND user_keys.device_nid = (SELECT nid FROM the_device)
155			"#,
156			user_id.as_str(),
157			device_id.as_str(),
158			// See <https://spec.matrix.org/v1.16/client-server-api/#key-and-signature-security>
159			format!("ed25519:{}", device_id),
160			key_object,
161		)
162		.fetch_one(self.connection().await?)
163		.await?)
164	}
165
166	/// Imports a cross-signing key for a given user idempotently.
167	///
168	/// Returns true if a new cross-signing key was inserted, false if one
169	/// already exists
170	#[tracing::instrument(skip(self))]
171	async fn import_cross_signing_key(
172		&mut self,
173		user_id: &UserId,
174		key_type: DbKeyType,
175		key: &CrossSigningKey,
176	) -> Result<bool, DatabaseError> {
177		let key_id = single_cross_signing_key_id(key)?;
178		let key_object =
179			serde_json::to_value(key).invalid_data("Failed to serialize cross-signing key")?;
180		let result = sqlx::query!(
181			r#"
182			INSERT INTO user_keys (key_type, user_nid, device_nid, key_id, key_object)
183			SELECT
184				$1,
185				u.nid,
186				NULL,
187				$3,
188				$4
189			FROM
190				users u
191			WHERE
192				u.user_id = $2
193			ON CONFLICT (key_type,
194				user_nid,
195				device_nid)
196				DO NOTHING
197			"#,
198			key_type as DbKeyType,
199			user_id.as_str(),
200			key_id,
201			key_object,
202		)
203		.execute(self.connection().await?)
204		.await?;
205		Ok(result.rows_affected() > 0)
206	}
207
208	/// Resolves a `user_keys` row by user id and key id, returning a
209	/// [`UserKeyRef`] if found, or `None` if no matching row exists.
210	///
211	/// The `key_id` is matched against both the stored `key_id` column (device
212	/// keys, stored as `"ed25519:<device_id>"`) and against keys embedded in
213	/// `key_object->'keys'` (cross-signing keys, where the key id is a field
214	/// name inside the JSON object). This dual match handles both device keys
215	/// and cross-signing keys with a single query.
216	///
217	/// Returns `None` when:
218	/// - The user does not exist in the `users` table (e.g. a remote user whose keys were never
219	///   imported).
220	/// - No `user_keys` row matches the given `key_id`.
221	#[tracing::instrument(skip(self))]
222	async fn resolve_user_key(
223		&mut self,
224		user_id: &UserId,
225		key_id: &CrossSigningOrDeviceSigningKeyId,
226	) -> Result<Option<UserKeyRef>, DatabaseError> {
227		let mut rows = sqlx::query!(
228			r#"
229			SELECT
230				user_keys.nid,
231				key_type AS "key_type: DbKeyType"
232			FROM
233				user_keys
234				JOIN users ON user_keys.user_nid = users.nid
235			WHERE
236				user_id = $1
237				AND (key_id = $2
238					OR key_object -> 'keys' ? $2)
239			"#,
240			user_id.as_str(),
241			key_id.as_str(),
242		)
243		.fetch_all(self.connection().await?)
244		.await?;
245		if rows.len() > 1 {
246			return Err(DatabaseError::InvalidData(format!(
247				"key id '{key_id}' for user '{user_id}' is ambiguous: it matches {} user_keys rows",
248				rows.len()
249			)));
250		}
251		Ok(rows.pop().map(|record| UserKeyRef { nid: record.nid, key_type: record.key_type }))
252	}
253
254	/// Resolves a *device* `user_keys` row by user id and **bare device id**
255	/// (the `key_type = 'device'` row whose device has this `device_id`).
256	///
257	/// Cross-signing signatures name their signed target by bare device id,
258	/// which does not match the stored `key_id` (`ed25519:<device_id>`) nor a
259	/// `key_object->'keys'` field, so [`Self::resolve_user_key`] cannot find
260	/// it. This is the device branch of the ADR's signed-key resolution.
261	///
262	/// Returns `None` if the user, device, or its device-keys row is absent.
263	#[tracing::instrument(skip(self))]
264	async fn resolve_device_user_key(
265		&mut self,
266		user_id: &UserId,
267		device_id: &DeviceId,
268	) -> Result<Option<UserKeyRef>, DatabaseError> {
269		let mut rows = sqlx::query!(
270			r#"
271			SELECT
272				user_keys.nid,
273				user_keys.key_type AS "key_type: DbKeyType"
274			FROM
275				user_keys
276				JOIN devices ON user_keys.device_nid = devices.nid
277				JOIN users ON devices.user_nid = users.nid
278			WHERE
279				users.user_id = $1
280				AND user_keys.key_type = 'device'
281				AND devices.device_id = $2
282			"#,
283			user_id.as_str(),
284			device_id.as_str(),
285		)
286		.fetch_all(self.connection().await?)
287		.await?;
288		if rows.len() > 1 {
289			return Err(DatabaseError::InvalidData(format!(
290				"device id '{device_id}' for user '{user_id}' is ambiguous: it matches {} device user_keys rows",
291				rows.len()
292			)));
293		}
294		Ok(rows.pop().map(|record| UserKeyRef { nid: record.nid, key_type: record.key_type }))
295	}
296
297	/// Resolves a *cross-signing* `user_keys` row (`device_nid IS NULL`) by
298	/// user id and the key id a cross-signing signature names its target by.
299	///
300	/// Synapse names a signed cross-signing-key target by its **bare** public
301	/// key (e.g. `"<pubkey>"`), while arminius stores that key id with its
302	/// algorithm prefix (`"ed25519:<pubkey>"`), both in the `key_id` column and
303	/// as the field name inside `key_object->'keys'`.
304	/// [`Self::resolve_user_key`] only matches the exact stored form, so a
305	/// bare target never resolves. This method matches the bare id (by
306	/// prefixing it) as well as the already prefixed form, scoped to
307	/// cross-signing rows so it can never collide with a device key. This is
308	/// the cross-signing-key branch of the ADR's signed-key resolution (the
309	/// device branch is [`Self::resolve_device_user_key`]).
310	///
311	/// Returns `None` if the user has no matching cross-signing key.
312	#[tracing::instrument(skip(self))]
313	async fn resolve_cross_signing_user_key(
314		&mut self,
315		user_id: &UserId,
316		key_id: &str,
317	) -> Result<Option<UserKeyRef>, DatabaseError> {
318		let mut rows = sqlx::query!(
319			r#"
320			SELECT
321				user_keys.nid,
322				key_type AS "key_type: DbKeyType"
323			FROM
324				user_keys
325				JOIN users ON user_keys.user_nid = users.nid
326			WHERE
327				user_id = $1
328				AND device_nid IS NULL
329				AND (key_id = $2
330					OR key_id = 'ed25519:' || $2
331					OR key_object -> 'keys' ? $2
332					OR key_object -> 'keys' ? ('ed25519:' || $2))
333			"#,
334			user_id.as_str(),
335			key_id,
336		)
337		.fetch_all(self.connection().await?)
338		.await?;
339		if rows.len() > 1 {
340			return Err(DatabaseError::InvalidData(format!(
341				"cross-signing key id '{key_id}' for user '{user_id}' is ambiguous: it matches {} user_keys rows",
342				rows.len()
343			)));
344		}
345		Ok(rows.pop().map(|record| UserKeyRef { nid: record.nid, key_type: record.key_type }))
346	}
347
348	/// Imports device keys for a given user and device idempotently.
349	///
350	/// Inserts into `user_keys` with `key_type='device'` and returns `true` if
351	/// a new row was inserted. If a row for the same `(key_type, user_nid,
352	/// device_nid)` already exists, the insert is skipped and `false` is
353	/// returned, making this safe to replay and resume.
354	#[tracing::instrument(skip(self))]
355	async fn import_device_keys(
356		&mut self,
357		user_id: &UserId,
358		device_id: &DeviceId,
359		key: &DeviceKeys,
360	) -> Result<bool, DatabaseError> {
361		let key_object =
362			serde_json::to_value(key).invalid_data("Failed to serialize device keys")?;
363		let key_id = format!("ed25519:{device_id}");
364		let result = sqlx::query!(
365			r#"
366			INSERT INTO user_keys (key_type, user_nid, device_nid, key_id, key_object)
367			SELECT
368				'device',
369				users.nid,
370				devices.nid,
371				$3,
372				$4
373			FROM
374				devices
375				JOIN users ON devices.user_nid = users.nid
376			WHERE
377				user_id = $1
378				AND device_id = $2
379			ON CONFLICT (key_type,
380				user_nid,
381				device_nid)
382				DO NOTHING
383			"#,
384			user_id.as_str(),
385			device_id.as_str(),
386			key_id,
387			key_object,
388		)
389		.execute(self.connection().await?)
390		.await?;
391		Ok(result.rows_affected() > 0)
392	}
393
394	/// Given a list of users, and an optional list of device IDs for each user,
395	/// fetches all of their device keys.
396	///
397	/// NOTE: If the list of device IDs is empty for a user, fetches all of
398	/// their keys (instead of only the ones of that list).
399	#[tracing::instrument(skip(self))]
400	async fn fetch_device_keys_for_users(
401		&mut self,
402		device_keys: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
403	) -> Result<BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, DeviceKeysRow>>, DatabaseError> {
404		// Split the list of users-devices into three lists:
405		// - `specific_users` and `specific_devices` contains the pairs to query together
406		// - `wildcard_users` contains the users for whom to request all devices
407		let (mut specific_users, mut specific_devices, mut wildcard_users) =
408			(Vec::new(), Vec::new(), Vec::new());
409
410		for (user, devices) in device_keys {
411			if devices.is_empty() {
412				wildcard_users.push(user.to_string());
413			} else {
414				for device in devices {
415					specific_users.push(user.to_string());
416					specific_devices.push(device.to_string());
417				}
418			}
419		}
420
421		sqlx::query!(
422			"
423			SELECT
424			  users.user_id,
425			  devices.device_id,
426			  user_keys.nid,
427			  user_keys.key_object
428			FROM
429			  user_keys
430			  JOIN devices ON user_keys.device_nid = devices.nid
431			  JOIN users ON devices.user_nid = users.nid
432			WHERE
433			  user_keys.key_type = 'device'
434			  AND (
435				(
436				  users.user_id, devices.device_id
437				) IN (
438				  SELECT
439					user_id,
440					device_id
441				  FROM
442					UNNEST($1 :: TEXT[], $2 :: TEXT[]) AS pairs(user_id, device_id)
443				)
444				OR users.user_id = ANY($3 :: TEXT[])
445			  )
446			",
447			&specific_users,
448			&specific_devices,
449			&wildcard_users,
450		)
451		.fetch_all(self.connection().await?)
452		.await?
453		.into_iter()
454		.try_fold(BTreeMap::new(), |mut result, record| {
455			let user_id =
456				UserId::parse(record.user_id).invalid_data("Invalid user ID in database")?;
457			let key = serde_json::from_value(record.key_object)
458				.invalid_data("Invalid device keys object in database")?;
459
460			let user_keys: &mut BTreeMap<OwnedDeviceId, DeviceKeysRow> =
461				result.entry(user_id).or_default();
462			user_keys.insert(record.device_id.into(), DeviceKeysRow { nid: record.nid, key });
463
464			Ok(result)
465		})
466	}
467
468	/// Fetches a single cross-signing key from the database.
469	#[tracing::instrument(skip(self))]
470	async fn fetch_single_cross_signing_key_for_user(
471		&mut self,
472		user_id: &UserId,
473		key_type: DbKeyType,
474	) -> Result<Option<CrossSigningKey>, DatabaseError> {
475		sqlx::query!(
476			"SELECT key_object
477					FROM user_keys
478					JOIN users ON user_keys.user_nid = users.nid
479					WHERE users.user_id = $1 AND user_keys.key_type = $2",
480			user_id.to_string(),
481			key_type as DbKeyType
482		)
483		.fetch_optional(self.connection().await?)
484		.await?
485		.map(|row| {
486			serde_json::from_value(row.key_object)
487				.invalid_data("Invalid cross-signing key in database")
488		})
489		.transpose()
490	}
491
492	/// Fetches all the cross-signing keys for the given list of `user_ids`.
493	///
494	/// This query can be performed by both by the client API (when a local user
495	/// requests local keys) and by the Federation Input API (when a foreign
496	/// homeserver requests local keys). Additionally, the only case this query
497	/// should return a User-Signing key is when it is that of a local user
498	/// requesting their own keys. This is what the `requester` parameter is
499	/// used for, and it should only be set in the local-keys-fetching scenario.
500	///
501	/// This returns a map from [`OwnedUserId`] to a map from [`DbKeyType`] to
502	/// [`CrossSigningKey`].
503	#[tracing::instrument(skip(self))]
504	async fn fetch_allowed_cross_signing_keys(
505		&mut self,
506		requester: Option<&UserId>,
507		user_ids: &[OwnedUserId],
508	) -> Result<HashMap<OwnedUserId, HashMap<DbKeyType, CrossSigningKeyRow>>, DatabaseError> {
509		sqlx::query!(
510			r#"
511			SELECT
512				user_id,
513				key_type AS "key_type:DbKeyType",
514				user_keys.nid,
515				key_object
516			FROM
517				user_keys
518				JOIN users ON user_keys.user_nid = users.nid
519			WHERE
520				user_id = ANY ($1::text[])
521				AND (key_type = ANY ('{master,self_signing}')
522					-- Users are only allowed to see their own User-Signing key
523					OR (key_type = 'user_signing'
524						AND user_id = $2))
525			"#,
526			&user_ids.into_iter().map(ToString::to_string).collect::<Vec<_>>(),
527			requester.map(ToString::to_string)
528		)
529		.fetch_all(self.connection().await?)
530		.await?
531		.into_iter()
532		.try_fold(HashMap::new(), |mut result, record| {
533			let user_id =
534				UserId::parse(record.user_id).invalid_data("Invalid user ID in database")?;
535			let key = serde_json::from_value(record.key_object)
536				.invalid_data("Database contains invalid cross-signing key")?;
537
538			let user_keys: &mut HashMap<DbKeyType, CrossSigningKeyRow> =
539				result.entry(user_id).or_default();
540			user_keys.insert(record.key_type, CrossSigningKeyRow { nid: record.nid, key });
541
542			Ok(result)
543		})
544	}
545
546	/// Fetches all of a user's cross-signing keys IDs.
547	#[tracing::instrument(skip(self))]
548	async fn fetch_cross_signing_keys_ids_for_user(
549		&mut self,
550		user_id: &UserId,
551	) -> Result<Vec<String>, DatabaseError> {
552		Ok(sqlx::query_scalar!(
553			r#"SELECT jsonb_object_keys(key_object->'keys') AS "keys!"
554				FROM user_keys
555				JOIN users ON user_keys.user_nid = users.nid
556				WHERE users.user_id = $1 AND key_type != 'device'"#,
557			user_id.to_string()
558		)
559		.fetch_all(self.connection().await?)
560		.await?)
561	}
562
563	/// Inserts the given cross-signing keys.
564	///
565	/// # Conflict resolution
566	///
567	/// If a key already exists for the given key_type+user_id, its row gets
568	/// updated with the new `key_id` and `key_object` if and only if the new
569	/// `key_object` is different than the stored one. If they are the same,
570	/// nothing happens.
571	#[tracing::instrument(skip(self))]
572	async fn upsert_cross_signing_key(
573		&mut self,
574		user_id: &UserId,
575		key_type: DbKeyType,
576		key: &CrossSigningKey,
577	) -> Result<i64, DatabaseError> {
578		let key_id = single_cross_signing_key_id(key)?;
579		let key_object =
580			serde_json::to_value(key).invalid_data("Failed to serialize cross-signing key")?;
581		Ok(sqlx::query!(
582			"INSERT INTO user_keys (key_type, user_nid, key_id, key_object)
583				VALUES (
584					$1,
585					(SELECT nid FROM users WHERE user_id = $2),
586					$3,
587					$4
588				)
589				ON CONFLICT(key_type, user_nid, device_nid)
590				DO UPDATE SET
591					key_object = EXCLUDED.key_object,
592					key_id = EXCLUDED.key_id,
593					stream_id = EXCLUDED.stream_id
594				-- Only upsert if the keys were different
595				WHERE user_keys.key_object IS DISTINCT FROM EXCLUDED.key_object
596        RETURNING nid
597				",
598			// This cast is mandatory to prevent sqlx complaints.
599			key_type as DbKeyType,
600			user_id.as_str(),
601			key_id,
602			key_object
603		)
604		.fetch_one(self.connection().await?)
605		.await?
606		.nid)
607	}
608
609	/// Fetches all the user keys for the given users.
610	///
611	/// If the list of key types is empty, fetches all the types.
612	#[tracing::instrument(skip(self))]
613	async fn fetch_user_keys(
614		&mut self,
615		requested_keys: &[(OwnedUserId, Vec<DbKeyType>)],
616	) -> Result<BTreeMap<OwnedUserId, Vec<UserKeyRow>>, DatabaseError> {
617		// Split the list of requested user keys into three lists:
618		// - `specific_users` and `specific_types` go in pair and they contain the specific key
619		//   types to query
620		// - `wildcard_users` contains the users for whom to request all the keys
621		let (mut specific_users, mut specific_key_types, mut wildcard_users) =
622			(Vec::new(), Vec::new(), Vec::new());
623
624		for (user, key_types) in requested_keys {
625			if key_types.is_empty() {
626				wildcard_users.push(user.to_string());
627			} else {
628				for key_type in key_types {
629					specific_users.push(user.to_string());
630					specific_key_types.push(key_type);
631				}
632			}
633		}
634
635		sqlx::query!(
636			r#"
637			SELECT
638				user_id,
639				key_type AS "key_type:DbKeyType",
640				user_keys.nid,
641				key_object
642			FROM
643				user_keys
644				JOIN users ON user_keys.user_nid = users.nid
645			WHERE ((users.user_id, user_keys.key_type) IN (
646					SELECT
647						user_id,
648						key_type
649					FROM
650						UNNEST($1::text[], $2::KEY_TYPE[])))
651				OR users.user_id = ANY ($3::text[])
652			"#,
653			&specific_users,
654			&specific_key_types as &[&DbKeyType],
655			&wildcard_users
656		)
657		.fetch_all(self.connection().await?)
658		.await?
659		.into_iter()
660		.try_fold(BTreeMap::new(), |mut result, record| {
661			let user_id =
662				UserId::parse(record.user_id).invalid_data("Invalid user ID in database")?;
663
664			let user_keys: &mut Vec<UserKeyRow> = result.entry(user_id).or_default();
665
666			let db_key = match record.key_type {
667				DbKeyType::Device => {
668					let key: DeviceKeys = serde_json::from_value(record.key_object)
669						.invalid_data("Database contains invalid device keys object")?;
670
671					UserKeyRow::DeviceKeys(DeviceKeysRow { nid: record.nid, key })
672				}
673				_ => {
674					let key: CrossSigningKey = serde_json::from_value(record.key_object)
675						.invalid_data("Database contains invalid cross-signing key object")?;
676
677					UserKeyRow::CrossSigningKey(CrossSigningKeyRow { nid: record.nid, key })
678				}
679			};
680			user_keys.push(db_key);
681
682			Ok(result)
683		})
684	}
685
686	/// Returns the list of users that either:
687	/// - Updated their device identity or cross-signing keys after the given `from_keys_stream_id`
688	/// - Joined a room shared with `user_id` after the given `from_event_stream_id`
689	///
690	/// Or our own user name, if we changed our device keys
691	#[tracing::instrument(skip(self))]
692	async fn fetch_sync_user_keys_changes(
693		&mut self,
694		user_id: &UserId,
695		from_event_stream_id: StreamId,
696		from_keys_stream_id: StreamId,
697	) -> Result<Vec<ChangedUserKeys>, DatabaseError> {
698		let mut changed = sqlx::query!(
699			r#"
700			SELECT
701				users_sharing_encrypted_room.user_id AS "user_id!",
702				MAX(GREATEST (devices.stream_id, user_keys.stream_id, local_signatures.stream_id)) AS max_stream_id
703			FROM
704				-- Fetch all users sharing a room with the given user
705				users_sharing_encrypted_room ($1)
706				JOIN devices ON devices.user_nid = users_sharing_encrypted_room.user_nid
707				JOIN user_keys ON users_sharing_encrypted_room.user_nid = user_keys.user_nid
708				JOIN local_signatures ON local_signatures.signed_key_nid = user_keys.nid
709			WHERE
710				-- Filter out rooms that users are no longer members of
711				users_sharing_encrypted_room.membership_state NOT IN ('leave', 'kick', 'ban')
712				-- Either the keys have been updated/uploaded after the given keys stream ID
713				AND ((user_keys.stream_id > $2
714						AND user_keys.stream_id <= max_safe_seq ('user_keys_streamids'))
715					-- Or there were new signatures for that key since the last sync
716					OR (local_signatures.stream_id > $2
717						AND local_signatures.stream_id <= max_safe_seq ('user_keys_streamids'))
718					-- Or the user created a new device since the last sync
719					OR (devices.stream_id > $2
720						AND devices.stream_id <= max_safe_seq ('user_keys_streamids')))
721				-- Or the room has been joined after the given events stream ID
722				OR (users_sharing_encrypted_room.membership_state = 'join'
723					AND users_sharing_encrypted_room.membership_stream_id > $3
724					AND users_sharing_encrypted_room.membership_stream_id <= max_safe_seq ('eventids'))
725			GROUP BY
726				users_sharing_encrypted_room.user_id
727			"#,
728			user_id.to_string(),
729			from_keys_stream_id,
730			from_event_stream_id
731		)
732		.fetch_all(self.connection().await?)
733		.await?
734		.into_iter()
735		.map(|row| -> Result<_, DatabaseError> {
736			let user_id = UserId::parse(row.user_id).invalid_data("Invalid user ID in database")?;
737
738			Ok(ChangedUserKeys { stream_id: row.max_stream_id.unwrap_or(StreamId::MIN), user_id })
739		})
740		.collect::<Result<Vec<_>, _>>()?;
741
742		// And also check if our own user keys changed
743		if let Some(max_stream_id) = sqlx::query_scalar!(
744			r#"
745			 SELECT
746					 MAX(stream_id) AS max_stream_id
747			 FROM (
748					SELECT stream_id FROM devices JOIN users ON devices.user_nid = users.nid WHERE users.user_id = $1
749					UNION ALL
750					SELECT stream_id FROM user_keys JOIN users ON user_keys.user_nid = users.nid WHERE users.user_id = $1
751					UNION ALL
752					SELECT local_signatures.stream_id FROM user_keys JOIN local_signatures ON local_signatures.signed_key_nid = user_keys.nid JOIN users ON user_keys.user_nid = users.nid WHERE users.user_id = $1
753			 ) stream_ids
754			 HAVING MAX(stream_id) > $2
755      "#,
756			user_id.as_str(),
757			from_keys_stream_id
758		)
759		.fetch_optional(self.connection().await?)
760		.await?
761		{
762			changed.push(ChangedUserKeys {
763				stream_id: max_stream_id.unwrap_or(StreamId::MIN),
764				user_id: user_id.to_owned(),
765			});
766		}
767
768		Ok(changed)
769	}
770
771	/// Returns the list of users that no longer share any encrypted rooms since
772	/// the given `from_event_stream_id`.
773	#[tracing::instrument(skip(self))]
774	async fn fetch_sync_user_keys_left(
775		&mut self,
776		user_id: &UserId,
777		from_event_stream_id: StreamId,
778	) -> Result<Vec<OwnedUserId>, DatabaseError> {
779		sqlx::query!(
780			r#"
781			SELECT
782				users_sharing_encrypted_room.user_id AS "user_id!"
783			FROM
784				users_sharing_encrypted_room ($1)
785			WHERE
786				users_sharing_encrypted_room.membership_stream_id > $2
787				AND users_sharing_encrypted_room.membership_stream_id <= max_safe_seq ('eventids')
788			GROUP BY
789				users_sharing_encrypted_room.user_id
790			HAVING
791			-- Filter out users who still share encrypted room(s)
792			COUNT(*) FILTER (WHERE users_sharing_encrypted_room.membership_state NOT IN ('ban', 'leave', 'knock')) = 0
793			"#,
794			user_id.to_string(),
795			from_event_stream_id
796		)
797		.fetch_all(self.connection().await?)
798		.await?
799		.into_iter()
800		.map(|row| UserId::parse(row.user_id).invalid_data("Invalid user ID in database"))
801		.collect::<Result<Vec<_>, _>>()
802	}
803
804	/// Fetches the count of non-used one-time keys for the given user.
805	#[tracing::instrument(skip(self))]
806	async fn fetch_otk_counts(
807		&mut self,
808		user_id: &UserId,
809		device_id: &DeviceId,
810	) -> Result<BTreeMap<OneTimeKeyAlgorithm, UInt>, DatabaseError> {
811		sqlx::query!(
812			r#"
813			SELECT
814				key_algorithm,
815				COUNT(*) AS "count!"
816			FROM
817				one_time_keys
818				JOIN devices ON one_time_keys.device_nid = devices.nid
819			WHERE
820				devices.user_nid = (
821					SELECT
822						nid
823					FROM
824						users
825					WHERE
826						user_id = $1)
827				AND devices.device_id = $2
828			GROUP BY
829				key_algorithm
830			"#,
831			user_id.to_string(),
832			device_id.to_string()
833		)
834		.fetch_all(self.connection().await?)
835		.await?
836		.into_iter()
837		.map(|row| {
838			let count =
839				UInt::try_from(row.count as u64).invalid_data("Invalid one-time key count")?;
840
841			Ok((row.key_algorithm.into(), count))
842		})
843		.collect::<Result<BTreeMap<_, _>, _>>()
844	}
845
846	/// Fetches the highest stream ID ever issued for the `user_keys_streamids` stream.
847	#[tracing::instrument(skip(self))]
848	async fn fetch_current_user_keys_stream_id(&mut self) -> Result<StreamId, DatabaseError> {
849		Ok(sqlx::query_scalar!(
850			r#"
851		SELECT GREATEST(
852			(SELECT MAX(devices.stream_id) FROM devices),
853			(SELECT MAX(user_keys.stream_id) FROM user_keys),
854			(SELECT MAX(local_signatures.stream_id) FROM local_signatures)
855		) as max_stream_id
856		"#
857		)
858		.fetch_one(self.connection().await?)
859		.await?
860		.unwrap_or(0))
861	}
862}
863
864impl<T> UserKeysQueries for T where T: Database {}