account-settings: Allow to import and export room encryption keys
Part-of: <https://gitlab.gnome.org/GNOME/fractal/-/merge_requests/1157>
This commit is contained in:
parent
dc33441fad
commit
8dd205ffce
10 changed files with 739 additions and 2 deletions
|
@ -26,6 +26,8 @@
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-deactivate-account-subpage.ui">ui/account-settings-deactivate-account-subpage.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-deactivate-account-subpage.ui">ui/account-settings-deactivate-account-subpage.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-device-row.ui">ui/account-settings-device-row.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-device-row.ui">ui/account-settings-device-row.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-devices-page.ui">ui/account-settings-devices-page.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-devices-page.ui">ui/account-settings-devices-page.ui</file>
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-import-export-keys-subpage.ui">ui/account-settings-import-export-keys-subpage.ui</file>
|
||||||
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-security-page.ui">ui/account-settings-security-page.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-user-page.ui">ui/account-settings-user-page.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings-user-page.ui">ui/account-settings-user-page.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings.ui">ui/account-settings.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="account-settings.ui">ui/account-settings.ui</file>
|
||||||
<file compressed="true" preprocess="xml-stripblanks" alias="attachment-dialog.ui">ui/attachment-dialog.ui</file>
|
<file compressed="true" preprocess="xml-stripblanks" alias="attachment-dialog.ui">ui/attachment-dialog.ui</file>
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
<style>
|
<style>
|
||||||
<class name="body"/>
|
<class name="body"/>
|
||||||
</style>
|
</style>
|
||||||
<property name="label">Fractal’s support for encryption is unstable so you might lose access to your encrypted message history. It is recommended to backup your encryption keys from another Matrix client before proceeding.</property>
|
<property name="label">Fractal’s support for encryption is unstable so you might lose access to your encrypted message history. It is recommended to backup your encryption keys before proceeding.</property>
|
||||||
<property name="wrap">True</property>
|
<property name="wrap">True</property>
|
||||||
<property name="wrap-mode">word-char</property>
|
<property name="wrap-mode">word-char</property>
|
||||||
<property name="xalign">0.0</property>
|
<property name="xalign">0.0</property>
|
||||||
|
|
108
data/resources/ui/account-settings-import-export-keys-subpage.ui
Normal file
108
data/resources/ui/account-settings-import-export-keys-subpage.ui
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="ImportExportKeysSubpage" parent="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkHeaderBar">
|
||||||
|
<property name="title-widget">
|
||||||
|
<object class="GtkLabel" id="title">
|
||||||
|
<property name="single-line-mode">True</property>
|
||||||
|
<property name="ellipsize">end</property>
|
||||||
|
<property name="width-chars">5</property>
|
||||||
|
<style>
|
||||||
|
<class name="title"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
<child type="start">
|
||||||
|
<object class="GtkButton" id="back">
|
||||||
|
<property name="icon-name">go-previous-symbolic</property>
|
||||||
|
<property name="action-name">win.close-subpage</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesPage">
|
||||||
|
<style>
|
||||||
|
<class name="status-page"/>
|
||||||
|
</style>
|
||||||
|
<property name="vexpand">true</property>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="description">
|
||||||
|
<style>
|
||||||
|
<class name="body"/>
|
||||||
|
</style>
|
||||||
|
<property name="label"></property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="wrap-mode">word-char</property>
|
||||||
|
<property name="xalign">0.0</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="instructions">
|
||||||
|
<style>
|
||||||
|
<class name="body"/>
|
||||||
|
</style>
|
||||||
|
<property name="label"></property>
|
||||||
|
<property name="wrap">True</property>
|
||||||
|
<property name="wrap-mode">word-char</property>
|
||||||
|
<property name="xalign">0.0</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="ComponentsPasswordEntryRow" id="passphrase">
|
||||||
|
<property name="title" translatable="yes">Passphrase</property>
|
||||||
|
<signal name="activated" handler="handle_proceed" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="ComponentsPasswordEntryRow" id="confirm_passphrase">
|
||||||
|
<property name="title" translatable="yes">Confirm Passphrase</property>
|
||||||
|
<signal name="activated" handler="handle_proceed" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="AdwActionRow" id="file_row">
|
||||||
|
<property name="title">File</property>
|
||||||
|
<property name="subtitle" bind-source="ImportExportKeysSubpage" bind-property="file-path" bind-flags="sync-create"/>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="file_button">
|
||||||
|
<property name="label" translatable="yes">Choose…</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<signal name="clicked" handler="handle_choose_file" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<child>
|
||||||
|
<object class="SpinnerButton" id="proceed_button">
|
||||||
|
<style>
|
||||||
|
<class name="row"/>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
<property name="sensitive">false</property>
|
||||||
|
<signal name="clicked" handler="handle_proceed" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
30
data/resources/ui/account-settings-security-page.ui
Normal file
30
data/resources/ui/account-settings-security-page.ui
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="SecurityPage" parent="AdwPreferencesPage">
|
||||||
|
<property name="icon-name">channel-secure-symbolic</property>
|
||||||
|
<property name="title" translatable="yes">Security</property>
|
||||||
|
<property name="name">security</property>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<property name="title" translatable="yes">Room Encryption Keys</property>
|
||||||
|
<child>
|
||||||
|
<object class="ComponentsButtonRow">
|
||||||
|
<property name="title" translatable="yes">Export Room Encryption Keys</property>
|
||||||
|
<property name="to-subpage">true</property>
|
||||||
|
<signal name="activated" handler="handle_export_keys" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="ComponentsButtonRow">
|
||||||
|
<property name="title" translatable="yes">Import Room Encryption Keys</property>
|
||||||
|
<property name="to-subpage">true</property>
|
||||||
|
<signal name="activated" handler="handle_import_keys" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
<object class="ImportExportKeysSubpage" id="import_export_keys_subpage">
|
||||||
|
<property name="session" bind-source="SecurityPage" bind-property="session" bind-flags="sync-create"/>
|
||||||
|
</object>
|
||||||
|
</interface>
|
|
@ -18,5 +18,10 @@
|
||||||
</binding>
|
</binding>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="SecurityPage">
|
||||||
|
<property name="session" bind-source="AccountSettings" bind-property="session" bind-flags="sync-create"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</template>
|
</template>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
@ -9,7 +9,9 @@ data/resources/ui/account-settings-change-password-subpage.ui
|
||||||
data/resources/ui/account-settings-deactivate-account-subpage.ui
|
data/resources/ui/account-settings-deactivate-account-subpage.ui
|
||||||
data/resources/ui/account-settings-device-row.ui
|
data/resources/ui/account-settings-device-row.ui
|
||||||
data/resources/ui/account-settings-devices-page.ui
|
data/resources/ui/account-settings-devices-page.ui
|
||||||
|
data/resources/ui/account-settings-import-export-keys-subpage.ui
|
||||||
data/resources/ui/account-settings-user-page.ui
|
data/resources/ui/account-settings-user-page.ui
|
||||||
|
data/resources/ui/account-settings-security-page.ui
|
||||||
data/resources/ui/account-settings.ui
|
data/resources/ui/account-settings.ui
|
||||||
data/resources/ui/attachment-dialog.ui
|
data/resources/ui/attachment-dialog.ui
|
||||||
data/resources/ui/components-auth-dialog.ui
|
data/resources/ui/components-auth-dialog.ui
|
||||||
|
@ -51,6 +53,7 @@ src/login/mod.rs
|
||||||
src/secret.rs
|
src/secret.rs
|
||||||
src/session/account_settings/devices_page/device_list.rs
|
src/session/account_settings/devices_page/device_list.rs
|
||||||
src/session/account_settings/devices_page/device_row.rs
|
src/session/account_settings/devices_page/device_row.rs
|
||||||
|
src/session/account_settings/security_page/import_export_keys_subpage.rs
|
||||||
src/session/account_settings/user_page/change_password_subpage.rs
|
src/session/account_settings/user_page/change_password_subpage.rs
|
||||||
src/session/account_settings/user_page/deactivate_account_subpage.rs
|
src/session/account_settings/user_page/deactivate_account_subpage.rs
|
||||||
src/session/account_settings/user_page/mod.rs
|
src/session/account_settings/user_page/mod.rs
|
||||||
|
|
|
@ -225,7 +225,7 @@ impl DeviceRow {
|
||||||
self.imp().delete_logout_button.set_loading(true);
|
self.imp().delete_logout_button.set_loading(true);
|
||||||
|
|
||||||
let window: Option<gtk::Window> = self.root().and_then(|root| root.downcast().ok());
|
let window: Option<gtk::Window> = self.root().and_then(|root| root.downcast().ok());
|
||||||
let dialog = gtk::MessageDialog::new(window.as_ref(), gtk::DialogFlags::MODAL, gtk::MessageType::Info, gtk::ButtonsType::OkCancel, &gettext("Fractal’s support for encryption is unstable so you might lose access to your encrypted message history. It is recommended to backup your encryption keys from another Matrix client before proceeding."));
|
let dialog = gtk::MessageDialog::new(window.as_ref(), gtk::DialogFlags::MODAL, gtk::MessageType::Info, gtk::ButtonsType::OkCancel, &gettext("Fractal doesn't support online backup of room encryption keys so you might lose access to your encrypted message history. It is recommended to backup your encryption keys before proceeding."));
|
||||||
dialog.show();
|
dialog.show();
|
||||||
dialog.connect_response(
|
dialog.connect_response(
|
||||||
clone!(@weak self as obj, @weak dialog => move |_, response| {
|
clone!(@weak self as obj, @weak dialog => move |_, response| {
|
||||||
|
|
|
@ -6,8 +6,10 @@ use gtk::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod devices_page;
|
mod devices_page;
|
||||||
|
mod security_page;
|
||||||
mod user_page;
|
mod user_page;
|
||||||
use devices_page::DevicesPage;
|
use devices_page::DevicesPage;
|
||||||
|
use security_page::SecurityPage;
|
||||||
use user_page::UserPage;
|
use user_page::UserPage;
|
||||||
|
|
||||||
use super::Session;
|
use super::Session;
|
||||||
|
@ -35,6 +37,7 @@ mod imp {
|
||||||
fn class_init(klass: &mut Self::Class) {
|
fn class_init(klass: &mut Self::Class) {
|
||||||
DevicesPage::static_type();
|
DevicesPage::static_type();
|
||||||
UserPage::static_type();
|
UserPage::static_type();
|
||||||
|
SecurityPage::static_type();
|
||||||
Self::bind_template(klass);
|
Self::bind_template(klass);
|
||||||
|
|
||||||
klass.install_action("account-settings.close", None, |obj, _, _| {
|
klass.install_action("account-settings.close", None, |obj, _, _| {
|
||||||
|
|
|
@ -0,0 +1,451 @@
|
||||||
|
use adw::{prelude::*, subclass::prelude::*};
|
||||||
|
use gettextrs::gettext;
|
||||||
|
use gtk::{
|
||||||
|
gio,
|
||||||
|
glib::{self, clone},
|
||||||
|
CompositeTemplate,
|
||||||
|
};
|
||||||
|
use log::error;
|
||||||
|
use matrix_sdk::encryption::{KeyExportError, RoomKeyImportError};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{PasswordEntryRow, SpinnerButton},
|
||||||
|
i18n::ngettext_f,
|
||||||
|
session::Session,
|
||||||
|
spawn, spawn_tokio, toast,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
|
||||||
|
#[repr(u32)]
|
||||||
|
#[enum_type(name = "KeysSubpageMode")]
|
||||||
|
pub enum KeysSubpageMode {
|
||||||
|
Export = 0,
|
||||||
|
Import = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeysSubpageMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Export
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
||||||
|
use glib::{subclass::InitializingObject, WeakRef};
|
||||||
|
use once_cell::unsync::OnceCell;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, CompositeTemplate)]
|
||||||
|
#[template(resource = "/org/gnome/Fractal/account-settings-import-export-keys-subpage.ui")]
|
||||||
|
pub struct ImportExportKeysSubpage {
|
||||||
|
pub session: OnceCell<WeakRef<Session>>,
|
||||||
|
#[template_child]
|
||||||
|
pub title: TemplateChild<gtk::Label>,
|
||||||
|
#[template_child]
|
||||||
|
pub description: TemplateChild<gtk::Label>,
|
||||||
|
#[template_child]
|
||||||
|
pub instructions: TemplateChild<gtk::Label>,
|
||||||
|
#[template_child]
|
||||||
|
pub passphrase: TemplateChild<PasswordEntryRow>,
|
||||||
|
#[template_child]
|
||||||
|
pub confirm_passphrase: TemplateChild<PasswordEntryRow>,
|
||||||
|
#[template_child]
|
||||||
|
pub file_row: TemplateChild<adw::ActionRow>,
|
||||||
|
#[template_child]
|
||||||
|
pub file_button: TemplateChild<gtk::Button>,
|
||||||
|
#[template_child]
|
||||||
|
pub proceed_button: TemplateChild<SpinnerButton>,
|
||||||
|
pub file_path: RefCell<Option<gio::File>>,
|
||||||
|
pub mode: Cell<KeysSubpageMode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for ImportExportKeysSubpage {
|
||||||
|
const NAME: &'static str = "ImportExportKeysSubpage";
|
||||||
|
type Type = super::ImportExportKeysSubpage;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
klass.bind_template();
|
||||||
|
Self::Type::bind_template_callbacks(klass);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_init(obj: &InitializingObject<Self>) {
|
||||||
|
obj.init_template();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for ImportExportKeysSubpage {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpecObject::new(
|
||||||
|
"session",
|
||||||
|
"Session",
|
||||||
|
"The session",
|
||||||
|
Session::static_type(),
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecString::new(
|
||||||
|
"file-path",
|
||||||
|
"File Path",
|
||||||
|
"The path to export the keys to",
|
||||||
|
None,
|
||||||
|
glib::ParamFlags::READABLE,
|
||||||
|
),
|
||||||
|
glib::ParamSpecEnum::new(
|
||||||
|
"mode",
|
||||||
|
"Mode",
|
||||||
|
"The export/import mode of the subpage",
|
||||||
|
KeysSubpageMode::static_type(),
|
||||||
|
KeysSubpageMode::default() as i32,
|
||||||
|
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"session" => obj.set_session(value.get().unwrap()),
|
||||||
|
"mode" => obj.set_mode(value.get().unwrap()),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"session" => obj.session().to_value(),
|
||||||
|
"file-path" => obj
|
||||||
|
.file_path()
|
||||||
|
.and_then(|file| file.path())
|
||||||
|
.map(|path| path.to_string_lossy().to_string())
|
||||||
|
.to_value(),
|
||||||
|
"mode" => obj.mode().to_value(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
|
self.parent_constructed(obj);
|
||||||
|
|
||||||
|
self.passphrase
|
||||||
|
.connect_changed(clone!(@weak obj => move|_| {
|
||||||
|
obj.update_button();
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.confirm_passphrase
|
||||||
|
.connect_focused(clone!(@weak obj => move |entry, focused| {
|
||||||
|
if focused {
|
||||||
|
obj.validate_passphrase_confirmation();
|
||||||
|
} else {
|
||||||
|
entry.remove_css_class("warning");
|
||||||
|
entry.remove_css_class("success");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self.confirm_passphrase
|
||||||
|
.connect_changed(clone!(@weak obj => move|_| {
|
||||||
|
obj.validate_passphrase_confirmation();
|
||||||
|
}));
|
||||||
|
|
||||||
|
obj.update_for_mode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetImpl for ImportExportKeysSubpage {}
|
||||||
|
impl BoxImpl for ImportExportKeysSubpage {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
/// Subpage to export room encryption keys for backup.
|
||||||
|
pub struct ImportExportKeysSubpage(ObjectSubclass<imp::ImportExportKeysSubpage>)
|
||||||
|
@extends gtk::Widget, gtk::Box, @implements gtk::Accessible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl ImportExportKeysSubpage {
|
||||||
|
pub fn new(session: &Session) -> Self {
|
||||||
|
glib::Object::new(&[("session", session)])
|
||||||
|
.expect("Failed to create ImportExportKeysSubpage")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session(&self) -> Option<Session> {
|
||||||
|
self.imp()
|
||||||
|
.session
|
||||||
|
.get()
|
||||||
|
.and_then(|session| session.upgrade())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_session(&self, session: Option<Session>) {
|
||||||
|
if let Some(session) = session {
|
||||||
|
self.imp().session.set(session.downgrade()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_path(&self) -> Option<gio::File> {
|
||||||
|
self.imp().file_path.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_file_path(&self, path: Option<gio::File>) {
|
||||||
|
let priv_ = self.imp();
|
||||||
|
if priv_.file_path.borrow().as_ref() == path.as_ref() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
priv_.file_path.replace(path);
|
||||||
|
self.update_button();
|
||||||
|
self.notify("file-path");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mode(&self) -> KeysSubpageMode {
|
||||||
|
self.imp().mode.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mode(&self, mode: KeysSubpageMode) {
|
||||||
|
if self.mode() == mode {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imp().mode.set(mode);
|
||||||
|
self.update_for_mode();
|
||||||
|
self.clear();
|
||||||
|
self.notify("mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&self) {
|
||||||
|
let priv_ = self.imp();
|
||||||
|
|
||||||
|
self.set_file_path(None);
|
||||||
|
priv_.passphrase.set_text("");
|
||||||
|
priv_.confirm_passphrase.set_text("");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_for_mode(&self) {
|
||||||
|
let priv_ = self.imp();
|
||||||
|
|
||||||
|
if self.mode() == KeysSubpageMode::Export {
|
||||||
|
priv_
|
||||||
|
.title
|
||||||
|
.set_label(&gettext("Export Room Encryption Keys"));
|
||||||
|
priv_.description.set_label(&gettext(
|
||||||
|
"Exporting your room encryption keys allows you to make a backup to be able to decrypt your messages in end-to-end encrypted rooms on another device or with another Matrix client.",
|
||||||
|
));
|
||||||
|
priv_.instructions.set_label(&gettext(
|
||||||
|
"The backup must be stored in a safe place and must be protected with a strong passphrase that will be used to encrypt the data.",
|
||||||
|
));
|
||||||
|
priv_.confirm_passphrase.show();
|
||||||
|
priv_.proceed_button.set_label(&gettext("Export Keys"));
|
||||||
|
} else {
|
||||||
|
priv_
|
||||||
|
.title
|
||||||
|
.set_label(&gettext("Import Room Encryption Keys"));
|
||||||
|
priv_.description.set_label(&gettext(
|
||||||
|
"Importing your room encryption keys allows you to decrypt your messages in end-to-end encrypted rooms with a previous backup from a Matrix client.",
|
||||||
|
));
|
||||||
|
priv_.instructions.set_label(&gettext(
|
||||||
|
"Enter the passphrase provided when the backup file was created.",
|
||||||
|
));
|
||||||
|
priv_.confirm_passphrase.hide();
|
||||||
|
priv_.proceed_button.set_label(&gettext("Import Keys"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_button();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn handle_choose_file(&self) {
|
||||||
|
spawn!(clone!(@weak self as obj => async move {
|
||||||
|
obj.choose_file().await;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn choose_file(&self) {
|
||||||
|
let is_export = self.mode() == KeysSubpageMode::Export;
|
||||||
|
let (title, action) = if is_export {
|
||||||
|
(
|
||||||
|
gettext("Save Encryption Keys To…"),
|
||||||
|
gtk::FileChooserAction::Save,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
gettext("Import Encryption Keys From…"),
|
||||||
|
gtk::FileChooserAction::Open,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialog = gtk::FileChooserNative::builder()
|
||||||
|
.title(&title)
|
||||||
|
.modal(true)
|
||||||
|
.transient_for(
|
||||||
|
self.root()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| root.downcast_ref::<gtk::Window>())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.action(action)
|
||||||
|
.accept_label(&gettext("Select"))
|
||||||
|
.cancel_label(&gettext("Cancel"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if let Some(file) = self.file_path() {
|
||||||
|
let _ = dialog.set_file(&file);
|
||||||
|
} else if is_export {
|
||||||
|
// Translators: Do no translate "fractal" as it is the application
|
||||||
|
// name.
|
||||||
|
dialog.set_current_name(&format!("{}.txt", gettext("fractal-encryption-keys")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialog.run_future().await == gtk::ResponseType::Accept {
|
||||||
|
if let Some(file) = dialog.file() {
|
||||||
|
self.set_file_path(Some(file));
|
||||||
|
} else {
|
||||||
|
error!("No file chosen");
|
||||||
|
toast!(self, gettext("No file was chosen"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_passphrase_confirmation(&self) {
|
||||||
|
let priv_ = self.imp();
|
||||||
|
let entry = &priv_.confirm_passphrase;
|
||||||
|
let passphrase = priv_.passphrase.text();
|
||||||
|
let confirmation = entry.text();
|
||||||
|
|
||||||
|
if confirmation.is_empty() {
|
||||||
|
entry.set_hint("");
|
||||||
|
entry.remove_css_class("success");
|
||||||
|
entry.remove_css_class("warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if passphrase == confirmation {
|
||||||
|
entry.set_hint("");
|
||||||
|
entry.add_css_class("success");
|
||||||
|
entry.remove_css_class("warning");
|
||||||
|
} else {
|
||||||
|
entry.remove_css_class("success");
|
||||||
|
entry.add_css_class("warning");
|
||||||
|
entry.set_hint(&gettext("Passphrases do not match"));
|
||||||
|
}
|
||||||
|
self.update_button();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_button(&self) {
|
||||||
|
self.imp().proceed_button.set_sensitive(self.can_proceed());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_proceed(&self) -> bool {
|
||||||
|
let priv_ = self.imp();
|
||||||
|
let file_path = priv_.file_path.borrow();
|
||||||
|
let passphrase = priv_.passphrase.text();
|
||||||
|
|
||||||
|
let mut res = file_path
|
||||||
|
.as_ref()
|
||||||
|
.filter(|file| file.path().is_some())
|
||||||
|
.is_some()
|
||||||
|
&& !passphrase.is_empty();
|
||||||
|
|
||||||
|
if self.mode() == KeysSubpageMode::Export {
|
||||||
|
let confirmation = priv_.confirm_passphrase.text();
|
||||||
|
res = res && passphrase == confirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn handle_proceed(&self) {
|
||||||
|
spawn!(clone!(@weak self as obj => async move {
|
||||||
|
obj.proceed().await;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn proceed(&self) {
|
||||||
|
if !self.can_proceed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let priv_ = self.imp();
|
||||||
|
let file_path = self.file_path().and_then(|file| file.path()).unwrap();
|
||||||
|
let passphrase = priv_.passphrase.text();
|
||||||
|
let is_export = self.mode() == KeysSubpageMode::Export;
|
||||||
|
|
||||||
|
priv_.proceed_button.set_loading(true);
|
||||||
|
priv_.file_button.set_sensitive(false);
|
||||||
|
priv_.passphrase.set_entry_sensitive(false);
|
||||||
|
priv_.confirm_passphrase.set_entry_sensitive(false);
|
||||||
|
|
||||||
|
let encryption = self.session().unwrap().client().encryption();
|
||||||
|
|
||||||
|
let handle = spawn_tokio!(async move {
|
||||||
|
if is_export {
|
||||||
|
encryption
|
||||||
|
.export_keys(file_path, passphrase.as_str(), |_| true)
|
||||||
|
.await
|
||||||
|
.map(|_| 0usize)
|
||||||
|
.map_err::<Box<dyn std::error::Error + Send>, _>(|error| Box::new(error))
|
||||||
|
} else {
|
||||||
|
encryption
|
||||||
|
.import_keys(file_path, passphrase.as_str())
|
||||||
|
.await
|
||||||
|
.map(|res| res.imported_count)
|
||||||
|
.map_err::<Box<dyn std::error::Error + Send>, _>(|error| Box::new(error))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match handle.await.unwrap() {
|
||||||
|
Ok(nb) => {
|
||||||
|
if is_export {
|
||||||
|
toast!(self, gettext("Room encryption keys exported successfully"));
|
||||||
|
} else {
|
||||||
|
toast!(
|
||||||
|
self,
|
||||||
|
ngettext_f(
|
||||||
|
"Imported 1 room encryption key",
|
||||||
|
"Imported {n} room encryption keys",
|
||||||
|
nb as u32,
|
||||||
|
&[("n", &nb.to_string())]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.clear();
|
||||||
|
self.activate_action("win.close-subpage", None).unwrap();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if is_export {
|
||||||
|
error!("Failed to export the keys: {err:?}");
|
||||||
|
toast!(self, gettext("Could not export the keys"));
|
||||||
|
} else if err
|
||||||
|
.downcast_ref::<RoomKeyImportError>()
|
||||||
|
.filter(|err| {
|
||||||
|
matches!(err, RoomKeyImportError::Export(KeyExportError::InvalidMac))
|
||||||
|
})
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
toast!(
|
||||||
|
self,
|
||||||
|
gettext("The passphrase doesn't match the one used to export the keys.")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
error!("Failed to import the keys: {err:?}");
|
||||||
|
toast!(self, gettext("Could not import the keys"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
priv_.proceed_button.set_loading(false);
|
||||||
|
priv_.file_button.set_sensitive(true);
|
||||||
|
priv_.passphrase.set_entry_sensitive(true);
|
||||||
|
priv_.confirm_passphrase.set_entry_sensitive(true);
|
||||||
|
}
|
||||||
|
}
|
135
src/session/account_settings/security_page/mod.rs
Normal file
135
src/session/account_settings/security_page/mod.rs
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
use adw::{prelude::*, subclass::prelude::*};
|
||||||
|
use gtk::{glib, CompositeTemplate};
|
||||||
|
|
||||||
|
use crate::{components::ButtonRow, session::Session};
|
||||||
|
|
||||||
|
mod import_export_keys_subpage;
|
||||||
|
use import_export_keys_subpage::{ImportExportKeysSubpage, KeysSubpageMode};
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use glib::{subclass::InitializingObject, WeakRef};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, CompositeTemplate)]
|
||||||
|
#[template(resource = "/org/gnome/Fractal/account-settings-security-page.ui")]
|
||||||
|
pub struct SecurityPage {
|
||||||
|
pub session: RefCell<Option<WeakRef<Session>>>,
|
||||||
|
#[template_child]
|
||||||
|
pub import_export_keys_subpage: TemplateChild<ImportExportKeysSubpage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for SecurityPage {
|
||||||
|
const NAME: &'static str = "SecurityPage";
|
||||||
|
type Type = super::SecurityPage;
|
||||||
|
type ParentType = adw::PreferencesPage;
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
ButtonRow::static_type();
|
||||||
|
Self::bind_template(klass);
|
||||||
|
Self::Type::bind_template_callbacks(klass);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_init(obj: &InitializingObject<Self>) {
|
||||||
|
obj.init_template();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for SecurityPage {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![glib::ParamSpecObject::new(
|
||||||
|
"session",
|
||||||
|
"Session",
|
||||||
|
"The session",
|
||||||
|
Session::static_type(),
|
||||||
|
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"session" => obj.set_session(value.get().unwrap()),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"session" => obj.session().to_value(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetImpl for SecurityPage {}
|
||||||
|
impl PreferencesPageImpl for SecurityPage {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
/// Security settings page.
|
||||||
|
pub struct SecurityPage(ObjectSubclass<imp::SecurityPage>)
|
||||||
|
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl SecurityPage {
|
||||||
|
pub fn new(parent_window: &Option<gtk::Window>, session: &Session) -> Self {
|
||||||
|
glib::Object::new(&[("transient-for", parent_window), ("session", session)])
|
||||||
|
.expect("Failed to create SecurityPage")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session(&self) -> Option<Session> {
|
||||||
|
self.imp()
|
||||||
|
.session
|
||||||
|
.borrow()
|
||||||
|
.clone()
|
||||||
|
.and_then(|session| session.upgrade())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_session(&self, session: Option<Session>) {
|
||||||
|
if self.session() == session {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imp()
|
||||||
|
.session
|
||||||
|
.replace(session.map(|session| session.downgrade()));
|
||||||
|
self.notify("session");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn handle_export_keys(&self) {
|
||||||
|
let subpage = &*self.imp().import_export_keys_subpage;
|
||||||
|
subpage.set_mode(KeysSubpageMode::Export);
|
||||||
|
self.root()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| root.downcast_ref::<adw::PreferencesWindow>())
|
||||||
|
.unwrap()
|
||||||
|
.present_subpage(subpage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn handle_import_keys(&self) {
|
||||||
|
let subpage = &*self.imp().import_export_keys_subpage;
|
||||||
|
subpage.set_mode(KeysSubpageMode::Import);
|
||||||
|
self.root()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| root.downcast_ref::<adw::PreferencesWindow>())
|
||||||
|
.unwrap()
|
||||||
|
.present_subpage(subpage);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue