1mod 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#[derive(Debug)]
27pub enum UserKeyRow {
28 CrossSigningKey(CrossSigningKeyRow),
30 DeviceKeys(DeviceKeysRow),
32}
33
34#[derive(Debug)]
36pub struct CrossSigningKeyRow {
37 pub nid: i64,
39 pub key: CrossSigningKey,
41}
42
43#[derive(Debug)]
45pub struct DeviceKeysRow {
46 pub nid: i64,
48 pub key: DeviceKeys,
50}
51
52#[derive(Debug)]
54pub struct ChangedUserKeys {
55 pub stream_id: StreamId,
57 pub user_id: OwnedUserId,
59}
60
61#[derive(Debug, Clone)]
65pub struct UserKeyRef {
66 pub nid: i64,
68 pub key_type: DbKeyType,
70}
71
72fn 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#[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#[rust_analyzer::prefer_underscore_import]
98pub trait UserKeysQueries: Database {
99 #[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 format!("ed25519:{}", device_id),
160 key_object,
161 )
162 .fetch_one(self.connection().await?)
163 .await?)
164 }
165
166 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 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 #[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 #[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 #[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 {}