[fractal] account-settings: Allow to import and export room encryption keys
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] account-settings: Allow to import and export room encryption keys
- Date: Sat, 24 Sep 2022 10:30:10 +0000 (UTC)
commit 8dd205ffce65b06857e1eb377ff912870ef23304
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Thu Sep 15 18:52:14 2022 +0200
account-settings: Allow to import and export room encryption keys
Part-of: <https://gitlab.gnome.org/GNOME/fractal/-/merge_requests/1157>
data/resources/resources.gresource.xml | 2 +
.../ui/account-settings-change-password-subpage.ui | 2 +-
.../account-settings-import-export-keys-subpage.ui | 108 +++++
.../resources/ui/account-settings-security-page.ui | 30 ++
data/resources/ui/account-settings.ui | 5 +
po/POTFILES.in | 3 +
.../account_settings/devices_page/device_row.rs | 2 +-
src/session/account_settings/mod.rs | 3 +
.../security_page/import_export_keys_subpage.rs | 451 +++++++++++++++++++++
src/session/account_settings/security_page/mod.rs | 135 ++++++
10 files changed, 739 insertions(+), 2 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 77945325c..1c935894b 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -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>
diff --git a/data/resources/ui/account-settings-change-password-subpage.ui
b/data/resources/ui/account-settings-change-password-subpage.ui
index 38d25f27e..bba7b2306 100644
--- a/data/resources/ui/account-settings-change-password-subpage.ui
+++ b/data/resources/ui/account-settings-change-password-subpage.ui
@@ -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>
diff --git a/data/resources/ui/account-settings-import-export-keys-subpage.ui
b/data/resources/ui/account-settings-import-export-keys-subpage.ui
new file mode 100644
index 000000000..a2fc30576
--- /dev/null
+++ b/data/resources/ui/account-settings-import-export-keys-subpage.ui
@@ -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>
diff --git a/data/resources/ui/account-settings-security-page.ui
b/data/resources/ui/account-settings-security-page.ui
new file mode 100644
index 000000000..4b8852f93
--- /dev/null
+++ b/data/resources/ui/account-settings-security-page.ui
@@ -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>
diff --git a/data/resources/ui/account-settings.ui b/data/resources/ui/account-settings.ui
index c9ca479fa..3140cc89c 100644
--- a/data/resources/ui/account-settings.ui
+++ b/data/resources/ui/account-settings.ui
@@ -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>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 41468cb53..12ef17006 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
diff --git a/src/session/account_settings/devices_page/device_row.rs
b/src/session/account_settings/devices_page/device_row.rs
index 6d657b33f..bd8fa6553 100644
--- a/src/session/account_settings/devices_page/device_row.rs
+++ b/src/session/account_settings/devices_page/device_row.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| {
diff --git a/src/session/account_settings/mod.rs b/src/session/account_settings/mod.rs
index b50a0266e..7d97a9110 100644
--- a/src/session/account_settings/mod.rs
+++ b/src/session/account_settings/mod.rs
@@ -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, _, _| {
diff --git a/src/session/account_settings/security_page/import_export_keys_subpage.rs
b/src/session/account_settings/security_page/import_export_keys_subpage.rs
new file mode 100644
index 000000000..175a2face
--- /dev/null
+++ b/src/session/account_settings/security_page/import_export_keys_subpage.rs
@@ -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);
+ }
+}
diff --git a/src/session/account_settings/security_page/mod.rs
b/src/session/account_settings/security_page/mod.rs
new file mode 100644
index 000000000..7154e7ca7
--- /dev/null
+++ b/src/session/account_settings/security_page/mod.rs
@@ -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);
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]