use ruma::{ DeviceId, OwnedDeviceId, UserId, api::client::dehydrated_device::{ DehydratedDeviceData, put_dehydrated_device::unstable::Request, }, serde::Raw, }; use serde::{Deserialize, Serialize}; use tuwunel_core::{Err, Result, implement, trace}; use tuwunel_database::{Deserialized, Json}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DehydratedDevice { /// Unique ID of the device. pub device_id: OwnedDeviceId, /// Contains serialized and encrypted private data. pub device_data: Raw, } /// Creates or recreates the user's dehydrated device. #[implement(super::Service)] #[tracing::instrument( level = "info", skip_all, fields( %user_id, device_id = %request.device_id, display_name = ?request.initial_device_display_name, ) )] pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> Result { assert!( self.exists(user_id).await, "Tried to create dehydrated device for non-existent user" ); let existing_id = self.get_dehydrated_device_id(user_id).await; if existing_id.is_err() && self .device_exists(user_id, &request.device_id) .await { return Err!("A hydrated device already exists with that ID."); } if let Ok(existing_id) = existing_id { self.remove_device(user_id, &existing_id).await; } self.create_device( user_id, Some(&request.device_id), (None, None), None, request.initial_device_display_name.as_deref(), None, ) .await?; trace!(device_data = ?request.device_data); self.db.userid_dehydrateddevice.raw_put( user_id, Json(&DehydratedDevice { device_id: request.device_id.clone(), device_data: request.device_data, }), ); trace!(device_keys = ?request.device_keys); self.add_device_keys(user_id, &request.device_id, &request.device_keys) .await; trace!(one_time_keys = ?request.one_time_keys); self.add_one_time_keys( user_id, &request.device_id, request .one_time_keys .iter() .map(|(id, key)| (id.as_ref(), key)), ) .await?; Ok(()) } /// Removes a user's dehydrated device. /// /// Calling this directly will remove the dehydrated data but leak the frontage /// device. Thus this is called by the regular device interface such that the /// dehydrated data will not leak instead. /// /// If device_id is given, the user's dehydrated device must match or this is a /// no-op, but an Err is still returned to indicate that. Otherwise returns the /// removed dehydrated device_id. #[implement(super::Service)] #[tracing::instrument( level = "debug", skip_all, fields( %user_id, device_id = ?maybe_device_id, ) )] pub(super) async fn remove_dehydrated_device( &self, user_id: &UserId, maybe_device_id: Option<&DeviceId>, ) -> Result { let Ok(device_id) = self.get_dehydrated_device_id(user_id).await else { return Err!(Request(NotFound("No dehydrated device for this user."))); }; if let Some(maybe_device_id) = maybe_device_id && maybe_device_id != device_id { return Err!(Request(NotFound("Not the user's dehydrated device."))); } self.db.userid_dehydrateddevice.remove(user_id); Ok(device_id) } /// Get the device_id of the user's dehydrated device. #[implement(super::Service)] #[tracing::instrument( level = "debug", skip_all, fields(%user_id) )] pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result { self.get_dehydrated_device(user_id) .await .map(|device| device.device_id) } /// Get the dehydrated device private data #[implement(super::Service)] #[tracing::instrument( level = "debug", skip_all, fields(%user_id), ret, )] pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result { self.db .userid_dehydrateddevice .get(user_id) .await .deserialized::() .and_then(|raw| serde_json::from_str(&raw).map_err(Into::into)) }