1use std::{fmt, path::PathBuf};
4
5use gtk::glib;
6use matrix_sdk::{authentication::oauth::ClientId, Client, SessionMeta, SessionTokens};
7use rand::{
8 distr::{Alphanumeric, SampleString},
9 rng,
10};
11use ruma::{OwnedDeviceId, OwnedUserId};
12use thiserror::Error;
13use tokio::fs;
14use tracing::{debug, error};
15use url::Url;
16use zeroize::Zeroizing;
17
18mod file;
19#[cfg(target_os = "linux")]
20mod linux;
21
22use self::file::SecretFile;
23use crate::{
24 prelude::*,
25 spawn_tokio,
26 utils::{matrix::ClientSetupError, DataType},
27};
28
29pub(crate) const SESSION_ID_LENGTH: usize = 8;
31pub(crate) const PASSPHRASE_LENGTH: usize = 30;
33
34cfg_if::cfg_if! {
35 if #[cfg(target_os = "linux")] {
36 pub(crate) type Secret = linux::LinuxSecret;
38 } else {
39 pub(crate) type Secret = unimplemented::UnimplementedSecret;
41 }
42}
43
44pub(crate) trait SecretExt {
46 async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError>;
48
49 async fn store_session(session: StoredSession) -> Result<(), SecretError>;
52
53 async fn delete_session(session: &StoredSession);
55}
56
57#[cfg(not(target_os = "linux"))]
59mod unimplemented {
60 use super::*;
61
62 #[derive(Debug)]
63 pub(crate) struct UnimplementedSecret;
64
65 impl SecretExt for UnimplementedSecret {
66 async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError> {
67 unimplemented!()
68 }
69
70 async fn store_session(session: StoredSession) -> Result<(), SecretError> {
71 unimplemented!()
72 }
73
74 async fn delete_session(session: &StoredSession) {
75 unimplemented!()
76 }
77 }
78}
79
80#[derive(Debug, Error)]
82pub(crate) enum SecretError {
83 #[error("Service error: {0}")]
85 Service(String),
86}
87
88impl UserFacingError for SecretError {
89 fn to_user_facing(&self) -> String {
90 match self {
91 SecretError::Service(error) => error.clone(),
92 }
93 }
94}
95
96#[derive(Clone, glib::Boxed)]
98#[boxed_type(name = "StoredSession")]
99pub struct StoredSession {
100 pub homeserver: Url,
102 pub user_id: OwnedUserId,
104 pub device_id: OwnedDeviceId,
106 pub id: String,
110 pub client_id: Option<ClientId>,
112 pub passphrase: Zeroizing<String>,
114}
115
116impl fmt::Debug for StoredSession {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.debug_struct("StoredSession")
119 .field("homeserver", &self.homeserver)
120 .field("user_id", &self.user_id)
121 .field("device_id", &self.device_id)
122 .field("id", &self.id)
123 .finish_non_exhaustive()
124 }
125}
126
127impl StoredSession {
128 pub(crate) async fn new(client: &Client) -> Result<Self, ClientSetupError> {
133 let mut id = None;
135 let data_path = DataType::Persistent.dir_path();
136
137 for _ in 0..10 {
139 let generated = Alphanumeric.sample_string(&mut rng(), SESSION_ID_LENGTH);
140
141 let path = data_path.join(&generated);
143 if !path.exists() {
144 id = Some(generated);
145 break;
146 }
147 }
148
149 let Some(id) = id else {
150 return Err(ClientSetupError::NoSessionId);
151 };
152
153 let homeserver = client.homeserver();
154 let SessionMeta { user_id, device_id } = client
155 .session_meta()
156 .expect("logged-in client should have session meta")
157 .clone();
158 let tokens = client
159 .session_tokens()
160 .expect("logged-in client should have session tokens")
161 .clone();
162 let client_id = client.oauth().client_id().cloned();
163
164 let passphrase = Alphanumeric.sample_string(&mut rng(), PASSPHRASE_LENGTH);
165
166 let session = Self {
167 homeserver,
168 user_id,
169 device_id,
170 id,
171 client_id,
172 passphrase: passphrase.into(),
173 };
174
175 session.create_data_dir().await;
176 session.store_tokens(tokens).await;
177
178 Ok(session)
179 }
180
181 pub(crate) fn data_path(&self) -> PathBuf {
183 let mut path = DataType::Persistent.dir_path();
184 path.push(&self.id);
185 path
186 }
187
188 async fn create_data_dir(&self) {
191 let data_path = self.data_path();
192
193 spawn_tokio!(async move {
194 if let Err(error) = fs::create_dir_all(data_path).await {
195 error!("Could not create session data directory: {error}");
196 }
197 })
198 .await
199 .expect("task was not aborted");
200 }
201
202 pub(crate) fn cache_path(&self) -> PathBuf {
204 let mut path = DataType::Cache.dir_path();
205 path.push(&self.id);
206 path
207 }
208
209 pub(crate) async fn delete(self) {
211 debug!(
212 "Removing stored session {} for Matrix user {}…",
213 self.id, self.user_id,
214 );
215
216 Secret::delete_session(&self).await;
217
218 spawn_tokio!(async move {
219 if let Err(error) = fs::remove_dir_all(self.data_path()).await {
220 error!("Could not remove session database: {error}");
221 }
222 if let Err(error) = fs::remove_dir_all(self.cache_path()).await {
223 error!("Could not remove session cache: {error}");
224 }
225 })
226 .await
227 .expect("task was not aborted");
228 }
229
230 fn tokens_path(&self) -> PathBuf {
232 let mut path = self.data_path();
233 path.push("tokens");
234 path
235 }
236
237 pub(crate) async fn load_tokens(&self) -> Option<SessionTokens> {
239 let tokens_path = self.tokens_path();
240 let passphrase = self.passphrase.clone();
241
242 let handle = spawn_tokio!(async move { SecretFile::read(&tokens_path, &passphrase).await });
243
244 match handle.await.expect("task was not aborted") {
245 Ok(tokens) => Some(tokens),
246 Err(error) => {
247 error!("Could not load session tokens: {error}");
248 None
249 }
250 }
251 }
252
253 pub(crate) async fn store_tokens(&self, tokens: SessionTokens) {
255 let tokens_path = self.tokens_path();
256 let passphrase = self.passphrase.clone();
257
258 let handle =
259 spawn_tokio!(
260 async move { SecretFile::write(&tokens_path, &passphrase, &tokens).await }
261 );
262
263 if let Err(error) = handle.await.expect("task was not aborted") {
264 error!("Could not store session tokens: {error}");
265 }
266 }
267}