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.
This commit is contained in:
Julian Sparber 2021-05-07 19:01:01 +02:00
parent fc02302620
commit 601ddee7c3
5 changed files with 165 additions and 94 deletions

1
Cargo.lock generated
View File

@ -684,6 +684,7 @@ dependencies = [
"log",
"matrix-sdk",
"once_cell",
"rand 0.8.3",
"secret-service",
"serde_json",
"sourceview5",

View File

@ -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"

View File

@ -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);
}

View File

@ -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;
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<(String, matrix_sdk::Session)>, secret_service::Error> {
use std::convert::TryInto;
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(())
}

View File

@ -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);