From 837de4063dbe398735a5b1d35bde1821c177b555 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sat, 11 May 2019 09:39:02 -0600 Subject: OMEMO: Move files to fitting subdirectory --- plugins/omemo/src/ui/account_settings_entry.vala | 26 +++ plugins/omemo/src/ui/account_settings_widget.vala | 54 +++++ plugins/omemo/src/ui/contact_details_dialog.vala | 253 +++++++++++++++++++++ plugins/omemo/src/ui/contact_details_provider.vala | 48 ++++ .../src/ui/device_notification_populator.vala | 99 ++++++++ plugins/omemo/src/ui/encryption_list_entry.vala | 23 ++ plugins/omemo/src/ui/manage_key_dialog.vala | 166 ++++++++++++++ plugins/omemo/src/ui/own_notifications.vala | 42 ++++ plugins/omemo/src/ui/util.vala | 60 +++++ 9 files changed, 771 insertions(+) create mode 100644 plugins/omemo/src/ui/account_settings_entry.vala create mode 100644 plugins/omemo/src/ui/account_settings_widget.vala create mode 100644 plugins/omemo/src/ui/contact_details_dialog.vala create mode 100644 plugins/omemo/src/ui/contact_details_provider.vala create mode 100644 plugins/omemo/src/ui/device_notification_populator.vala create mode 100644 plugins/omemo/src/ui/encryption_list_entry.vala create mode 100644 plugins/omemo/src/ui/manage_key_dialog.vala create mode 100644 plugins/omemo/src/ui/own_notifications.vala create mode 100644 plugins/omemo/src/ui/util.vala (limited to 'plugins/omemo/src/ui') diff --git a/plugins/omemo/src/ui/account_settings_entry.vala b/plugins/omemo/src/ui/account_settings_entry.vala new file mode 100644 index 00000000..3866febe --- /dev/null +++ b/plugins/omemo/src/ui/account_settings_entry.vala @@ -0,0 +1,26 @@ +namespace Dino.Plugins.Omemo { + +public class AccountSettingsEntry : Plugins.AccountSettingsEntry { + private Plugin plugin; + + public AccountSettingsEntry(Plugin plugin) { + this.plugin = plugin; + } + + public override string id { get { + return "omemo_identity_key"; + }} + + public override string name { get { + return "OMEMO"; + }} + + public override Plugins.AccountSettingsWidget? get_widget(WidgetType type) { + if (type == WidgetType.GTK) { + return new AccountSettingWidget(plugin); + } + return null; + } +} + +} \ No newline at end of file diff --git a/plugins/omemo/src/ui/account_settings_widget.vala b/plugins/omemo/src/ui/account_settings_widget.vala new file mode 100644 index 00000000..6148da56 --- /dev/null +++ b/plugins/omemo/src/ui/account_settings_widget.vala @@ -0,0 +1,54 @@ +using Gtk; +using Dino.Entities; + +namespace Dino.Plugins.Omemo { + +public class AccountSettingWidget : Plugins.AccountSettingsWidget, Box { + private Plugin plugin; + private Label fingerprint; + private Account account; + private Button btn; + + public AccountSettingWidget(Plugin plugin) { + this.plugin = plugin; + + fingerprint = new Label("..."); + fingerprint.xalign = 0; + Border border = new Button().get_style_context().get_padding(StateFlags.NORMAL); + fingerprint.margin_top = border.top + 1; + fingerprint.margin_start = border.left + 1; + fingerprint.visible = true; + pack_start(fingerprint); + + btn = new Button(); + btn.image = new Image.from_icon_name("view-list-symbolic", IconSize.BUTTON); + btn.relief = ReliefStyle.NONE; + btn.visible = false; + btn.valign = Align.CENTER; + btn.clicked.connect(() => { + activated(); + ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, account, account.bare_jid); + dialog.set_transient_for((Window) get_toplevel()); + dialog.present(); + }); + pack_start(btn, false); + } + + public void set_account(Account account) { + this.account = account; + btn.visible = false; + Qlite.Row? row = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id).inner; + if (row == null) { + fingerprint.set_markup("%s\n%s".printf(_("Own fingerprint"), _("Will be generated on first connect"))); + } else { + string res = fingerprint_markup(fingerprint_from_base64(((!)row)[plugin.db.identity.identity_key_public_base64])); + fingerprint.set_markup("%s\n%s".printf(_("Own fingerprint"), res)); + btn.visible = true; + } + } + + public void deactivate() { + } +} + +} diff --git a/plugins/omemo/src/ui/contact_details_dialog.vala b/plugins/omemo/src/ui/contact_details_dialog.vala new file mode 100644 index 00000000..6899acf6 --- /dev/null +++ b/plugins/omemo/src/ui/contact_details_dialog.vala @@ -0,0 +1,253 @@ +using Gtk; +using Xmpp; +using Gee; +using Qlite; +using Dino.Entities; +using Qrencode; +using Gdk; + +namespace Dino.Plugins.Omemo { + +[GtkTemplate (ui = "/im/dino/Dino/omemo/contact_details_dialog.ui")] +public class ContactDetailsDialog : Gtk.Dialog { + + private Plugin plugin; + private Account account; + private Jid jid; + private bool own = false; + private int own_id = 0; + + [GtkChild] private Label automatically_accept_new_descr; + [GtkChild] private Box own_fingerprint_container; + [GtkChild] private Label own_fingerprint_label; + [GtkChild] private Box new_keys_container; + [GtkChild] private ListBox new_keys_listbox; + [GtkChild] private Box keys_container; + [GtkChild] private ListBox keys_listbox; + [GtkChild] private Switch auto_accept_switch; + [GtkChild] private Button copy_button; + [GtkChild] private Button show_qrcode_button; + [GtkChild] private Image qrcode_image; + [GtkChild] private Popover qrcode_popover; + + public ContactDetailsDialog(Plugin plugin, Account account, Jid jid) { + Object(use_header_bar : Environment.get_variable("GTK_CSD") != "0" ? 1 : 0); + this.plugin = plugin; + this.account = account; + this.jid = jid; + + if (Environment.get_variable("GTK_CSD") != "0") { + (get_header_bar() as HeaderBar).set_subtitle(jid.bare_jid.to_string()); + } + + int identity_id = plugin.db.identity.get_id(account.id); + if (identity_id < 0) return; + + // Dialog opened from the account settings menu + // Show the fingerprint for this device separately with buttons for a qrcode and to copy + if(jid.equals(account.bare_jid)) { + own = true; + own_id = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id]; + + automatically_accept_new_descr.label = _("When you add new encryption keys to your account, automatically accept them."); + + own_fingerprint_container.visible = true; + + string own_b64 = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.identity_key_public_base64]; + string fingerprint = fingerprint_from_base64(own_b64); + own_fingerprint_label.set_markup(fingerprint_markup(fingerprint)); + + copy_button.clicked.connect(() => {Clipboard.get_default(get_display()).set_text(fingerprint, fingerprint.length);}); + + int sid = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id]; + Pixbuf qr_pixbuf = new QRcode(@"xmpp:$(account.bare_jid)?omemo-sid-$(sid)=$(fingerprint)", 2).to_pixbuf(); + qr_pixbuf = qr_pixbuf.scale_simple(150, 150, InterpType.NEAREST); + + Pixbuf pixbuf = new Pixbuf( + qr_pixbuf.colorspace, + qr_pixbuf.has_alpha, + qr_pixbuf.bits_per_sample, + 170, + 170 + ); + pixbuf.fill(uint32.MAX); + qr_pixbuf.copy_area(0, 0, 150, 150, pixbuf, 10, 10); + + qrcode_image.set_from_pixbuf(pixbuf); + show_qrcode_button.clicked.connect(qrcode_popover.popup); + } + + new_keys_listbox.set_header_func(header_function); + + keys_listbox.set_header_func(header_function); + + //Show any new devices for which the user must decide whether to accept or reject + foreach (Row device in plugin.db.identity_meta.get_new_devices(identity_id, jid.to_string())) { + add_new_fingerprint(device); + } + + //Show the normal devicelist + foreach (Row device in plugin.db.identity_meta.get_known_devices(identity_id, jid.to_string())) { + if(own && device[plugin.db.identity_meta.device_id] == own_id) { + continue; + } + add_fingerprint(device, (Database.IdentityMetaTable.TrustLevel) device[plugin.db.identity_meta.trust_level]); + } + + auto_accept_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string())); + + auto_accept_switch.state_set.connect((active) => { + plugin.trust_manager.set_blind_trust(account, jid, active); + + if (active) { + new_keys_container.visible = false; + + foreach (Row device in plugin.db.identity_meta.get_new_devices(identity_id, jid.to_string())) { + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], Database.IdentityMetaTable.TrustLevel.TRUSTED); + add_fingerprint(device, Database.IdentityMetaTable.TrustLevel.TRUSTED); + } + } + + return false; + }); + + } + + private void header_function(ListBoxRow row, ListBoxRow? before) { + if (row.get_header() == null && before != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + } + + private void set_row(int trust, bool now_active, Image img, Label status_lbl, Label lbl, ListBoxRow lbr){ + switch(trust) { + case Database.IdentityMetaTable.TrustLevel.TRUSTED: + img.icon_name = "emblem-ok-symbolic"; + status_lbl.set_markup("%s".printf(_("Accepted"))); + lbl.get_style_context().remove_class("dim-label"); + break; + case Database.IdentityMetaTable.TrustLevel.UNTRUSTED: + img.icon_name = "action-unavailable-symbolic"; + status_lbl.set_markup("%s".printf(_("Rejected"))); + lbl.get_style_context().add_class("dim-label"); + break; + case Database.IdentityMetaTable.TrustLevel.VERIFIED: + img.icon_name = "security-high-symbolic"; + status_lbl.set_markup("%s".printf(_("Verified"))); + lbl.get_style_context().remove_class("dim-label"); + break; + } + + if (!now_active) { + img.icon_name = "appointment-missed-symbolic"; + status_lbl.set_markup("%s".printf(_("Unused"))); + lbr.activatable = false; + } + } + + private void add_fingerprint(Row device, Database.IdentityMetaTable.TrustLevel trust) { + keys_container.visible = true; + + ListBoxRow lbr = new ListBoxRow() { visible = true, activatable = true, hexpand = true }; + Box box = new Box(Gtk.Orientation.HORIZONTAL, 40) { visible = true, margin_start = 20, margin_end = 20, margin_top = 14, margin_bottom = 14, hexpand = true }; + + Box status_box = new Box(Gtk.Orientation.HORIZONTAL, 5) { visible = true, hexpand = true }; + Label status_lbl = new Label(null) { visible = true, hexpand = true, xalign = 0 }; + + Image img = new Image() { visible = true, halign = Align.END, icon_size = IconSize.BUTTON }; + + string res = fingerprint_markup(fingerprint_from_base64(device[plugin.db.identity_meta.identity_key_public_base64])); + Label lbl = new Label(res) + { use_markup=true, justify=Justification.RIGHT, visible=true, halign = Align.START, valign = Align.CENTER, hexpand = false }; + + set_row(trust, device[plugin.db.identity_meta.now_active], img, status_lbl, lbl, lbr); + + box.add(lbl); + box.add(status_box); + + status_box.add(status_lbl); + status_box.add(img); + + lbr.add(box); + keys_listbox.add(lbr); + + //Row clicked - pull the most up to date device info from the database and show the manage window + keys_listbox.row_activated.connect((row) => { + if(row == lbr) { + Row updated_device = plugin.db.identity_meta.get_device(device[plugin.db.identity_meta.identity_id], device[plugin.db.identity_meta.address_name], device[plugin.db.identity_meta.device_id]); + ManageKeyDialog manage_dialog = new ManageKeyDialog(updated_device, plugin.db); + manage_dialog.set_transient_for((Gtk.Window) get_toplevel()); + manage_dialog.present(); + manage_dialog.response.connect((response) => { + set_row(response, device[plugin.db.identity_meta.now_active], img, status_lbl, lbl, lbr); + update_device(response, device); + }); + } + }); + } + + private void update_device(int response, Row device){ + switch (response) { + case Database.IdentityMetaTable.TrustLevel.TRUSTED: + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], Database.IdentityMetaTable.TrustLevel.TRUSTED); + break; + case Database.IdentityMetaTable.TrustLevel.UNTRUSTED: + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], Database.IdentityMetaTable.TrustLevel.UNTRUSTED); + break; + case Database.IdentityMetaTable.TrustLevel.VERIFIED: + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], Database.IdentityMetaTable.TrustLevel.VERIFIED); + plugin.trust_manager.set_blind_trust(account, jid, false); + auto_accept_switch.set_active(false); + break; + } + } + + private void add_new_fingerprint(Row device){ + new_keys_container.visible = true; + + ListBoxRow lbr = new ListBoxRow() { visible = true, activatable = false, hexpand = true }; + Box box = new Box(Gtk.Orientation.HORIZONTAL, 40) { visible = true, margin_start = 20, margin_end = 20, margin_top = 14, margin_bottom = 14, hexpand = true }; + + Box control_box = new Box(Gtk.Orientation.HORIZONTAL, 0) { visible = true, hexpand = true }; + + Button yes_button = new Button() { visible = true, valign = Align.CENTER, hexpand = true }; + yes_button.image = new Image.from_icon_name("emblem-ok-symbolic", IconSize.BUTTON); + yes_button.get_style_context().add_class("suggested-action"); + + Button no_button = new Button() { visible = true, valign = Align.CENTER, hexpand = true }; + no_button.image = new Image.from_icon_name("action-unavailable-symbolic", IconSize.BUTTON); + no_button.get_style_context().add_class("destructive-action"); + + yes_button.clicked.connect(() => { + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], Database.IdentityMetaTable.TrustLevel.TRUSTED); + add_fingerprint(device, Database.IdentityMetaTable.TrustLevel.TRUSTED); + new_keys_listbox.remove(lbr); + if (new_keys_listbox.get_children().length() < 1) new_keys_container.visible = false; + }); + + no_button.clicked.connect(() => { + plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], Database.IdentityMetaTable.TrustLevel.UNTRUSTED); + add_fingerprint(device, Database.IdentityMetaTable.TrustLevel.UNTRUSTED); + new_keys_listbox.remove(lbr); + if (new_keys_listbox.get_children().length() < 1) new_keys_container.visible = false; + }); + + string res = fingerprint_markup(fingerprint_from_base64(device[plugin.db.identity_meta.identity_key_public_base64])); + Label lbl = new Label(res) + { use_markup=true, justify=Justification.RIGHT, visible=true, halign = Align.START, valign = Align.CENTER, hexpand = false }; + + + box.add(lbl); + + control_box.add(yes_button); + control_box.add(no_button); + control_box.get_style_context().add_class("linked"); + + box.add(control_box); + + lbr.add(box); + new_keys_listbox.add(lbr); + } +} + +} diff --git a/plugins/omemo/src/ui/contact_details_provider.vala b/plugins/omemo/src/ui/contact_details_provider.vala new file mode 100644 index 00000000..7250d135 --- /dev/null +++ b/plugins/omemo/src/ui/contact_details_provider.vala @@ -0,0 +1,48 @@ +using Gtk; +using Gee; +using Qlite; +using Dino.Entities; + +namespace Dino.Plugins.Omemo { + +public class ContactDetailsProvider : Plugins.ContactDetailsProvider, Object { + public string id { get { return "omemo_info"; } } + + private Plugin plugin; + + public ContactDetailsProvider(Plugin plugin) { + this.plugin = plugin; + } + + public void populate(Conversation conversation, Plugins.ContactDetails contact_details, WidgetType type) { + if (conversation.type_ == Conversation.Type.CHAT && type == WidgetType.GTK) { + + int identity_id = plugin.db.identity.get_id(conversation.account.id); + if (identity_id < 0) return; + + int i = 0; + foreach (Row row in plugin.db.identity_meta.with_address(identity_id, conversation.counterpart.to_string())) { + if (row[plugin.db.identity_meta.identity_key_public_base64] != null) { + i++; + } + } + + if (i > 0) { + Button btn = new Button.from_icon_name("view-list-symbolic") { visible = true, valign = Align.CENTER, relief = ReliefStyle.NONE }; + btn.clicked.connect(() => { + btn.activate(); + ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, conversation.counterpart); + dialog.set_transient_for((Window) btn.get_toplevel()); + dialog.response.connect((response_type) => { + plugin.device_notification_populator.should_hide(); + }); + dialog.present(); + }); + + contact_details.add(_("Encryption"), "OMEMO", n("%d OMEMO device", "%d OMEMO devices", i).printf(i), btn); + } + } + } +} + +} diff --git a/plugins/omemo/src/ui/device_notification_populator.vala b/plugins/omemo/src/ui/device_notification_populator.vala new file mode 100644 index 00000000..5b47611c --- /dev/null +++ b/plugins/omemo/src/ui/device_notification_populator.vala @@ -0,0 +1,99 @@ +using Dino.Entities; +using Xmpp; +using Gtk; + +namespace Dino.Plugins.Omemo { + +public class DeviceNotificationPopulator : NotificationPopulator, Object { + + public string id { get { return "device_notification"; } } + + private StreamInteractor? stream_interactor; + private Plugin plugin; + private Conversation? current_conversation; + private NotificationCollection? notification_collection; + private ConversationNotification notification; + + public DeviceNotificationPopulator(Plugin plugin, StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + this.plugin = plugin; + + stream_interactor.account_added.connect(on_account_added); + } + + public bool has_new_devices(Jid jid) { + int identity_id = plugin.db.identity.get_id(current_conversation.account.id); + if (identity_id < 0) return false; + return plugin.db.identity_meta.get_new_devices(identity_id, jid.bare_jid.to_string()).count() > 0; + } + + public void init(Conversation conversation, NotificationCollection notification_collection, Plugins.WidgetType type) { + current_conversation = conversation; + this.notification_collection = notification_collection; + if (has_new_devices(conversation.counterpart) && conversation.type_ == Conversation.Type.CHAT) { + display_notification(); + } + } + + public void close(Conversation conversation) { + notification = null; + } + + private void display_notification() { + if (notification == null) { + notification = new ConversationNotification(plugin, current_conversation.account, current_conversation.counterpart); + notification.should_hide.connect(should_hide); + notification_collection.add_meta_notification(notification); + } + } + + public void should_hide() { + if (!has_new_devices(current_conversation.counterpart) && notification != null){ + notification_collection.remove_meta_notification(notification); + notification = null; + } + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { + if (current_conversation != null && jid.equals(current_conversation.counterpart) && has_new_devices(current_conversation.counterpart)) { + display_notification(); + } + }); + } +} + +private class ConversationNotification : MetaConversationNotification { + private Widget widget; + private Plugin plugin; + private Jid jid; + private Account account; + public signal void should_hide(); + + public ConversationNotification(Plugin plugin, Account account, Jid jid) { + this.plugin = plugin; + this.jid = jid; + this.account = account; + + Box box = new Box(Orientation.HORIZONTAL, 5) { visible=true }; + Button manage_button = new Button() { label=_("Manage"), visible=true }; + manage_button.clicked.connect(() => { + manage_button.activate(); + ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, account, jid); + dialog.set_transient_for((Window) manage_button.get_toplevel()); + dialog.response.connect((response_type) => { + should_hide(); + }); + dialog.present(); + }); + box.add(new Label(_("This contact has new devices")) { margin_end=10, visible=true }); + box.add(manage_button); + widget = box; + } + + public override Object? get_widget(WidgetType type) { + return widget; + } +} + +} diff --git a/plugins/omemo/src/ui/encryption_list_entry.vala b/plugins/omemo/src/ui/encryption_list_entry.vala new file mode 100644 index 00000000..2e8905e2 --- /dev/null +++ b/plugins/omemo/src/ui/encryption_list_entry.vala @@ -0,0 +1,23 @@ +namespace Dino.Plugins.Omemo { + +public class EncryptionListEntry : Plugins.EncryptionListEntry, Object { + private Plugin plugin; + + public EncryptionListEntry(Plugin plugin) { + this.plugin = plugin; + } + + public Entities.Encryption encryption { get { + return Entities.Encryption.OMEMO; + }} + + public string name { get { + return "OMEMO"; + }} + + public bool can_encrypt(Entities.Conversation conversation) { + return plugin.app.stream_interactor.get_module(Manager.IDENTITY).can_encrypt(conversation); + } +} + +} diff --git a/plugins/omemo/src/ui/manage_key_dialog.vala b/plugins/omemo/src/ui/manage_key_dialog.vala new file mode 100644 index 00000000..87d43de8 --- /dev/null +++ b/plugins/omemo/src/ui/manage_key_dialog.vala @@ -0,0 +1,166 @@ +using Gtk; +using Qlite; + +namespace Dino.Plugins.Omemo { + +[GtkTemplate (ui = "/im/dino/Dino/omemo/manage_key_dialog.ui")] +public class ManageKeyDialog : Gtk.Dialog { + + [GtkChild] private Stack manage_stack; + + [GtkChild] private Button cancel_button; + [GtkChild] private Button ok_button; + + [GtkChild] private Label main_desc_label; + [GtkChild] private ListBox main_action_list; + + [GtkChild] private Image confirm_image; + [GtkChild] private Label confirm_title_label; + [GtkChild] private Label confirm_desc_label; + + [GtkChild] private Label verify_label; + [GtkChild] private Button verify_yes_button; + [GtkChild] private Button verify_no_button; + + private Row device; + private Database db; + + private bool return_to_main; + private int current_response; + + public ManageKeyDialog(Row device, Database db) { + Object(use_header_bar : Environment.get_variable("GTK_CSD") != "0" ? 1 : 0); + + this.device = device; + this.db = db; + + setup_main_screen(); + setup_verify_screen(); + + cancel_button.clicked.connect(handle_cancel); + ok_button.clicked.connect(() => { + response(current_response); + close(); + }); + + verify_yes_button.clicked.connect(() => { + confirm_image.set_from_icon_name("security-high-symbolic", IconSize.DIALOG); + confirm_title_label.label = _("Verify key"); + confirm_desc_label.set_markup(_("Once confirmed, any future messages sent by %s using this key will be highlighted accordingly in the chat window.").printf(@"$(device[db.identity_meta.address_name])")); + manage_stack.set_visible_child_name("confirm"); + ok_button.sensitive = true; + return_to_main = false; + current_response = Database.IdentityMetaTable.TrustLevel.VERIFIED; + }); + + verify_no_button.clicked.connect(() => { + return_to_main = false; + confirm_image.set_from_icon_name("dialog-warning-symbolic", IconSize.DIALOG); + confirm_title_label.label = _("Fingerprints do not match"); + confirm_desc_label.set_markup(_("Please verify that you are comparing the correct fingerprint. If fingerprints do not match, %s's account may be compromised and you should consider rejecting this key.").printf(@"$(device[db.identity_meta.address_name])")); + manage_stack.set_visible_child_name("confirm"); + }); + } + + private void handle_cancel() { + if (manage_stack.get_visible_child_name() == "main") close(); + + if (manage_stack.get_visible_child_name() == "verify") { + manage_stack.set_visible_child_name("main"); + cancel_button.label = _("Cancel"); + } + + if (manage_stack.get_visible_child_name() == "confirm") { + if (return_to_main) { + manage_stack.set_visible_child_name("main"); + cancel_button.label = _("Cancel"); + } else { + manage_stack.set_visible_child_name("verify"); + } + } + + ok_button.sensitive = false; + } + + private Box make_action_box(string title, string desc){ + Box box = new Box(Orientation.VERTICAL, 0) { visible = true, margin_start = 20, margin_end = 20, margin_top = 14, margin_bottom = 14 }; + Label lbl_title = new Label(title) { visible = true, halign = Align.START }; + Label lbl_desc = new Label(desc) { visible = true, xalign = 0, wrap = true, max_width_chars = 40 }; + + Pango.AttrList title_attrs = new Pango.AttrList(); + title_attrs.insert(Pango.attr_scale_new(1.1)); + lbl_title.attributes = title_attrs; + Pango.AttrList desc_attrs = new Pango.AttrList(); + desc_attrs.insert(Pango.attr_scale_new(0.8)); + lbl_desc.attributes = desc_attrs; + lbl_desc.get_style_context().add_class("dim-label"); + + box.add(lbl_title); + box.add(lbl_desc); + + return box; + } + + private void setup_main_screen() { + main_action_list.set_header_func((row, before_row) => { + if (row.get_header() == null && before_row != null) { + row.set_header(new Separator(Orientation.HORIZONTAL)); + } + }); + + ListBoxRow verify_row = new ListBoxRow() { visible = true }; + verify_row.add(make_action_box(_("Verify key fingerprint"), _("Compare this key's fingerprint with the fingerprint displayed on the contact's device."))); + ListBoxRow reject_row = new ListBoxRow() { visible = true }; + reject_row.add(make_action_box(_("Reject key"), _("Stop accepting this key during communication with its associated contact."))); + ListBoxRow accept_row = new ListBoxRow() {visible = true }; + accept_row.add(make_action_box(_("Accept key"), _("Start accepting this key during communication with its associated contact"))); + + switch((Database.IdentityMetaTable.TrustLevel) device[db.identity_meta.trust_level]) { + case Database.IdentityMetaTable.TrustLevel.TRUSTED: + main_desc_label.set_markup(_("This key is currently %s.").printf(""+_("accepted")+"")+" "+_("This means it can be used by %s to receive and send messages.").printf(@"$(device[db.identity_meta.address_name])")); + main_action_list.add(verify_row); + main_action_list.add(reject_row); + break; + case Database.IdentityMetaTable.TrustLevel.VERIFIED: + main_desc_label.set_markup(_("This key is currently %s.").printf(""+_("verified")+"")+" "+_("This means it can be used by %s to receive and send messages.") + " " + _("Additionally it has been verified to match the key on the contact's device.").printf(@"$(device[db.identity_meta.address_name])")); + main_action_list.add(reject_row); + break; + case Database.IdentityMetaTable.TrustLevel.UNTRUSTED: + main_desc_label.set_markup(_("This key is currently %s.").printf(""+_("rejected")+"")+" "+_("This means it cannot be used by %s to receive messages, and any messages sent by it will be ignored.").printf(@"$(device[db.identity_meta.address_name])")); + main_action_list.add(accept_row); + break; + } + + //Row clicked - go to appropriate screen + main_action_list.row_activated.connect((row) => { + if(row == verify_row) { + manage_stack.set_visible_child_name("verify"); + } else if (row == reject_row) { + confirm_image.set_from_icon_name("action-unavailable-symbolic", IconSize.DIALOG); + confirm_title_label.label = _("Reject key"); + confirm_desc_label.set_markup(_("Once confirmed, any future messages sent by %s using this key will be ignored and none of your messages will be readable using this key.").printf(@"$(device[db.identity_meta.address_name])")); + manage_stack.set_visible_child_name("confirm"); + ok_button.sensitive = true; + return_to_main = true; + current_response = Database.IdentityMetaTable.TrustLevel.UNTRUSTED; + } else if (row == accept_row) { + confirm_image.set_from_icon_name("emblem-ok-symbolic", IconSize.DIALOG); + confirm_title_label.label = _("Accept key"); + confirm_desc_label.set_markup(_("Once confirmed this key will be usable by %s to receive and send messages.").printf(@"$(device[db.identity_meta.address_name])")); + manage_stack.set_visible_child_name("confirm"); + ok_button.sensitive = true; + return_to_main = true; + current_response = Database.IdentityMetaTable.TrustLevel.TRUSTED; + } + cancel_button.label = _("Back"); + }); + + manage_stack.set_visible_child_name("main"); + } + + private void setup_verify_screen() { + verify_label.set_markup(fingerprint_markup(fingerprint_from_base64(device[db.identity_meta.identity_key_public_base64]))); + } +} + +} diff --git a/plugins/omemo/src/ui/own_notifications.vala b/plugins/omemo/src/ui/own_notifications.vala new file mode 100644 index 00000000..f882d03a --- /dev/null +++ b/plugins/omemo/src/ui/own_notifications.vala @@ -0,0 +1,42 @@ +using Dino.Entities; +using Xmpp; +using Gtk; + +namespace Dino.Plugins.Omemo { + +public class OwnNotifications { + + private StreamInteractor stream_interactor; + private Plugin plugin; + private Account account; + + public OwnNotifications (Plugin plugin, StreamInteractor stream_interactor, Account account) { + this.stream_interactor = (!)stream_interactor; + this.plugin = plugin; + this.account = account; + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { + if (jid.equals(account.bare_jid) && has_new_devices(account.bare_jid)) { + display_notification(); + } + }); + + if (has_new_devices(account.bare_jid)) { + display_notification(); + } + } + + public bool has_new_devices(Jid jid) { + int identity_id = plugin.db.identity.get_id(account.id); + if (identity_id < 0) return false; + + return plugin.db.identity_meta.get_new_devices(identity_id, jid.bare_jid.to_string()).count() > 0; + } + + private void display_notification() { + Notification notification = new Notification(_("OMEMO trust decision required")); + notification.set_default_action_and_target_value("app.own-keys", new Variant.int32(account.id)); + notification.set_body(_("Did you add a new device for account %s?").printf(@"$(account.bare_jid.to_string())")); + plugin.app.send_notification(account.id.to_string()+"-new-device", notification); + } +} +} diff --git a/plugins/omemo/src/ui/util.vala b/plugins/omemo/src/ui/util.vala new file mode 100644 index 00000000..88d30b3b --- /dev/null +++ b/plugins/omemo/src/ui/util.vala @@ -0,0 +1,60 @@ +namespace Dino.Plugins.Omemo { + +public static string fingerprint_from_base64(string b64) { + uint8[] arr = Base64.decode(b64); + + arr = arr[1:arr.length]; + string s = ""; + foreach (uint8 i in arr) { + string tmp = i.to_string("%x"); + if (tmp.length == 1) tmp = "0" + tmp; + s = s + tmp; + } + + return s; +} + +public static string fingerprint_markup(string s) { + string markup = ""; + for (int i = 0; i < s.length; i += 4) { + string four_chars = s.substring(i, 4).down(); + + int raw = (int) four_chars.to_long(null, 16); + uint8[] bytes = {(uint8) ((raw >> 8) & 0xff - 128), (uint8) (raw & 0xff - 128)}; + + Checksum checksum = new Checksum(ChecksumType.SHA1); + checksum.update(bytes, bytes.length); + uint8[] digest = new uint8[20]; + size_t len = 20; + checksum.get_digest(digest, ref len); + + uint8 r = digest[0]; + uint8 g = digest[1]; + uint8 b = digest[2]; + + if (r == 0 && g == 0 && b == 0) r = g = b = 1; + + double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b; + + if (brightness < 80) { + double factor = 80.0 / brightness; + r = uint8.min(255, (uint8) (r * factor)); + g = uint8.min(255, (uint8) (g * factor)); + b = uint8.min(255, (uint8) (b * factor)); + + } else if (brightness > 180) { + double factor = 180.0 / brightness; + r = (uint8) (r * factor); + g = (uint8) (g * factor); + b = (uint8) (b * factor); + } + + if (i % 32 == 0 && i != 0) markup += "\n"; + markup += @"$four_chars"; + if (i % 8 == 4 && i % 32 != 28) markup += " "; + } + + return "" + markup + ""; +} + +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf