Skip to main content

tuwunel_service/users/
dehydrated_device.rs

1use ruma::{
2	DeviceId, OwnedDeviceId, UserId,
3	api::client::dehydrated_device::{
4		DehydratedDeviceData, put_dehydrated_device::unstable::Request,
5	},
6	serde::Raw,
7};
8use serde::{Deserialize, Serialize};
9use tuwunel_core::{Err, Result, implement, trace};
10use tuwunel_database::{Deserialized, Json};
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct DehydratedDevice {
14	/// Unique ID of the device.
15	pub device_id: OwnedDeviceId,
16
17	/// Contains serialized and encrypted private data.
18	pub device_data: Raw<DehydratedDeviceData>,
19}
20
21/// Creates or recreates the user's dehydrated device.
22#[implement(super::Service)]
23#[tracing::instrument(
24	level = "info",
25	skip_all,
26	fields(
27		%user_id,
28		device_id = %request.device_id,
29		display_name = ?request.initial_device_display_name,
30	)
31)]
32pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> Result {
33	assert!(
34		self.exists(user_id).await,
35		"Tried to create dehydrated device for non-existent user"
36	);
37
38	let existing_id = self.get_dehydrated_device_id(user_id).await;
39
40	if existing_id.is_err()
41		&& self
42			.device_exists(user_id, &request.device_id)
43			.await
44	{
45		return Err!("A hydrated device already exists with that ID.");
46	}
47
48	if let Ok(existing_id) = existing_id {
49		self.remove_device(user_id, &existing_id).await;
50	}
51
52	self.create_device(
53		user_id,
54		Some(&request.device_id),
55		(None, None),
56		None,
57		request.initial_device_display_name.as_deref(),
58		None,
59	)
60	.await?;
61
62	trace!(device_data = ?request.device_data);
63	self.db.userid_dehydrateddevice.raw_put(
64		user_id,
65		Json(&DehydratedDevice {
66			device_id: request.device_id.clone(),
67			device_data: request.device_data,
68		}),
69	);
70
71	trace!(device_keys = ?request.device_keys);
72	self.add_device_keys(user_id, &request.device_id, &request.device_keys)
73		.await;
74
75	trace!(one_time_keys = ?request.one_time_keys);
76	self.add_one_time_keys(
77		user_id,
78		&request.device_id,
79		request
80			.one_time_keys
81			.iter()
82			.map(|(id, key)| (id.as_ref(), key)),
83	)
84	.await?;
85
86	Ok(())
87}
88
89/// Removes a user's dehydrated device.
90///
91/// Calling this directly will remove the dehydrated data but leak the frontage
92/// device. Thus this is called by the regular device interface such that the
93/// dehydrated data will not leak instead.
94///
95/// If device_id is given, the user's dehydrated device must match or this is a
96/// no-op, but an Err is still returned to indicate that. Otherwise returns the
97/// removed dehydrated device_id.
98#[implement(super::Service)]
99#[tracing::instrument(
100	level = "debug",
101	skip_all,
102	fields(
103		%user_id,
104		device_id = ?maybe_device_id,
105	)
106)]
107pub(super) async fn remove_dehydrated_device(
108	&self,
109	user_id: &UserId,
110	maybe_device_id: Option<&DeviceId>,
111) -> Result<OwnedDeviceId> {
112	let Ok(device_id) = self.get_dehydrated_device_id(user_id).await else {
113		return Err!(Request(NotFound("No dehydrated device for this user.")));
114	};
115
116	if let Some(maybe_device_id) = maybe_device_id
117		&& maybe_device_id != device_id
118	{
119		return Err!(Request(NotFound("Not the user's dehydrated device.")));
120	}
121
122	self.db.userid_dehydrateddevice.remove(user_id);
123
124	Ok(device_id)
125}
126
127/// Get the device_id of the user's dehydrated device.
128#[implement(super::Service)]
129#[tracing::instrument(
130	level = "debug",
131	skip_all,
132	fields(%user_id)
133)]
134pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result<OwnedDeviceId> {
135	self.get_dehydrated_device(user_id)
136		.await
137		.map(|device| device.device_id)
138}
139
140/// Get the dehydrated device private data
141#[implement(super::Service)]
142#[tracing::instrument(
143	level = "debug",
144	skip_all,
145	fields(%user_id),
146	ret,
147)]
148pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
149	self.db
150		.userid_dehydrateddevice
151		.get(user_id)
152		.await
153		.deserialized::<String>()
154		.and_then(|raw| serde_json::from_str(&raw).map_err(Into::into))
155}