You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
8.2 KiB
267 lines
8.2 KiB
//! API to store the data of a session in a secret store on the system. |
|
|
|
use std::{fmt, path::PathBuf}; |
|
|
|
use gtk::glib; |
|
use matrix_sdk::{authentication::oauth::ClientId, Client, SessionMeta, SessionTokens}; |
|
use rand::{ |
|
distr::{Alphanumeric, SampleString}, |
|
rng, |
|
}; |
|
use ruma::{OwnedDeviceId, OwnedUserId}; |
|
use thiserror::Error; |
|
use tokio::fs; |
|
use tracing::{debug, error}; |
|
use url::Url; |
|
use zeroize::Zeroizing; |
|
|
|
mod file; |
|
#[cfg(target_os = "linux")] |
|
mod linux; |
|
|
|
use self::file::SecretFile; |
|
use crate::{ |
|
prelude::*, |
|
spawn_tokio, |
|
utils::{data_dir_path, matrix::ClientSetupError, DataType}, |
|
}; |
|
|
|
/// The length of a session ID, in chars or bytes as the string is ASCII. |
|
pub(crate) const SESSION_ID_LENGTH: usize = 8; |
|
/// The length of a passphrase, in chars or bytes as the string is ASCII. |
|
pub(crate) const PASSPHRASE_LENGTH: usize = 30; |
|
|
|
cfg_if::cfg_if! { |
|
if #[cfg(target_os = "linux")] { |
|
/// The secret API. |
|
pub(crate) type Secret = linux::LinuxSecret; |
|
} else { |
|
/// The secret API. |
|
pub(crate) type Secret = unimplemented::UnimplementedSecret; |
|
} |
|
} |
|
|
|
/// Trait implemented by secret backends. |
|
pub(crate) trait SecretExt { |
|
/// Retrieves all sessions stored in the secret backend. |
|
async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError>; |
|
|
|
/// Store the given session into the secret backend, overwriting any |
|
/// previously stored session with the same attributes. |
|
async fn store_session(session: StoredSession) -> Result<(), SecretError>; |
|
|
|
/// Delete the given session from the secret backend. |
|
async fn delete_session(session: &StoredSession); |
|
} |
|
|
|
/// The fallback `Secret` API, to use on platforms where it is unimplemented. |
|
#[cfg(not(target_os = "linux"))] |
|
mod unimplemented { |
|
use super::*; |
|
|
|
#[derive(Debug)] |
|
pub(crate) struct UnimplementedSecret; |
|
|
|
impl SecretExt for UnimplementedSecret { |
|
async fn restore_sessions() -> Result<Vec<StoredSession>, SecretError> { |
|
unimplemented!() |
|
} |
|
|
|
async fn store_session(session: StoredSession) -> Result<(), SecretError> { |
|
unimplemented!() |
|
} |
|
|
|
async fn delete_session(session: &StoredSession) { |
|
unimplemented!() |
|
} |
|
} |
|
} |
|
|
|
/// Any error that can happen when interacting with the secret service. |
|
#[derive(Debug, Error)] |
|
pub(crate) enum SecretError { |
|
/// An error occurred interacting with the secret service. |
|
#[error("Service error: {0}")] |
|
Service(String), |
|
} |
|
|
|
impl UserFacingError for SecretError { |
|
fn to_user_facing(&self) -> String { |
|
match self { |
|
SecretError::Service(error) => error.clone(), |
|
} |
|
} |
|
} |
|
|
|
/// A session, as stored in the secret service. |
|
#[derive(Clone, glib::Boxed)] |
|
#[boxed_type(name = "StoredSession")] |
|
pub struct StoredSession { |
|
/// The URL of the homeserver where the account lives. |
|
pub homeserver: Url, |
|
/// The unique identifier of the user. |
|
pub user_id: OwnedUserId, |
|
/// The unique identifier of the session on the homeserver. |
|
pub device_id: OwnedDeviceId, |
|
/// The unique local identifier of the session. |
|
/// |
|
/// This is the name of the directories where the session data lives. |
|
pub id: String, |
|
/// The unique identifier of the client with the homeserver. |
|
pub client_id: Option<ClientId>, |
|
/// The passphrase used to encrypt the local databases. |
|
pub passphrase: Zeroizing<String>, |
|
} |
|
|
|
impl fmt::Debug for StoredSession { |
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
f.debug_struct("StoredSession") |
|
.field("homeserver", &self.homeserver) |
|
.field("user_id", &self.user_id) |
|
.field("device_id", &self.device_id) |
|
.field("id", &self.id) |
|
.finish_non_exhaustive() |
|
} |
|
} |
|
|
|
impl StoredSession { |
|
/// Construct a `StoredSession` from the session of the given Matrix client. |
|
/// |
|
/// Returns an error if we failed to generate a unique session ID for the |
|
/// new session. |
|
pub(crate) async fn new(client: &Client) -> Result<Self, ClientSetupError> { |
|
// Generate a unique random session ID. |
|
let mut id = None; |
|
let data_path = data_dir_path(DataType::Persistent); |
|
|
|
// Try 10 times, so we do not have an infinite loop. |
|
for _ in 0..10 { |
|
let generated = Alphanumeric.sample_string(&mut rng(), SESSION_ID_LENGTH); |
|
|
|
// Make sure that the ID is not already in use. |
|
let path = data_path.join(&generated); |
|
if !path.exists() { |
|
id = Some(generated); |
|
break; |
|
} |
|
} |
|
|
|
let Some(id) = id else { |
|
return Err(ClientSetupError::NoSessionId); |
|
}; |
|
|
|
let homeserver = client.homeserver(); |
|
let SessionMeta { user_id, device_id } = client |
|
.session_meta() |
|
.expect("logged-in client should have session meta") |
|
.clone(); |
|
let tokens = client |
|
.session_tokens() |
|
.expect("logged-in client should have session tokens") |
|
.clone(); |
|
let client_id = client.oauth().client_id().cloned(); |
|
|
|
let passphrase = Alphanumeric.sample_string(&mut rng(), PASSPHRASE_LENGTH); |
|
|
|
let session = Self { |
|
homeserver, |
|
user_id, |
|
device_id, |
|
id, |
|
client_id, |
|
passphrase: passphrase.into(), |
|
}; |
|
|
|
session.create_data_dir().await; |
|
session.store_tokens(tokens).await; |
|
|
|
Ok(session) |
|
} |
|
|
|
/// The path where the persistent data of this session lives. |
|
pub(crate) fn data_path(&self) -> PathBuf { |
|
let mut path = data_dir_path(DataType::Persistent); |
|
path.push(&self.id); |
|
path |
|
} |
|
|
|
/// Create the directory where the persistent data of this session will |
|
/// live. |
|
async fn create_data_dir(&self) { |
|
let data_path = self.data_path(); |
|
|
|
spawn_tokio!(async move { |
|
if let Err(error) = fs::create_dir_all(data_path).await { |
|
error!("Could not create session data directory: {error}"); |
|
} |
|
}) |
|
.await |
|
.expect("task was not aborted"); |
|
} |
|
|
|
/// The path where the cached data of this session lives. |
|
pub(crate) fn cache_path(&self) -> PathBuf { |
|
let mut path = data_dir_path(DataType::Cache); |
|
path.push(&self.id); |
|
path |
|
} |
|
|
|
/// Delete this session from the system. |
|
pub(crate) async fn delete(self) { |
|
debug!( |
|
"Removing stored session {} for Matrix user {}…", |
|
self.id, self.user_id, |
|
); |
|
|
|
Secret::delete_session(&self).await; |
|
|
|
spawn_tokio!(async move { |
|
if let Err(error) = fs::remove_dir_all(self.data_path()).await { |
|
error!("Could not remove session database: {error}"); |
|
} |
|
if let Err(error) = fs::remove_dir_all(self.cache_path()).await { |
|
error!("Could not remove session cache: {error}"); |
|
} |
|
}) |
|
.await |
|
.expect("task was not aborted"); |
|
} |
|
|
|
/// The path to the files containing the session tokens. |
|
fn tokens_path(&self) -> PathBuf { |
|
let mut path = self.data_path(); |
|
path.push("tokens"); |
|
path |
|
} |
|
|
|
/// Load the tokens of this session. |
|
pub(crate) async fn load_tokens(&self) -> Option<SessionTokens> { |
|
let tokens_path = self.tokens_path(); |
|
let passphrase = self.passphrase.clone(); |
|
|
|
let handle = spawn_tokio!(async move { SecretFile::read(&tokens_path, &passphrase).await }); |
|
|
|
match handle.await.expect("task was not aborted") { |
|
Ok(tokens) => Some(tokens), |
|
Err(error) => { |
|
error!("Could not load session tokens: {error}"); |
|
None |
|
} |
|
} |
|
} |
|
|
|
/// Store the tokens of this session. |
|
pub(crate) async fn store_tokens(&self, tokens: SessionTokens) { |
|
let tokens_path = self.tokens_path(); |
|
let passphrase = self.passphrase.clone(); |
|
|
|
let handle = |
|
spawn_tokio!( |
|
async move { SecretFile::write(&tokens_path, &passphrase, &tokens).await } |
|
); |
|
|
|
if let Err(error) = handle.await.expect("task was not aborted") { |
|
error!("Could not store session tokens: {error}"); |
|
} |
|
} |
|
}
|
|
|