[fractal] account-settings: Allow to import and export room encryption keys



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]