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-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-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.ui">ui/account-settings.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="attachment-dialog.ui">ui/attachment-dialog.ui</file>
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<style>
|
||||
<class name="body"/>
|
||||
</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-mode">word-char</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>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="SecurityPage">
|
||||
<property name="session" bind-source="AccountSettings" bind-property="session" bind-flags="sync-create"/>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</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-device-row.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-security-page.ui
|
||||
data/resources/ui/account-settings.ui
|
||||
data/resources/ui/attachment-dialog.ui
|
||||
data/resources/ui/components-auth-dialog.ui
|
||||
|
@ -51,6 +53,7 @@ src/login/mod.rs
|
|||
src/secret.rs
|
||||
src/session/account_settings/devices_page/device_list.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/deactivate_account_subpage.rs
|
||||
src/session/account_settings/user_page/mod.rs
|
||||
|
|
|
@ -225,7 +225,7 @@ impl DeviceRow {
|
|||
self.imp().delete_logout_button.set_loading(true);
|
||||
|
||||
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.connect_response(
|
||||
clone!(@weak self as obj, @weak dialog => move |_, response| {
|
||||
|
|
|
@ -6,8 +6,10 @@ use gtk::{
|
|||
};
|
||||
|
||||
mod devices_page;
|
||||
mod security_page;
|
||||
mod user_page;
|
||||
use devices_page::DevicesPage;
|
||||
use security_page::SecurityPage;
|
||||
use user_page::UserPage;
|
||||
|
||||
use super::Session;
|
||||
|
@ -35,6 +37,7 @@ mod imp {
|
|||
fn class_init(klass: &mut Self::Class) {
|
||||
DevicesPage::static_type();
|
||||
UserPage::static_type();
|
||||
SecurityPage::static_type();
|
||||
Self::bind_template(klass);
|
||||
|
||||
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