2022-09-15 16:52:14 +00:00
use adw ::{ prelude ::* , subclass ::prelude ::* } ;
use gettextrs ::gettext ;
use gtk ::{
gio ,
glib ::{ self , clone } ,
CompositeTemplate ,
} ;
2023-03-16 13:34:18 +00:00
use log ::{ debug , error } ;
2022-09-15 16:52:14 +00:00
use matrix_sdk ::encryption ::{ KeyExportError , RoomKeyImportError } ;
use crate ::{
2023-05-21 14:52:51 +00:00
components ::SpinnerButton , ngettext_f , session ::model ::Session , spawn , spawn_tokio , toast ,
2022-09-15 16:52:14 +00:00
} ;
2022-10-26 11:01:33 +00:00
#[ derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum) ]
2022-09-15 16:52:14 +00:00
#[ repr(u32) ]
#[ enum_type(name = " KeysSubpageMode " ) ]
pub enum KeysSubpageMode {
2022-10-26 11:01:33 +00:00
#[ default ]
2022-09-15 16:52:14 +00:00
Export = 0 ,
Import = 1 ,
}
mod imp {
use std ::cell ::{ Cell , RefCell } ;
use glib ::{ subclass ::InitializingObject , WeakRef } ;
use super ::* ;
#[ derive(Debug, Default, CompositeTemplate) ]
2023-05-21 21:11:45 +00:00
#[ template(
resource = " /org/gnome/Fractal/ui/session/view/account_settings/security_page/import_export_keys_subpage.ui "
) ]
2022-09-15 16:52:14 +00:00
pub struct ImportExportKeysSubpage {
2022-09-28 15:32:50 +00:00
pub session : WeakRef < Session > ,
2022-09-15 16:52:14 +00:00
#[ template_child ]
pub title : TemplateChild < gtk ::Label > ,
#[ template_child ]
pub description : TemplateChild < gtk ::Label > ,
#[ template_child ]
pub instructions : TemplateChild < gtk ::Label > ,
#[ template_child ]
2022-10-07 16:54:13 +00:00
pub passphrase : TemplateChild < adw ::PasswordEntryRow > ,
2022-09-15 16:52:14 +00:00
#[ template_child ]
2022-10-07 16:54:13 +00:00
pub confirm_passphrase_box : TemplateChild < gtk ::Box > ,
#[ template_child ]
pub confirm_passphrase : TemplateChild < adw ::PasswordEntryRow > ,
#[ template_child ]
pub confirm_passphrase_error_revealer : TemplateChild < gtk ::Revealer > ,
#[ template_child ]
pub confirm_passphrase_error : TemplateChild < gtk ::Label > ,
2022-09-15 16:52:14 +00:00
#[ 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! [
2022-10-26 10:25:41 +00:00
glib ::ParamSpecObject ::builder ::< Session > ( " session " ) . build ( ) ,
glib ::ParamSpecString ::builder ( " file-path " )
. read_only ( )
. build ( ) ,
2023-02-15 05:23:49 +00:00
glib ::ParamSpecEnum ::builder ::< KeysSubpageMode > ( " mode " )
2022-10-26 10:25:41 +00:00
. explicit_notify ( )
. build ( ) ,
2022-09-15 16:52:14 +00:00
]
} ) ;
PROPERTIES . as_ref ( )
}
2022-10-25 11:59:39 +00:00
fn set_property ( & self , _id : usize , value : & glib ::Value , pspec : & glib ::ParamSpec ) {
let obj = self . obj ( ) ;
2022-09-15 16:52:14 +00:00
match pspec . name ( ) {
" session " = > obj . set_session ( value . get ( ) . unwrap ( ) ) ,
" mode " = > obj . set_mode ( value . get ( ) . unwrap ( ) ) ,
_ = > unimplemented! ( ) ,
}
}
2022-10-25 11:59:39 +00:00
fn property ( & self , _id : usize , pspec : & glib ::ParamSpec ) -> glib ::Value {
let obj = self . obj ( ) ;
2022-09-15 16:52:14 +00:00
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! ( ) ,
}
}
2022-10-25 11:59:39 +00:00
fn constructed ( & self ) {
self . parent_constructed ( ) ;
let obj = self . obj ( ) ;
2022-09-15 16:52:14 +00:00
self . passphrase
. connect_changed ( clone! ( @ weak obj = > move | _ | {
2022-10-07 16:54:13 +00:00
obj . validate_passphrase_confirmation ( ) ;
2022-09-15 16:52:14 +00:00
} ) ) ;
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 {
2022-10-25 11:59:39 +00:00
glib ::Object ::builder ( ) . property ( " session " , session ) . build ( )
2022-09-15 16:52:14 +00:00
}
2022-10-26 10:25:41 +00:00
/// The current session.
2022-09-15 16:52:14 +00:00
pub fn session ( & self ) -> Option < Session > {
2022-09-28 15:32:50 +00:00
self . imp ( ) . session . upgrade ( )
2022-09-15 16:52:14 +00:00
}
2022-10-26 10:25:41 +00:00
/// Set the current session.
2022-09-15 16:52:14 +00:00
pub fn set_session ( & self , session : Option < Session > ) {
2022-09-28 15:32:50 +00:00
self . imp ( ) . session . set ( session . as_ref ( ) ) ;
2022-09-15 16:52:14 +00:00
}
2022-10-26 10:25:41 +00:00
/// The path to export the keys to.
2022-09-15 16:52:14 +00:00
pub fn file_path ( & self ) -> Option < gio ::File > {
self . imp ( ) . file_path . borrow ( ) . clone ( )
}
2022-10-26 10:25:41 +00:00
/// Set the path to export the keys to.
fn set_file_path ( & self , path : Option < gio ::File > ) {
2022-10-26 10:43:59 +00:00
let imp = self . imp ( ) ;
if imp . file_path . borrow ( ) . as_ref ( ) = = path . as_ref ( ) {
2022-09-15 16:52:14 +00:00
return ;
}
2022-10-26 10:43:59 +00:00
imp . file_path . replace ( path ) ;
2022-09-15 16:52:14 +00:00
self . update_button ( ) ;
self . notify ( " file-path " ) ;
}
2022-10-26 10:25:41 +00:00
/// The export/import mode of the subpage.
2022-09-15 16:52:14 +00:00
pub fn mode ( & self ) -> KeysSubpageMode {
self . imp ( ) . mode . get ( )
}
2022-10-26 10:25:41 +00:00
/// Set the export/import mode of the subpage.
2022-09-15 16:52:14 +00:00
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 ) {
2022-10-26 10:43:59 +00:00
let imp = self . imp ( ) ;
2022-09-15 16:52:14 +00:00
self . set_file_path ( None ) ;
2022-10-26 10:43:59 +00:00
imp . passphrase . set_text ( " " ) ;
imp . confirm_passphrase . set_text ( " " ) ;
2022-09-15 16:52:14 +00:00
}
fn update_for_mode ( & self ) {
2022-10-26 10:43:59 +00:00
let imp = self . imp ( ) ;
2022-09-15 16:52:14 +00:00
if self . mode ( ) = = KeysSubpageMode ::Export {
2022-10-26 10:43:59 +00:00
imp . title . set_label ( & gettext ( " Export Room Encryption Keys " ) ) ;
imp . description . set_label ( & gettext (
2022-09-15 16:52:14 +00:00
" 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. " ,
) ) ;
2022-10-26 10:43:59 +00:00
imp . instructions . set_label ( & gettext (
2022-09-15 16:52:14 +00:00
" 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. " ,
) ) ;
2023-03-16 11:12:54 +00:00
imp . confirm_passphrase_box . set_visible ( true ) ;
2022-10-26 10:43:59 +00:00
imp . proceed_button . set_label ( & gettext ( " Export Keys " ) ) ;
2022-09-15 16:52:14 +00:00
} else {
2022-10-26 10:43:59 +00:00
imp . title . set_label ( & gettext ( " Import Room Encryption Keys " ) ) ;
imp . description . set_label ( & gettext (
2022-09-15 16:52:14 +00:00
" 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. " ,
) ) ;
2022-10-26 10:43:59 +00:00
imp . instructions . set_label ( & gettext (
2022-09-15 16:52:14 +00:00
" Enter the passphrase provided when the backup file was created. " ,
) ) ;
2023-03-16 11:12:54 +00:00
imp . confirm_passphrase_box . set_visible ( false ) ;
2022-10-26 10:43:59 +00:00
imp . proceed_button . set_label ( & gettext ( " Import Keys " ) ) ;
2022-09-15 16:52:14 +00:00
}
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 ;
2023-03-16 13:34:18 +00:00
let dialog = gtk ::FileDialog ::builder ( )
2022-09-15 16:52:14 +00:00
. modal ( true )
2023-03-16 13:34:18 +00:00
. accept_label ( gettext ( " Choose " ) )
2022-09-15 16:52:14 +00:00
. build ( ) ;
if let Some ( file ) = self . file_path ( ) {
2023-03-16 13:34:18 +00:00
dialog . set_initial_file ( Some ( & file ) ) ;
2022-09-15 16:52:14 +00:00
} else if is_export {
// Translators: Do no translate "fractal" as it is the application
// name.
2023-03-16 13:34:18 +00:00
dialog . set_initial_name ( Some ( & format! ( " {} .txt " , gettext ( " fractal-encryption-keys " ) ) ) ) ;
2022-09-15 16:52:14 +00:00
}
2023-03-16 13:34:18 +00:00
let parent_window = self . root ( ) . and_then ( | r | r . downcast ::< gtk ::Window > ( ) . ok ( ) ) ;
let res = if is_export {
dialog . set_title ( & gettext ( " Save Encryption Keys To… " ) ) ;
dialog . save_future ( parent_window . as_ref ( ) ) . await
} else {
dialog . set_title ( & gettext ( " Import Encryption Keys From… " ) ) ;
dialog . open_future ( parent_window . as_ref ( ) ) . await
} ;
match res {
Ok ( file ) = > {
2022-09-15 16:52:14 +00:00
self . set_file_path ( Some ( file ) ) ;
}
2023-03-16 13:34:18 +00:00
Err ( error ) = > {
if error . matches ( gtk ::DialogError ::Dismissed ) {
debug! ( " File dialog dismissed by user " ) ;
} else {
error! ( " Could not access file: {error:?} " ) ;
toast! ( self , gettext ( " Could not access file " ) ) ;
}
}
} ;
2022-09-15 16:52:14 +00:00
}
fn validate_passphrase_confirmation ( & self ) {
2022-10-26 10:43:59 +00:00
let imp = self . imp ( ) ;
let entry = & imp . confirm_passphrase ;
let revealer = & imp . confirm_passphrase_error_revealer ;
let label = & imp . confirm_passphrase_error ;
let passphrase = imp . passphrase . text ( ) ;
2022-09-15 16:52:14 +00:00
let confirmation = entry . text ( ) ;
if confirmation . is_empty ( ) {
2022-10-07 16:54:13 +00:00
revealer . set_reveal_child ( false ) ;
2022-09-15 16:52:14 +00:00
entry . remove_css_class ( " success " ) ;
entry . remove_css_class ( " warning " ) ;
return ;
}
if passphrase = = confirmation {
2022-10-07 16:54:13 +00:00
revealer . set_reveal_child ( false ) ;
2022-09-15 16:52:14 +00:00
entry . add_css_class ( " success " ) ;
entry . remove_css_class ( " warning " ) ;
} else {
2022-10-07 16:54:13 +00:00
label . set_label ( & gettext ( " Passphrases do not match " ) ) ;
revealer . set_reveal_child ( true ) ;
2022-09-15 16:52:14 +00:00
entry . remove_css_class ( " success " ) ;
entry . add_css_class ( " warning " ) ;
}
self . update_button ( ) ;
}
fn update_button ( & self ) {
self . imp ( ) . proceed_button . set_sensitive ( self . can_proceed ( ) ) ;
}
fn can_proceed ( & self ) -> bool {
2022-10-26 10:43:59 +00:00
let imp = self . imp ( ) ;
let file_path = imp . file_path . borrow ( ) ;
let passphrase = imp . passphrase . text ( ) ;
2022-09-15 16:52:14 +00:00
let mut res = file_path
. as_ref ( )
. filter ( | file | file . path ( ) . is_some ( ) )
. is_some ( )
& & ! passphrase . is_empty ( ) ;
if self . mode ( ) = = KeysSubpageMode ::Export {
2022-10-26 10:43:59 +00:00
let confirmation = imp . confirm_passphrase . text ( ) ;
2022-09-15 16:52:14 +00:00
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 ;
}
2022-10-26 10:43:59 +00:00
let imp = self . imp ( ) ;
2022-09-15 16:52:14 +00:00
let file_path = self . file_path ( ) . and_then ( | file | file . path ( ) ) . unwrap ( ) ;
2022-10-26 10:43:59 +00:00
let passphrase = imp . passphrase . text ( ) ;
2022-09-15 16:52:14 +00:00
let is_export = self . mode ( ) = = KeysSubpageMode ::Export ;
2022-10-26 10:43:59 +00:00
imp . proceed_button . set_loading ( true ) ;
imp . file_button . set_sensitive ( false ) ;
imp . passphrase . set_sensitive ( false ) ;
imp . confirm_passphrase . set_sensitive ( false ) ;
2022-09-15 16:52:14 +00:00
let encryption = self . session ( ) . unwrap ( ) . client ( ) . encryption ( ) ;
let handle = spawn_tokio! ( async move {
if is_export {
encryption
2022-10-04 15:03:01 +00:00
. export_room_keys ( file_path , passphrase . as_str ( ) , | _ | true )
2022-09-15 16:52:14 +00:00
. await
. map ( | _ | 0 usize )
. map_err ::< Box < dyn std ::error ::Error + Send > , _ > ( | error | Box ::new ( error ) )
} else {
encryption
2022-10-04 15:03:01 +00:00
. import_room_keys ( file_path , passphrase . as_str ( ) )
2022-09-15 16:52:14 +00:00
. 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 " ) ) ;
}
}
}
2022-10-26 10:43:59 +00:00
imp . proceed_button . set_loading ( false ) ;
imp . file_button . set_sensitive ( true ) ;
imp . passphrase . set_sensitive ( true ) ;
imp . confirm_passphrase . set_sensitive ( true ) ;
2022-09-15 16:52:14 +00:00
}
}