Skip to main content

arminius_lib_db_keys/
one_time_keys.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Queries related to the `one_time_keys` table.
6
7mod tests;
8
9use std::collections::{BTreeMap, HashMap};
10
11use arminius_lib_db::{Database, DatabaseError, ResultExt as _};
12use arminius_lib_model::user_keys::{OneTimeKeyMap, OneTimeKeyRow};
13use ruma::{
14	DeviceId, KeyId, OneTimeKeyAlgorithm, OneTimeKeyName, OwnedDeviceId, OwnedOneTimeKeyId,
15	OwnedOneTimeKeyName, OwnedUserId, UInt, UserId, encryption::OneTimeKey,
16};
17
18/// Queries related to the `one_time_keys` table.
19// Queries are usually used as extension traits so suggest `import as _`.
20#[rust_analyzer::prefer_underscore_import]
21pub trait OneTimeKeysQueries: Database {
22	/// Tries inserting one-time keys for a user into the database.
23	///
24	/// NOTE: If the given key already exists, does nothing.
25	#[tracing::instrument(skip(self))]
26	async fn try_insert_one_time_key(
27		&mut self,
28		key_algorithm: &OneTimeKeyAlgorithm,
29		key_name: &OneTimeKeyName,
30		user_id: &UserId,
31		device_id: &DeviceId,
32		key: &OneTimeKey,
33	) -> Result<(), DatabaseError> {
34		let key_object =
35			serde_json::to_value(key).invalid_data("Failed to serialize one-time key")?;
36		sqlx::query!(
37			"INSERT INTO one_time_keys (key_algorithm, key_name, device_nid, key_object)
38				VALUES (
39					$1,
40					$2,
41					(
42						SELECT devices.nid
43						FROM devices
44						JOIN users ON devices.user_nid = users.nid
45						WHERE user_id = $3 AND device_id = $4
46					),
47					$5)
48				ON CONFLICT DO NOTHING",
49			key_algorithm.as_str(),
50			key_name.as_str(),
51			user_id.as_str(),
52			device_id.as_str(),
53			key_object,
54		)
55		.execute(self.connection().await?)
56		.await?;
57
58		Ok(())
59	}
60
61	/// Idempotently inserts a one-time key for import purposes.
62	///
63	/// Returns `true` if a new row was inserted, `false` if it already existed
64	/// (i.e. the `ON CONFLICT DO NOTHING` branch fired). This makes imports
65	/// safe to resume and replay.
66	#[tracing::instrument(skip(self))]
67	async fn import_one_time_key(
68		&mut self,
69		key_algorithm: &OneTimeKeyAlgorithm,
70		key_name: &OneTimeKeyName,
71		user_id: &UserId,
72		device_id: &DeviceId,
73		key: &OneTimeKey,
74	) -> Result<bool, DatabaseError> {
75		let key_object =
76			serde_json::to_value(key).invalid_data("Failed to serialize one-time key")?;
77		let result = sqlx::query!(
78			r#"
79			INSERT INTO one_time_keys (key_algorithm, key_name, device_nid, key_object)
80			SELECT
81				$1,
82				$2,
83				devices.nid,
84				$5
85			FROM
86				devices
87				JOIN users ON devices.user_nid = users.nid
88			WHERE
89				user_id = $3
90				AND device_id = $4
91			ON CONFLICT
92				DO NOTHING
93			"#,
94			key_algorithm.as_str(),
95			key_name.as_str(),
96			user_id.as_str(),
97			device_id.as_str(),
98			key_object,
99		)
100		.execute(self.connection().await?)
101		.await?;
102		Ok(result.rows_affected() > 0)
103	}
104
105	/// Given a [`UserId`], returns the counts of OTKs per algorithm they
106	/// own.
107	#[tracing::instrument(skip(self))]
108	async fn fetch_one_time_keys_counts(
109		&mut self,
110		user_id: &UserId,
111		device_id: &DeviceId,
112	) -> Result<BTreeMap<OneTimeKeyAlgorithm, UInt>, DatabaseError> {
113		Ok(sqlx::query!(
114			"SELECT
115				key_algorithm,
116				COUNT(*) AS count
117			 FROM
118			 	one_time_keys
119				JOIN devices ON one_time_keys.device_nid = devices.nid
120				JOIN users ON devices.user_nid = users.nid
121			 WHERE
122			 	user_id = $1
123			 	AND device_id = $2
124			 GROUP BY
125				key_algorithm
126		",
127			user_id.as_str(),
128			device_id.as_str(),
129		)
130		.fetch_all(self.connection().await?)
131		.await?
132		.into_iter()
133		.map(|row| {
134			(
135				OneTimeKeyAlgorithm::from(row.key_algorithm),
136				// SAFETY: `UInt::try_from(0)` should never fail.
137				UInt::try_from(row.count.unwrap_or(0)).unwrap_or(UInt::MAX),
138			)
139		})
140		.collect())
141	}
142
143	/// Fetch **one** one-time key for each requested user's device and
144	/// algorithm.
145	#[tracing::instrument(skip(self))]
146	async fn fetch_one_time_keys(
147		&mut self,
148		one_time_keys: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, OneTimeKeyAlgorithm>>,
149	) -> Result<OneTimeKeyMap, DatabaseError> {
150		let mut users_ids = Vec::new();
151		let mut devices_ids = Vec::new();
152		let mut otks_algorithms = Vec::new();
153
154		for (user_id, devices) in one_time_keys {
155			for (device_id, otk_algorithm) in devices {
156				users_ids.push(user_id.to_string());
157				devices_ids.push(device_id.to_string());
158				otks_algorithms.push(otk_algorithm.to_string());
159			}
160		}
161
162		sqlx::query!(
163			"SELECT
164				otks.nid,
165				users.user_id,
166				devices.device_id,
167				otks.key_algorithm,
168				otks.key_name,
169				otks.key_object
170			FROM
171				users
172				JOIN devices ON devices.user_nid = users.nid
173				CROSS JOIN LATERAL (
174					SELECT
175						nid, key_algorithm, key_name, key_object
176					FROM
177						one_time_keys
178					WHERE
179						one_time_keys.device_nid = devices.nid
180						AND key_algorithm = ANY($3::text[])
181					ORDER BY
182						upload_revision ASC
183					LIMIT 1
184				) AS otks
185			WHERE
186				(user_id, device_id) IN
187				(SELECT * FROM UNNEST($1::text[], $2::text[]))
188				",
189			&users_ids,
190			&devices_ids,
191			&otks_algorithms
192		)
193		.fetch_all(self.connection().await?)
194		.await?
195		.into_iter()
196		.try_fold(
197			HashMap::new(),
198			|mut result: HashMap<
199				OwnedUserId,
200				HashMap<OwnedDeviceId, HashMap<OwnedOneTimeKeyId, OneTimeKeyRow>>,
201			>,
202			 row| {
203				let user_id = row.user_id.parse().invalid_data("Invalid user ID in database")?;
204				let key_id = KeyId::from_parts(
205					OneTimeKeyAlgorithm::from(row.key_algorithm),
206					OwnedOneTimeKeyName::from(row.key_name).as_ref(),
207				);
208				let key = serde_json::from_value(row.key_object)
209					.invalid_data("Invalid one-time key in database")?;
210
211				result
212					.entry(user_id)
213					.or_default()
214					.entry(row.device_id.into())
215					.or_default()
216					.entry(key_id)
217					.or_insert(OneTimeKeyRow { nid: row.nid, key, used: None });
218
219				Ok(result)
220			},
221		)
222	}
223
224	/// Deletes a one-time key from the database by its ID.
225	///
226	/// Returns the number of affected (e.g deleted) rows.
227	#[tracing::instrument(skip(self))]
228	async fn delete_one_time_keys(&mut self, nids: &[i32]) -> Result<(), DatabaseError> {
229		if sqlx::query!("DELETE FROM one_time_keys WHERE nid = ANY($1::int[])", nids)
230			.execute(self.connection().await?)
231			.await?
232			.rows_affected() as usize
233			!= nids.len()
234		{
235			return Err(DatabaseError::SqlxError(sqlx::Error::RowNotFound));
236		}
237
238		Ok(())
239	}
240}
241
242impl<T> OneTimeKeysQueries for T where T: Database {}