[fractal/fractal-next] Actually make state store presistent



commit 601ddee7c351dffd6078956db9d602eb2894be4f
Author: Julian Sparber <julian sparber net>
Date:   Fri May 7 19:01:01 2021 +0200

    Actually make state store presistent
    
    This sets a passphrase and path for the state store created by the sdk.
    The additional information is stored in via the SecretService.
    This sets opt-level for deps to 3, because otherwise the statestore
    would be really slow because of the passphrase.

 Cargo.lock         |   1 +
 Cargo.toml         |   4 ++
 src/login.rs       |  12 +++--
 src/secret.rs      | 105 ++++++++++++++++++++++++++++++----------
 src/session/mod.rs | 137 ++++++++++++++++++++++++++++-------------------------
 5 files changed, 165 insertions(+), 94 deletions(-)
---
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 <danigm wadobo com>"]
 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<Vec<(String, matrix_sdk::Session)>, 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<Vec<StoredSession>, 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<String>))> {
+                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<Vec<(String, matrix_sdk::Session)>, 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<Sidebar>,
         #[template_child]
         pub content: TemplateChild<Content>,
-        pub homeserver: OnceCell<String>,
         /// Contains the error if something went wrong
         pub error: RefCell<Option<matrix_sdk::Error>>,
         pub client: OnceCell<Client>,
@@ -79,13 +81,6 @@ mod imp {
         fn properties() -> &'static [glib::ParamSpec] {
             static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = 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<Room> {
@@ -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<matrix_sdk::Session, matrix_sdk::Error>,
+        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);
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]