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	let device_id = self
53		.create_device(
54			user_id,
55			Some(&request.device_id),
56			(None, None),
57			None,
58			request.initial_device_display_name.as_deref(),
59			None,
60		)
61		.await?;
62
63	trace!(device_data = ?request.device_data);
64	self.db.userid_dehydrateddevice.raw_put(
65		user_id,
66		Json(&DehydratedDevice {
67			device_id: device_id.clone(),
68			device_data: request.device_data,
69		}),
70	);
71
72	trace!(device_keys = ?request.device_keys);
73	self.add_device_keys(user_id, &device_id, &request.device_keys)
74		.await;
75
76	trace!(one_time_keys = ?request.one_time_keys);
77	self.add_one_time_keys(
78		user_id,
79		&device_id,
80		&request.one_time_keys,
81		request.one_time_keys.len(),
82	)
83	.await?;
84
85	// MSC3814: dehydrated device MUST be cross-signed and have a fallback key.
86	trace!(fallback_keys = ?request.fallback_keys);
87	self.add_fallback_keys(
88		user_id,
89		&device_id,
90		request
91			.fallback_keys
92			.iter()
93			.map(|(id, key)| (id.as_ref(), key)),
94	)
95	.await?;
96
97	Ok(())
98}
99
100/// Removes a user's dehydrated device.
101///
102/// Calling this directly will remove the dehydrated data but leak the frontage
103/// device. Thus this is called by the regular device interface such that the
104/// dehydrated data will not leak instead.
105///
106/// If device_id is given, the user's dehydrated device must match or this is a
107/// no-op, but an Err is still returned to indicate that. Otherwise returns the
108/// removed dehydrated device_id.
109#[implement(super::Service)]
110#[tracing::instrument(
111	level = "debug",
112	skip_all,
113	fields(
114		%user_id,
115		device_id = ?maybe_device_id,
116	)
117)]
118pub(super) async fn remove_dehydrated_device(
119	&self,
120	user_id: &UserId,
121	maybe_device_id: Option<&DeviceId>,
122) -> Result<OwnedDeviceId> {
123	let Ok(device_id) = self.get_dehydrated_device_id(user_id).await else {
124		return Err!(Request(NotFound("No dehydrated device for this user.")));
125	};
126
127	if let Some(maybe_device_id) = maybe_device_id
128		&& maybe_device_id != device_id
129	{
130		return Err!(Request(NotFound("Not the user's dehydrated device.")));
131	}
132
133	self.db.userid_dehydrateddevice.remove(user_id);
134
135	Ok(device_id)
136}
137
138/// Get the device_id of the user's dehydrated device.
139#[implement(super::Service)]
140#[tracing::instrument(
141	level = "debug",
142	skip_all,
143	fields(%user_id)
144)]
145pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result<OwnedDeviceId> {
146	self.get_dehydrated_device(user_id)
147		.await
148		.map(|device| device.device_id)
149}
150
151/// Get the dehydrated device private data
152#[implement(super::Service)]
153#[tracing::instrument(
154	level = "debug",
155	skip_all,
156	fields(%user_id),
157	ret,
158)]
159pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
160	self.db
161		.userid_dehydrateddevice
162		.get(user_id)
163		.await
164		.deserialized::<String>()
165		.and_then(|raw| serde_json::from_str(&raw).map_err(Into::into))
166}