fractal/secret/
mod.rs

1//! API to store the data of a session in a secret store on the system.
2
3use 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
29/// The length of a session ID, in chars or bytes as the string is ASCII.
30pub(crate) const SESSION_ID_LENGTH: usize = 8;
31/// The length of a passphrase, in chars or bytes as the string is ASCII.
32pub(crate) const PASSPHRASE_LENGTH: usize = 30;
33
34cfg_if::cfg_if! {
35    if #[cfg(target_os = "linux")] {
36        /// The secret API.
37        pub(crate) type Secret = linux::LinuxSecret;
38    } else {
39        /// The secret API.
40        pub(crate) type Secret = unimplemented::UnimplementedSecret;
41    }
42}
43
44/// Trait implemented by secret backends.
45pub(crate) trait SecretExt {
46    /// Retrieves all sessions stored in the secret backend.
47    async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError>;
48
49    /// Store the given session into the secret backend, overwriting any
50    /// previously stored session with the same attributes.
51    async fn store_session(session: StoredSession) -> Result<(), SecretError>;
52
53    /// Delete the given session from the secret backend.
54    async fn delete_session(session: &StoredSession);
55}
56
57/// The fallback `Secret` API, to use on platforms where it is unimplemented.
58#[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/// Any error that can happen when interacting with the secret service.
81#[derive(Debug, Error)]
82pub(crate) enum SecretError {
83    /// An error occurred interacting with the secret service.
84    #[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/// A session, as stored in the secret service.
97#[derive(Clone, glib::Boxed)]
98#[boxed_type(name = "StoredSession")]
99pub struct StoredSession {
100    /// The URL of the homeserver where the account lives.
101    pub homeserver: Url,
102    /// The unique identifier of the user.
103    pub user_id: OwnedUserId,
104    /// The unique identifier of the session on the homeserver.
105    pub device_id: OwnedDeviceId,
106    /// The unique local identifier of the session.
107    ///
108    /// This is the name of the directories where the session data lives.
109    pub id: String,
110    /// The unique identifier of the client with the homeserver.
111    pub client_id: Option<ClientId>,
112    /// The passphrase used to encrypt the local databases.
113    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    /// Construct a `StoredSession` from the session of the given Matrix client.
129    ///
130    /// Returns an error if we failed to generate a unique session ID for the
131    /// new session.
132    pub(crate) async fn new(client: &Client) -> Result<Self, ClientSetupError> {
133        // Generate a unique random session ID.
134        let mut id = None;
135        let data_path = DataType::Persistent.dir_path();
136
137        // Try 10 times, so we do not have an infinite loop.
138        for _ in 0..10 {
139            let generated = Alphanumeric.sample_string(&mut rng(), SESSION_ID_LENGTH);
140
141            // Make sure that the ID is not already in use.
142            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    /// The path where the persistent data of this session lives.
182    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    /// Create the directory where the persistent data of this session will
189    /// live.
190    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    /// The path where the cached data of this session lives.
203    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    /// Delete this session from the system.
210    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    /// The path to the files containing the session tokens.
231    fn tokens_path(&self) -> PathBuf {
232        let mut path = self.data_path();
233        path.push("tokens");
234        path
235    }
236
237    /// Load the tokens of this session.
238    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    /// Store the tokens of this session.
254    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}