diff --git a/Cargo.lock b/Cargo.lock index e51b2629..ae63e4d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,7 @@ dependencies = [ "log", "matrix-sdk", "once_cell", + "rand 0.8.3", "secret-service", "serde_json", "sourceview5", diff --git a/Cargo.toml b/Cargo.toml index c10aa5c1..80df81b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" authors = ["Daniel GarcĂ­a Moreno "] edition = "2018" +[profile.dev.package."*"] +opt-level = 3 + [dependencies] log = "0.4" tracing-subscriber = "0.2" @@ -18,6 +21,7 @@ html2pango = "0.4" chrono = "0.4" futures = "0.3" comrak = "0.10" +rand = "0.8" [dependencies.sourceview] branch = "main" diff --git a/src/login.rs b/src/login.rs index 50e8eba6..dcc45626 100644 --- a/src/login.rs +++ b/src/login.rs @@ -121,16 +121,20 @@ impl Login { self.freeze(); - let session = Session::new(homeserver); + let session = Session::new(); self.setup_session(&session); - session.login_with_password(username, password); + session.login_with_password( + url::Url::parse(homeserver.as_str()).unwrap(), + username, + password, + ); } pub fn restore_sessions(&self) -> Result<(), secret_service::Error> { let sessions = secret::restore_sessions()?; - for (homeserver, stored_session) in sessions { - let session = Session::new(homeserver.to_string()); + for stored_session in sessions { + let session = Session::new(); self.setup_session(&session); session.login_with_previous_session(stored_session); } diff --git a/src/secret.rs b/src/secret.rs index 68ca4877..3feb854e 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -1,38 +1,74 @@ +use matrix_sdk::identifiers::{DeviceIdBox, UserId}; use secret_service::EncryptionType; use secret_service::SecretService; use std::collections::HashMap; +use std::convert::TryFrom; +use std::path::PathBuf; +use url::Url; -/// Retrives all sessions stored to the `SecretService` -pub fn restore_sessions() -> Result, secret_service::Error> { - use std::convert::TryInto; +pub struct StoredSession { + pub homeserver: Url, + pub path: PathBuf, + pub passphrase: String, + pub user_id: UserId, + pub access_token: String, + pub device_id: DeviceIdBox, +} +/// Retrives all sessions stored to the `SecretService` +pub fn restore_sessions() -> Result, secret_service::Error> { let ss = SecretService::new(EncryptionType::Dh)?; let collection = ss.get_default_collection()?; // Sessions that contain or produce errors are ignored. // TODO: Return error for corrupt sessions + let res = collection .get_all_items()? .iter() - .filter_map(|item| { - let attr = item.get_attributes().ok()?; - if let (Some(homeserver), Some(access_token), Some(user_id), Some(device_id)) = ( - attr.get("homeserver"), - String::from_utf8(item.get_secret().ok()?).ok(), - attr.get("user-id") - .and_then(|s| s.to_string().try_into().ok()), - attr.get("device-id") - .and_then(|s| Some(s.to_string().into())), - ) { - let session = matrix_sdk::Session { - access_token, - user_id, - device_id, - }; - Some((homeserver.to_string(), session)) - } else { - None + .fold(HashMap::new(), |mut acc, item| { + let finder = move || -> Option<((String, String, String), (String, Option))> { + let attr = item.get_attributes().ok()?; + + let homeserver = attr.get("homeserver")?.to_string(); + let user_id = attr.get("user-id")?.to_string(); + let device_id = attr.get("device-id")?.to_string(); + let secret = String::from_utf8(item.get_secret().ok()?).ok()?; + let path = attr.get("path").map(|s| s.to_string()); + Some(((homeserver, user_id, device_id), (secret, path))) + }; + + if let Some((key, value)) = finder() { + acc.entry(key).or_insert(vec![]).push(value); } + + acc + }) + .into_iter() + .filter_map(|((homeserver, user_id, device_id), mut items)| { + if items.len() != 2 { + return None; + } + let (access_token, passphrase, path) = match items.pop()? { + (secret, Some(path)) => (items.pop()?.0, secret, PathBuf::from(path)), + (secret, None) => { + let item = items.pop()?; + (secret, item.0, PathBuf::from(item.1?)) + } + }; + + let homeserver = Url::parse(&homeserver).ok()?; + let user_id = UserId::try_from(user_id).ok()?; + let device_id = DeviceIdBox::try_from(device_id).ok()?; + + Some(StoredSession { + homeserver, + path, + passphrase, + user_id, + access_token, + device_id, + }) }) .collect(); @@ -41,16 +77,14 @@ pub fn restore_sessions() -> Result, secret_s /// Writes a sessions to the `SecretService`, overwriting any previously stored session with the /// same `homeserver`, `username` and `device-id`. -pub fn store_session( - homeserver: &str, - session: matrix_sdk::Session, -) -> Result<(), secret_service::Error> { +pub fn store_session(session: StoredSession) -> Result<(), secret_service::Error> { let ss = SecretService::new(EncryptionType::Dh)?; let collection = ss.get_default_collection()?; + // Store the infromation for the login let attributes: HashMap<&str, &str> = [ ("user-id", session.user_id.as_str()), - ("homeserver", homeserver), + ("homeserver", session.homeserver.as_str()), ("device-id", session.device_id.as_str()), ] .iter() @@ -65,5 +99,24 @@ pub fn store_session( "text/plain", )?; + // Store the infromation for the crypto store + let attributes: HashMap<&str, &str> = [ + ("path", session.path.to_str().unwrap()), + ("user-id", session.user_id.as_str()), + ("homeserver", session.homeserver.as_str()), + ("device-id", session.device_id.as_str()), + ] + .iter() + .cloned() + .collect(); + + collection.create_item( + "Fractal (Encrypted local database)", + attributes, + session.passphrase.as_bytes(), + true, + "text/plain", + )?; + Ok(()) } diff --git a/src/session/mod.rs b/src/session/mod.rs index ab8b303e..c76282c8 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -11,9 +11,11 @@ use self::user::User; use crate::event_from_sync_event; use crate::secret; +use crate::secret::StoredSession; use crate::utils::do_async; use crate::RUNTIME; +use crate::session::categories::Categories; use adw; use adw::subclass::prelude::BinImpl; use gtk::subclass::prelude::*; @@ -27,13 +29,14 @@ use matrix_sdk::{ deserialized_responses::SyncResponse, events::{AnyRoomEvent, AnySyncRoomEvent}, identifiers::RoomId, + uuid::Uuid, Client, ClientConfig, RequestConfig, SyncSettings, }; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use std::fs; use std::time::Duration; use url::Url; -use crate::session::categories::Categories; - mod imp { use super::*; use glib::subclass::{InitializingObject, Signal}; @@ -50,7 +53,6 @@ mod imp { pub sidebar: TemplateChild, #[template_child] pub content: TemplateChild, - pub homeserver: OnceCell, /// Contains the error if something went wrong pub error: RefCell>, pub client: OnceCell, @@ -79,13 +81,6 @@ mod imp { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ - glib::ParamSpec::new_string( - "homeserver", - "Homeserver", - "The matrix homeserver of this session", - None, - glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, - ), glib::ParamSpec::new_object( "categories", "Categories", @@ -114,10 +109,6 @@ mod imp { pspec: &glib::ParamSpec, ) { match pspec.name() { - "homeserver" => { - let homeserver = value.get().unwrap(); - let _ = obj.set_homeserver(homeserver); - } "selected-room" => { let selected_room = value.get().unwrap(); obj.set_selected_room(selected_room); @@ -128,7 +119,6 @@ mod imp { fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { - "homeserver" => self.homeserver.get().to_value(), "categories" => self.categories.to_value(), "selected-room" => obj.selected_room().to_value(), _ => unimplemented!(), @@ -156,8 +146,8 @@ glib::wrapper! { } impl Session { - pub fn new(homeserver: String) -> Self { - glib::Object::new(&[("homeserver", &homeserver)]).expect("Failed to create Session") + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create Session") } pub fn selected_room(&self) -> Option { @@ -184,42 +174,51 @@ impl Session { self.notify("selected-room"); } - fn set_homeserver(&self, homeserver: String) { - let priv_ = imp::Session::from_instance(self); - - priv_.homeserver.set(homeserver.clone()).unwrap(); - - let config = ClientConfig::new().request_config(RequestConfig::new().retry_limit(2)); - let homeserver = match Url::parse(homeserver.as_str()) { - Ok(homeserver) => homeserver, - Err(_error) => { - // TODO: hanlde parse error - panic!(); - } - }; - - let client = Client::new_with_config(homeserver, config).unwrap(); - priv_.client.set(client).unwrap(); - } - - fn client(&self) -> Client { - let priv_ = imp::Session::from_instance(self); - priv_.client.get().unwrap().clone() - } - - pub fn login_with_password(&self, username: String, password: String) { - let client = self.client(); + pub fn login_with_password(&self, homeserver: Url, username: String, password: String) { + let mut path = glib::user_data_dir(); + path.push( + &Uuid::new_v4() + .to_hyphenated() + .encode_lower(&mut Uuid::encode_buffer()), + ); do_async( async move { - client + let passphrase: String = { + let mut rng = thread_rng(); + (&mut rng) + .sample_iter(Alphanumeric) + .take(30) + .map(char::from) + .collect() + }; + let config = ClientConfig::new() + .request_config(RequestConfig::new().retry_limit(2)) + .passphrase(passphrase.clone()) + .store_path(path.clone()); + + let client = Client::new_with_config(homeserver.clone(), config).unwrap(); + let response = client .login(&username, &password, None, Some("Fractal Next")) - .await - .map(|response| matrix_sdk::Session { - access_token: response.access_token, - user_id: response.user_id, - device_id: response.device_id, - }) + .await; + match response { + Ok(response) => Ok(( + client, + StoredSession { + homeserver: homeserver, + path: path, + passphrase: passphrase, + access_token: response.access_token, + user_id: response.user_id, + device_id: response.device_id, + }, + )), + Err(error) => { + // Remove the store created by Client::new() + fs::remove_dir_all(path).unwrap(); + Err(error) + } + } }, clone!(@weak self as obj => move |result| async move { obj.handle_login_result(result, true); @@ -227,11 +226,24 @@ impl Session { ); } - pub fn login_with_previous_session(&self, session: matrix_sdk::Session) { - let client = self.client(); - + pub fn login_with_previous_session(&self, session: StoredSession) { do_async( - async move { client.restore_login(session.clone()).await.map(|_| session) }, + async move { + let config = ClientConfig::new() + .request_config(RequestConfig::new().retry_limit(2)) + .passphrase(session.passphrase.clone()) + .store_path(session.path.clone()); + + let client = Client::new_with_config(session.homeserver.clone(), config).unwrap(); + client + .restore_login(matrix_sdk::Session { + user_id: session.user_id.clone(), + device_id: session.device_id.clone(), + access_token: session.access_token.clone(), + }) + .await + .map(|_| (client, session)) + }, clone!(@weak self as obj => move |result| async move { obj.handle_login_result(result, false); }), @@ -240,15 +252,17 @@ impl Session { fn handle_login_result( &self, - result: Result, + result: Result<(Client, StoredSession), matrix_sdk::Error>, store_session: bool, ) { - let priv_ = &imp::Session::from_instance(self); + let priv_ = imp::Session::from_instance(self); match result { - Ok(session) => { + Ok((client, session)) => { + priv_.client.set(client).unwrap(); self.set_user(User::new(&session.user_id)); if store_session { - self.store_session(session).unwrap(); + // TODO: report secret service errors + secret::store_session(session).unwrap(); } self.load(); self.sync(); @@ -261,8 +275,9 @@ impl Session { } fn sync(&self) { + let priv_ = imp::Session::from_instance(self); let sender = self.create_new_sync_response_sender(); - let client = self.client(); + let client = priv_.client.get().unwrap().clone(); RUNTIME.spawn(async move { // TODO: only create the filter once and reuse it in the future let room_event_filter = assign!(RoomEventFilter::default(), { @@ -344,12 +359,6 @@ impl Session { .unwrap() } - fn store_session(&self, session: matrix_sdk::Session) -> Result<(), secret_service::Error> { - let priv_ = &imp::Session::from_instance(self); - let homeserver = priv_.homeserver.get().unwrap(); - secret::store_session(homeserver, session) - } - fn handle_sync_response(&self, response: SyncResponse) { let priv_ = imp::Session::from_instance(self);