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/CMakeLists.txt | 46 +-- plugins/omemo/src/account_settings_entry.vala | 26 -- plugins/omemo/src/account_settings_widget.vala | 54 --- plugins/omemo/src/bundle.vala | 88 ----- plugins/omemo/src/contact_details_dialog.vala | 253 -------------- plugins/omemo/src/contact_details_provider.vala | 48 --- plugins/omemo/src/database.vala | 249 ------------- .../omemo/src/device_notification_populator.vala | 99 ------ plugins/omemo/src/encrypt_state.vala | 24 -- plugins/omemo/src/encryption_list_entry.vala | 23 -- plugins/omemo/src/file_provider.vala | 168 --------- plugins/omemo/src/file_sender.vala | 108 ------ plugins/omemo/src/file_transfer/file_provider.vala | 168 +++++++++ plugins/omemo/src/file_transfer/file_sender.vala | 108 ++++++ plugins/omemo/src/logic/database.vala | 249 +++++++++++++ plugins/omemo/src/logic/encrypt_state.vala | 24 ++ plugins/omemo/src/logic/manager.vala | 386 +++++++++++++++++++++ plugins/omemo/src/logic/pre_key_store.vala | 45 +++ plugins/omemo/src/logic/session_store.vala | 49 +++ plugins/omemo/src/logic/signed_pre_key_store.vala | 45 +++ plugins/omemo/src/logic/trust_manager.vala | 346 ++++++++++++++++++ plugins/omemo/src/manage_key_dialog.vala | 166 --------- plugins/omemo/src/manager.vala | 386 --------------------- plugins/omemo/src/message_flag.vala | 23 -- plugins/omemo/src/own_notifications.vala | 42 --- plugins/omemo/src/pre_key_store.vala | 45 --- plugins/omemo/src/protocol/bundle.vala | 88 +++++ plugins/omemo/src/protocol/message_flag.vala | 23 ++ plugins/omemo/src/protocol/stream_module.vala | 271 +++++++++++++++ plugins/omemo/src/session_store.vala | 49 --- plugins/omemo/src/signed_pre_key_store.vala | 45 --- plugins/omemo/src/stream_module.vala | 271 --------------- plugins/omemo/src/trust_manager.vala | 346 ------------------ 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 ++++ plugins/omemo/src/util.vala | 60 ---- 43 files changed, 2598 insertions(+), 2594 deletions(-) delete mode 100644 plugins/omemo/src/account_settings_entry.vala delete mode 100644 plugins/omemo/src/account_settings_widget.vala delete mode 100644 plugins/omemo/src/bundle.vala delete mode 100644 plugins/omemo/src/contact_details_dialog.vala delete mode 100644 plugins/omemo/src/contact_details_provider.vala delete mode 100644 plugins/omemo/src/database.vala delete mode 100644 plugins/omemo/src/device_notification_populator.vala delete mode 100644 plugins/omemo/src/encrypt_state.vala delete mode 100644 plugins/omemo/src/encryption_list_entry.vala delete mode 100644 plugins/omemo/src/file_provider.vala delete mode 100644 plugins/omemo/src/file_sender.vala create mode 100644 plugins/omemo/src/file_transfer/file_provider.vala create mode 100644 plugins/omemo/src/file_transfer/file_sender.vala create mode 100644 plugins/omemo/src/logic/database.vala create mode 100644 plugins/omemo/src/logic/encrypt_state.vala create mode 100644 plugins/omemo/src/logic/manager.vala create mode 100644 plugins/omemo/src/logic/pre_key_store.vala create mode 100644 plugins/omemo/src/logic/session_store.vala create mode 100644 plugins/omemo/src/logic/signed_pre_key_store.vala create mode 100644 plugins/omemo/src/logic/trust_manager.vala delete mode 100644 plugins/omemo/src/manage_key_dialog.vala delete mode 100644 plugins/omemo/src/manager.vala delete mode 100644 plugins/omemo/src/message_flag.vala delete mode 100644 plugins/omemo/src/own_notifications.vala delete mode 100644 plugins/omemo/src/pre_key_store.vala create mode 100644 plugins/omemo/src/protocol/bundle.vala create mode 100644 plugins/omemo/src/protocol/message_flag.vala create mode 100644 plugins/omemo/src/protocol/stream_module.vala delete mode 100644 plugins/omemo/src/session_store.vala delete mode 100644 plugins/omemo/src/signed_pre_key_store.vala delete mode 100644 plugins/omemo/src/stream_module.vala delete mode 100644 plugins/omemo/src/trust_manager.vala 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 delete mode 100644 plugins/omemo/src/util.vala diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index fe6bb079..0331851e 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -30,29 +30,33 @@ compile_gresources( vala_precompile(OMEMO_VALA_C SOURCES - src/account_settings_entry.vala - src/account_settings_widget.vala - src/bundle.vala - src/contact_details_provider.vala - src/contact_details_dialog.vala - src/database.vala - src/device_notification_populator.vala - src/own_notifications.vala - src/encrypt_state.vala - src/encryption_list_entry.vala - src/file_provider.vala - src/file_sender.vala - src/manage_key_dialog.vala - src/manager.vala - src/message_flag.vala src/plugin.vala - src/pre_key_store.vala src/register_plugin.vala - src/session_store.vala - src/signed_pre_key_store.vala - src/stream_module.vala - src/trust_manager.vala - src/util.vala + + src/file_transfer/file_provider.vala + src/file_transfer/file_sender.vala + + src/logic/database.vala + src/logic/encrypt_state.vala + src/logic/manager.vala + src/logic/pre_key_store.vala + src/logic/session_store.vala + src/logic/signed_pre_key_store.vala + src/logic/trust_manager.vala + + src/protocol/bundle.vala + src/protocol/message_flag.vala + src/protocol/stream_module.vala + + src/ui/account_settings_entry.vala + src/ui/account_settings_widget.vala + src/ui/contact_details_provider.vala + src/ui/contact_details_dialog.vala + src/ui/device_notification_populator.vala + src/ui/own_notifications.vala + src/ui/encryption_list_entry.vala + src/ui/manage_key_dialog.vala + src/ui/util.vala CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi diff --git a/plugins/omemo/src/account_settings_entry.vala b/plugins/omemo/src/account_settings_entry.vala deleted file mode 100644 index 3866febe..00000000 --- a/plugins/omemo/src/account_settings_entry.vala +++ /dev/null @@ -1,26 +0,0 @@ -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/account_settings_widget.vala b/plugins/omemo/src/account_settings_widget.vala deleted file mode 100644 index 6148da56..00000000 --- a/plugins/omemo/src/account_settings_widget.vala +++ /dev/null @@ -1,54 +0,0 @@ -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/bundle.vala b/plugins/omemo/src/bundle.vala deleted file mode 100644 index 9b01f299..00000000 --- a/plugins/omemo/src/bundle.vala +++ /dev/null @@ -1,88 +0,0 @@ -using Gee; -using Signal; -using Xmpp; - -namespace Dino.Plugins.Omemo { - -public class Bundle { - private StanzaNode? node; - - public Bundle(StanzaNode? node) { - this.node = node; - assert(Plugin.ensure_context()); - } - - public int32 signed_pre_key_id { owned get { - if (node == null) return -1; - string? id = ((!)node).get_deep_attribute("signedPreKeyPublic", "signedPreKeyId"); - if (id == null) return -1; - return int.parse((!)id); - }} - - public ECPublicKey? signed_pre_key { owned get { - if (node == null) return null; - string? key = ((!)node).get_deep_string_content("signedPreKeyPublic"); - if (key == null) return null; - try { - return Plugin.get_context().decode_public_key(Base64.decode((!)key)); - } catch (Error e) { - return null; - } - }} - - public uint8[]? signed_pre_key_signature { owned get { - if (node == null) return null; - string? sig = ((!)node).get_deep_string_content("signedPreKeySignature"); - if (sig == null) return null; - return Base64.decode((!)sig); - }} - - public ECPublicKey? identity_key { owned get { - if (node == null) return null; - string? key = ((!)node).get_deep_string_content("identityKey"); - if (key == null) return null; - try { - return Plugin.get_context().decode_public_key(Base64.decode((!)key)); - } catch (Error e) { - return null; - } - }} - - public ArrayList pre_keys { owned get { - ArrayList list = new ArrayList(); - if (node == null || ((!)node).get_subnode("prekeys") == null) return list; - ((!)node).get_deep_subnodes("prekeys", "preKeyPublic") - .filter((node) => ((!)node).get_attribute("preKeyId") != null) - .map(PreKey.create) - .foreach((key) => list.add(key)); - return list; - }} - - public class PreKey { - private StanzaNode node; - - public static PreKey create(owned StanzaNode node) { - return new PreKey(node); - } - - public PreKey(StanzaNode node) { - this.node = node; - } - - public int32 key_id { owned get { - return int.parse(node.get_attribute("preKeyId") ?? "-1"); - }} - - public ECPublicKey? key { owned get { - string? key = node.get_string_content(); - if (key == null) return null; - try { - return Plugin.get_context().decode_public_key(Base64.decode((!)key)); - } catch (Error e) { - return null; - } - }} - } -} - -} \ No newline at end of file diff --git a/plugins/omemo/src/contact_details_dialog.vala b/plugins/omemo/src/contact_details_dialog.vala deleted file mode 100644 index 6899acf6..00000000 --- a/plugins/omemo/src/contact_details_dialog.vala +++ /dev/null @@ -1,253 +0,0 @@ -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/contact_details_provider.vala b/plugins/omemo/src/contact_details_provider.vala deleted file mode 100644 index 7250d135..00000000 --- a/plugins/omemo/src/contact_details_provider.vala +++ /dev/null @@ -1,48 +0,0 @@ -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/database.vala b/plugins/omemo/src/database.vala deleted file mode 100644 index bce1d4e6..00000000 --- a/plugins/omemo/src/database.vala +++ /dev/null @@ -1,249 +0,0 @@ -using Gee; -using Qlite; - -using Dino.Entities; - -namespace Dino.Plugins.Omemo { - -public class Database : Qlite.Database { - private const int VERSION = 4; - - public class IdentityMetaTable : Table { - public enum TrustLevel { - VERIFIED, - TRUSTED, - UNTRUSTED, - UNKNOWN; - - public string to_string() { - int val = this; - return val.to_string(); - } - } - - //Default to provide backwards compatability - public Column identity_id = new Column.Integer("identity_id") { not_null = true, min_version = 2, default = "-1" }; - public Column address_name = new Column.Text("address_name") { not_null = true }; - public Column device_id = new Column.Integer("device_id") { not_null = true }; - public Column identity_key_public_base64 = new Column.Text("identity_key_public_base64"); - public Column trusted_identity = new Column.BoolInt("trusted_identity") { default = "0", max_version = 1 }; - public Column trust_level = new Column.Integer("trust_level") { default = TrustLevel.UNKNOWN.to_string(), min_version = 2 }; - public Column now_active = new Column.BoolInt("now_active") { default = "1" }; - public Column last_active = new Column.Long("last_active"); - - internal IdentityMetaTable(Database db) { - base(db, "identity_meta"); - init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active}); - index("identity_meta_idx", {identity_id, address_name, device_id}, true); - index("identity_meta_list_idx", {identity_id, address_name}); - } - - public QueryBuilder with_address(int identity_id, string address_name) { - return select().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name); - } - - public void insert_device_list(int32 identity_id, string address_name, ArrayList devices) { - update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform(); - foreach (int32 device_id in devices) { - upsert() - .value(this.identity_id, identity_id, true) - .value(this.address_name, address_name, true) - .value(this.device_id, device_id, true) - .value(this.now_active, true) - .value(this.last_active, (long) new DateTime.now_utc().to_unix()) - .perform(); - } - } - - public int64 insert_device_bundle(int32 identity_id, string address_name, int device_id, Bundle bundle, TrustLevel trust) { - if (bundle == null || bundle.identity_key == null) return -1; - // Do not replace identity_key if it was known before, it should never change! - string identity_key = Base64.encode(bundle.identity_key.serialize()); - RowOption row = with_address(identity_id, address_name).with(this.device_id, "=", device_id).single().row(); - if (row.is_present() && row[identity_key_public_base64] != null && row[identity_key_public_base64] != identity_key) { - error("Tried to change the identity key for a known device id. Likely an attack."); - } - return upsert() - .value(this.identity_id, identity_id, true) - .value(this.address_name, address_name, true) - .value(this.device_id, device_id, true) - .value(this.identity_key_public_base64, identity_key) - .value(this.trust_level, trust).perform(); - } - - public QueryBuilder get_trusted_devices(int identity_id, string address_name) { - return this.with_address(identity_id, address_name) - .with(this.trust_level, "!=", TrustLevel.UNTRUSTED) - .with(this.now_active, "=", true); - } - - public QueryBuilder get_known_devices(int identity_id, string address_name) { - return this.with_address(identity_id, address_name) - .with(this.trust_level, "!=", TrustLevel.UNKNOWN) - .without_null(this.identity_key_public_base64); - } - - public QueryBuilder get_unknown_devices(int identity_id, string address_name) { - return this.with_address(identity_id, address_name) - .with_null(this.identity_key_public_base64); - } - - public QueryBuilder get_new_devices(int identity_id, string address_name) { - return this.with_address(identity_id, address_name) - .with(this.trust_level, "=", TrustLevel.UNKNOWN) - .without_null(this.identity_key_public_base64); - } - - public Row? get_device(int identity_id, string address_name, int device_id) { - return this.with_address(identity_id, address_name) - .with(this.device_id, "=", device_id).single().row().inner; - } - - public QueryBuilder get_with_device_id(int device_id) { - return select().with(this.device_id, "=", device_id); - } - } - - - public class TrustTable : Table { - public Column identity_id = new Column.Integer("identity_id") { not_null = true }; - public Column address_name = new Column.Text("address_name"); - public Column blind_trust = new Column.BoolInt("blind_trust") { default = "1" } ; - - internal TrustTable(Database db) { - base(db, "trust"); - init({identity_id, address_name, blind_trust}); - index("trust_idx", {identity_id, address_name}, true); - } - - public bool get_blind_trust(int32 identity_id, string address_name) { - return this.select().with(this.identity_id, "=", identity_id) - .with(this.address_name, "=", address_name) - .with(this.blind_trust, "=", true).count() > 0; - } - } - - public class IdentityTable : Table { - public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; - public Column account_id = new Column.Integer("account_id") { unique = true, not_null = true }; - public Column device_id = new Column.Integer("device_id") { not_null = true }; - public Column identity_key_private_base64 = new Column.NonNullText("identity_key_private_base64"); - public Column identity_key_public_base64 = new Column.NonNullText("identity_key_public_base64"); - - internal IdentityTable(Database db) { - base(db, "identity"); - init({id, account_id, device_id, identity_key_private_base64, identity_key_public_base64}); - } - - public int get_id(int account_id) { - int id = -1; - Row? row = this.row_with(this.account_id, account_id).inner; - if (row != null) id = ((!)row)[this.id]; - return id; - } - } - - public class SignedPreKeyTable : Table { - public Column identity_id = new Column.Integer("identity_id") { not_null = true }; - public Column signed_pre_key_id = new Column.Integer("signed_pre_key_id") { not_null = true }; - public Column record_base64 = new Column.NonNullText("record_base64"); - - internal SignedPreKeyTable(Database db) { - base(db, "signed_pre_key"); - init({identity_id, signed_pre_key_id, record_base64}); - unique({identity_id, signed_pre_key_id}); - index("signed_pre_key_idx", {identity_id, signed_pre_key_id}, true); - } - } - - public class PreKeyTable : Table { - public Column identity_id = new Column.Integer("identity_id") { not_null = true }; - public Column pre_key_id = new Column.Integer("pre_key_id") { not_null = true }; - public Column record_base64 = new Column.NonNullText("record_base64"); - - internal PreKeyTable(Database db) { - base(db, "pre_key"); - init({identity_id, pre_key_id, record_base64}); - unique({identity_id, pre_key_id}); - index("pre_key_idx", {identity_id, pre_key_id}, true); - } - } - - public class SessionTable : Table { - public Column identity_id = new Column.Integer("identity_id") { not_null = true }; - public Column address_name = new Column.NonNullText("name"); - public Column device_id = new Column.Integer("device_id") { not_null = true }; - public Column record_base64 = new Column.NonNullText("record_base64"); - - internal SessionTable(Database db) { - base(db, "session"); - init({identity_id, address_name, device_id, record_base64}); - unique({identity_id, address_name, device_id}); - index("session_idx", {identity_id, address_name, device_id}, true); - } - } - - public class ContentItemMetaTable : Table { - public Column content_item_id = new Column.Integer("message_id") { primary_key = true }; - public Column identity_id = new Column.Integer("identity_id") { not_null = true }; - public Column address_name = new Column.Text("address_name") { not_null = true }; - public Column device_id = new Column.Integer("device_id") { not_null = true }; - public Column trusted_when_received = new Column.BoolInt("trusted_when_received") { not_null = true, default = "1" }; - - internal ContentItemMetaTable(Database db) { - base(db, "content_item_meta"); - init({content_item_id, identity_id, address_name, device_id, trusted_when_received}); - index("content_item_meta_device_idx", {identity_id, device_id, address_name}); - } - - public RowOption with_content_item(ContentItem item) { - return row_with(content_item_id, item.id); - } - - public QueryBuilder with_device(int identity_id, string address_name, int device_id) { - return select() - .with(this.identity_id, "=", identity_id) - .with(this.address_name, "=", address_name) - .with(this.device_id, "=", device_id); - } - } - - public IdentityMetaTable identity_meta { get; private set; } - public TrustTable trust { get; private set; } - public IdentityTable identity { get; private set; } - public SignedPreKeyTable signed_pre_key { get; private set; } - public PreKeyTable pre_key { get; private set; } - public SessionTable session { get; private set; } - public ContentItemMetaTable content_item_meta { get; private set; } - - public Database(string fileName) { - base(fileName, VERSION); - identity_meta = new IdentityMetaTable(this); - trust = new TrustTable(this); - identity = new IdentityTable(this); - signed_pre_key = new SignedPreKeyTable(this); - pre_key = new PreKeyTable(this); - session = new SessionTable(this); - content_item_meta = new ContentItemMetaTable(this); - init({identity_meta, trust, identity, signed_pre_key, pre_key, session, content_item_meta}); - try { - exec("PRAGMA synchronous=0"); - } catch (Error e) { } - } - - public override void migrate(long oldVersion) { - if(oldVersion == 1) { - try { - exec("DROP INDEX identity_meta_idx"); - exec("DROP INDEX identity_meta_list_idx"); - exec("CREATE UNIQUE INDEX identity_meta_idx ON identity_meta (identity_id, address_name, device_id)"); - exec("CREATE INDEX identity_meta_list_idx ON identity_meta (identity_id, address_name)"); - } catch (Error e) { - stderr.printf("Failed to migrate OMEMO database\n"); - Process.exit(-1); - } - } - } -} - -} diff --git a/plugins/omemo/src/device_notification_populator.vala b/plugins/omemo/src/device_notification_populator.vala deleted file mode 100644 index 5b47611c..00000000 --- a/plugins/omemo/src/device_notification_populator.vala +++ /dev/null @@ -1,99 +0,0 @@ -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/encrypt_state.vala b/plugins/omemo/src/encrypt_state.vala deleted file mode 100644 index fd72faf4..00000000 --- a/plugins/omemo/src/encrypt_state.vala +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dino.Plugins.Omemo { - -public class EncryptState { - public bool encrypted { get; internal set; } - public int other_devices { get; internal set; } - public int other_success { get; internal set; } - public int other_lost { get; internal set; } - public int other_unknown { get; internal set; } - public int other_failure { get; internal set; } - public int other_waiting_lists { get; internal set; } - - public int own_devices { get; internal set; } - public int own_success { get; internal set; } - public int own_lost { get; internal set; } - public int own_unknown { get; internal set; } - public int own_failure { get; internal set; } - public bool own_list { get; internal set; } - - public string to_string() { - return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; - } -} - -} diff --git a/plugins/omemo/src/encryption_list_entry.vala b/plugins/omemo/src/encryption_list_entry.vala deleted file mode 100644 index 2e8905e2..00000000 --- a/plugins/omemo/src/encryption_list_entry.vala +++ /dev/null @@ -1,23 +0,0 @@ -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/file_provider.vala b/plugins/omemo/src/file_provider.vala deleted file mode 100644 index 938ec0cf..00000000 --- a/plugins/omemo/src/file_provider.vala +++ /dev/null @@ -1,168 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; -using Xmpp; -using Signal; - -namespace Dino.Plugins.Omemo { - -public class FileProvider : Dino.FileProvider, Object { - public string id { get { return "aesgcm"; } } - - private StreamInteractor stream_interactor; - private Dino.Database dino_db; - private Regex url_regex; - - public FileProvider(StreamInteractor stream_interactor, Dino.Database dino_db) { - this.stream_interactor = stream_interactor; - this.dino_db = dino_db; - this.url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/; - - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); - } - - private class ReceivedMessageListener : MessageListener { - - public string[] after_actions_const = new string[]{ "STORE" }; - public override string action_group { get { return ""; } } - public override string[] after_actions { get { return after_actions_const; } } - - private FileProvider outer; - private StreamInteractor stream_interactor; - - public ReceivedMessageListener(FileProvider outer) { - this.outer = outer; - this.stream_interactor = outer.stream_interactor; - } - - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - if (message.body.has_prefix("aesgcm://") && outer.url_regex.match(message.body)) { - yield outer.on_file_message(message, conversation); - } - return false; - } - } - - private async void on_file_message(Entities.Message message, Conversation conversation) { - MatchInfo match_info; - this.url_regex.match(message.body, 0, out match_info); - string url_without_hash = match_info.fetch(1); - - FileTransfer file_transfer = new FileTransfer(); - file_transfer.account = conversation.account; - file_transfer.counterpart = message.counterpart; - file_transfer.ourpart = message.ourpart; - file_transfer.encryption = Encryption.NONE; - file_transfer.time = message.time; - file_transfer.local_time = message.local_time; - file_transfer.direction = message.direction; - file_transfer.file_name = url_without_hash.substring(url_without_hash.last_index_of("/") + 1); - file_transfer.size = -1; - file_transfer.state = FileTransfer.State.NOT_STARTED; - file_transfer.provider = 0; - file_transfer.info = message.id.to_string(); - - if (stream_interactor.get_module(FileManager.IDENTITY).is_sender_trustworthy(file_transfer, conversation)) { - yield get_meta_info(file_transfer); - if (file_transfer.size >= 0 && file_transfer.size < 5000000) { - ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); - if (content_item != null) { - stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); - } - } - file_incoming(file_transfer, conversation); - } - } - - public async void get_meta_info(FileTransfer file_transfer) { - string url_body = dino_db.message.select({dino_db.message.body}).with(dino_db.message.id, "=", int.parse(file_transfer.info))[dino_db.message.body]; - string url = this.aesgcm_to_https_link(url_body); - var session = new Soup.Session(); - var head_message = new Soup.Message("HEAD", url); - if (head_message != null) { - yield session.send_async(head_message, null); - - if (head_message.status_code >= 200 && head_message.status_code < 300) { - string? content_type = null, content_length = null; - head_message.response_headers.foreach((name, val) => { - if (name == "Content-Type") content_type = val; - if (name == "Content-Length") content_length = val; - }); - file_transfer.mime_type = content_type; - if (content_length != null) { - file_transfer.size = int.parse(content_length); - } - } else { - warning("HTTP HEAD download status code " + head_message.status_code.to_string()); - } - } - } - - public async void download(FileTransfer file_transfer, File file) { - try { - string url_body = dino_db.message.select({dino_db.message.body}).with(dino_db.message.id, "=", int.parse(file_transfer.info))[dino_db.message.body]; - string url = this.aesgcm_to_https_link(url_body); - var session = new Soup.Session(); - Soup.Request request = session.request(url); - - file_transfer.input_stream = yield decrypt_file(yield request.send_async(null), url_body); - file_transfer.encryption = Encryption.OMEMO; - - OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); - yield os.splice_async(file_transfer.input_stream, 0); - os.close(); - file_transfer.path = file.get_basename(); - file_transfer.input_stream = yield file.read_async(); - - file_transfer.state = FileTransfer.State.COMPLETE; - } catch (Error e) { - file_transfer.state = FileTransfer.State.FAILED; - } - } - - public async InputStream? decrypt_file(InputStream input_stream, string url) { - // Decode IV and key - MatchInfo match_info; - this.url_regex.match(url, 0, out match_info); - uint8[] iv_and_key = hex_to_bin(match_info.fetch(2).up()); - uint8[] iv, key; - if (iv_and_key.length == 44) { - iv = iv_and_key[0:12]; - key = iv_and_key[12:44]; - } else { - iv = iv_and_key[0:16]; - key = iv_and_key[16:48]; - } - - // Read data - uint8[] buf = new uint8[256]; - Array data = new Array(false, true, 0); - size_t len = -1; - do { - len = yield input_stream.read_async(buf); - data.append_vals(buf, (uint) len); - } while(len > 0); - - // Decrypt - uint8[] cleartext = Signal.aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data); - return new MemoryInputStream.from_data(cleartext); - } - - private uint8[] hex_to_bin(string hex) { - uint8[] bin = new uint8[hex.length / 2]; - const string HEX = "0123456789ABCDEF"; - for (int i = 0; i < hex.length / 2; i++) { - bin[i] = (uint8) (HEX.index_of_char(hex[i*2]) << 4) | HEX.index_of_char(hex[i*2+1]); - } - return bin; - } - - private string aesgcm_to_https_link(string aesgcm_link) { - MatchInfo match_info; - this.url_regex.match(aesgcm_link, 0, out match_info); - return "https://" + match_info.fetch(1); - } -} - -} diff --git a/plugins/omemo/src/file_sender.vala b/plugins/omemo/src/file_sender.vala deleted file mode 100644 index b63d3dc5..00000000 --- a/plugins/omemo/src/file_sender.vala +++ /dev/null @@ -1,108 +0,0 @@ -using Dino.Entities; -using Gee; -using Signal; -using Xmpp; - -namespace Dino.Plugins.Omemo { - -public class AesGcmFileSender : StreamInteractionModule, FileSender, Object { - public static ModuleIdentity IDENTITY = new ModuleIdentity("http_files"); - public string id { get { return IDENTITY.id; } } - - - private StreamInteractor stream_interactor; - private HashMap max_file_sizes = new HashMap(Account.hash_func, Account.equals_func); - - public AesGcmFileSender(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - stream_interactor.stream_negotiated.connect(on_stream_negotiated); - } - - public void send_file(Conversation conversation, FileTransfer file_transfer) { - Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); - uint8[] buf = new uint8[256]; - Array data = new Array(false, true, 0); - size_t len = -1; - do { - try { - len = file_transfer.input_stream.read(buf); - } catch (IOError error) { - warning(@"HTTP upload: IOError reading stream: $(error.message)"); - file_transfer.state = FileTransfer.State.FAILED; - } - data.append_vals(buf, (uint) len); - } while(len > 0); - - //Create a key and use it to encrypt the file - uint8[] iv = new uint8[16]; - Plugin.get_context().randomize(iv); - uint8[] key = new uint8[32]; - Plugin.get_context().randomize(key); - uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data); - - // Convert iv and key to hex - string iv_and_key = ""; - foreach (uint8 byte in iv) iv_and_key += byte.to_string("%02x"); - foreach (uint8 byte in key) iv_and_key += byte.to_string("%02x"); - - stream_interactor.module_manager.get_module(file_transfer.account, Xmpp.Xep.HttpFileUpload.Module.IDENTITY).request_slot(stream, file_transfer.server_file_name, (int) ciphertext.length, file_transfer.mime_type, - (stream, url_down, url_up) => { - Soup.Message message = new Soup.Message("PUT", url_up); - message.set_request(file_transfer.mime_type, Soup.MemoryUse.COPY, ciphertext); - Soup.Session session = new Soup.Session(); - session.send_async.begin(message, null, (obj, res) => { - try { - session.send_async.end(res); - if (message.status_code >= 200 && message.status_code < 300) { - string aesgcm_link = url_down + "#" + iv_and_key; - aesgcm_link = "aesgcm://" + aesgcm_link.substring(8); // replace https:// by aesgcm:// - - file_transfer.info = aesgcm_link; // store the message content temporarily so the message gets filtered out - Entities.Message xmpp_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(aesgcm_link, conversation); - xmpp_message.encryption = Encryption.OMEMO; - stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(xmpp_message, conversation); - file_transfer.info = xmpp_message.id.to_string(); - - ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, xmpp_message.id); - if (content_item != null) { - stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); - } - } else { - warning("HTTP upload status code " + message.status_code.to_string()); - file_transfer.state = FileTransfer.State.FAILED; - } - } catch (Error e) { - warning("HTTP upload error: " + e.message); - file_transfer.state = FileTransfer.State.FAILED; - } - }); - }, - (stream, error) => { - warning("HTTP upload error: " + error); - file_transfer.state = FileTransfer.State.FAILED; - } - ); - } - - public bool can_send(Conversation conversation, FileTransfer file_transfer) { - return file_transfer.encryption == Encryption.OMEMO; - } - - public bool is_upload_available(Conversation conversation) { - lock (max_file_sizes) { - return max_file_sizes.has_key(conversation.account); - } - } - - private void on_stream_negotiated(Account account, XmppStream stream) { - stream_interactor.module_manager.get_module(account, Xmpp.Xep.HttpFileUpload.Module.IDENTITY).feature_available.connect((stream, max_file_size) => { - lock (max_file_sizes) { - max_file_sizes[account] = max_file_size; - } - upload_available(account); - }); - } -} - -} diff --git a/plugins/omemo/src/file_transfer/file_provider.vala b/plugins/omemo/src/file_transfer/file_provider.vala new file mode 100644 index 00000000..938ec0cf --- /dev/null +++ b/plugins/omemo/src/file_transfer/file_provider.vala @@ -0,0 +1,168 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; +using Signal; + +namespace Dino.Plugins.Omemo { + +public class FileProvider : Dino.FileProvider, Object { + public string id { get { return "aesgcm"; } } + + private StreamInteractor stream_interactor; + private Dino.Database dino_db; + private Regex url_regex; + + public FileProvider(StreamInteractor stream_interactor, Dino.Database dino_db) { + this.stream_interactor = stream_interactor; + this.dino_db = dino_db; + this.url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/; + + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); + } + + private class ReceivedMessageListener : MessageListener { + + public string[] after_actions_const = new string[]{ "STORE" }; + public override string action_group { get { return ""; } } + public override string[] after_actions { get { return after_actions_const; } } + + private FileProvider outer; + private StreamInteractor stream_interactor; + + public ReceivedMessageListener(FileProvider outer) { + this.outer = outer; + this.stream_interactor = outer.stream_interactor; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + if (message.body.has_prefix("aesgcm://") && outer.url_regex.match(message.body)) { + yield outer.on_file_message(message, conversation); + } + return false; + } + } + + private async void on_file_message(Entities.Message message, Conversation conversation) { + MatchInfo match_info; + this.url_regex.match(message.body, 0, out match_info); + string url_without_hash = match_info.fetch(1); + + FileTransfer file_transfer = new FileTransfer(); + file_transfer.account = conversation.account; + file_transfer.counterpart = message.counterpart; + file_transfer.ourpart = message.ourpart; + file_transfer.encryption = Encryption.NONE; + file_transfer.time = message.time; + file_transfer.local_time = message.local_time; + file_transfer.direction = message.direction; + file_transfer.file_name = url_without_hash.substring(url_without_hash.last_index_of("/") + 1); + file_transfer.size = -1; + file_transfer.state = FileTransfer.State.NOT_STARTED; + file_transfer.provider = 0; + file_transfer.info = message.id.to_string(); + + if (stream_interactor.get_module(FileManager.IDENTITY).is_sender_trustworthy(file_transfer, conversation)) { + yield get_meta_info(file_transfer); + if (file_transfer.size >= 0 && file_transfer.size < 5000000) { + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); + if (content_item != null) { + stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); + } + } + file_incoming(file_transfer, conversation); + } + } + + public async void get_meta_info(FileTransfer file_transfer) { + string url_body = dino_db.message.select({dino_db.message.body}).with(dino_db.message.id, "=", int.parse(file_transfer.info))[dino_db.message.body]; + string url = this.aesgcm_to_https_link(url_body); + var session = new Soup.Session(); + var head_message = new Soup.Message("HEAD", url); + if (head_message != null) { + yield session.send_async(head_message, null); + + if (head_message.status_code >= 200 && head_message.status_code < 300) { + string? content_type = null, content_length = null; + head_message.response_headers.foreach((name, val) => { + if (name == "Content-Type") content_type = val; + if (name == "Content-Length") content_length = val; + }); + file_transfer.mime_type = content_type; + if (content_length != null) { + file_transfer.size = int.parse(content_length); + } + } else { + warning("HTTP HEAD download status code " + head_message.status_code.to_string()); + } + } + } + + public async void download(FileTransfer file_transfer, File file) { + try { + string url_body = dino_db.message.select({dino_db.message.body}).with(dino_db.message.id, "=", int.parse(file_transfer.info))[dino_db.message.body]; + string url = this.aesgcm_to_https_link(url_body); + var session = new Soup.Session(); + Soup.Request request = session.request(url); + + file_transfer.input_stream = yield decrypt_file(yield request.send_async(null), url_body); + file_transfer.encryption = Encryption.OMEMO; + + OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); + yield os.splice_async(file_transfer.input_stream, 0); + os.close(); + file_transfer.path = file.get_basename(); + file_transfer.input_stream = yield file.read_async(); + + file_transfer.state = FileTransfer.State.COMPLETE; + } catch (Error e) { + file_transfer.state = FileTransfer.State.FAILED; + } + } + + public async InputStream? decrypt_file(InputStream input_stream, string url) { + // Decode IV and key + MatchInfo match_info; + this.url_regex.match(url, 0, out match_info); + uint8[] iv_and_key = hex_to_bin(match_info.fetch(2).up()); + uint8[] iv, key; + if (iv_and_key.length == 44) { + iv = iv_and_key[0:12]; + key = iv_and_key[12:44]; + } else { + iv = iv_and_key[0:16]; + key = iv_and_key[16:48]; + } + + // Read data + uint8[] buf = new uint8[256]; + Array data = new Array(false, true, 0); + size_t len = -1; + do { + len = yield input_stream.read_async(buf); + data.append_vals(buf, (uint) len); + } while(len > 0); + + // Decrypt + uint8[] cleartext = Signal.aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data); + return new MemoryInputStream.from_data(cleartext); + } + + private uint8[] hex_to_bin(string hex) { + uint8[] bin = new uint8[hex.length / 2]; + const string HEX = "0123456789ABCDEF"; + for (int i = 0; i < hex.length / 2; i++) { + bin[i] = (uint8) (HEX.index_of_char(hex[i*2]) << 4) | HEX.index_of_char(hex[i*2+1]); + } + return bin; + } + + private string aesgcm_to_https_link(string aesgcm_link) { + MatchInfo match_info; + this.url_regex.match(aesgcm_link, 0, out match_info); + return "https://" + match_info.fetch(1); + } +} + +} diff --git a/plugins/omemo/src/file_transfer/file_sender.vala b/plugins/omemo/src/file_transfer/file_sender.vala new file mode 100644 index 00000000..b63d3dc5 --- /dev/null +++ b/plugins/omemo/src/file_transfer/file_sender.vala @@ -0,0 +1,108 @@ +using Dino.Entities; +using Gee; +using Signal; +using Xmpp; + +namespace Dino.Plugins.Omemo { + +public class AesGcmFileSender : StreamInteractionModule, FileSender, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("http_files"); + public string id { get { return IDENTITY.id; } } + + + private StreamInteractor stream_interactor; + private HashMap max_file_sizes = new HashMap(Account.hash_func, Account.equals_func); + + public AesGcmFileSender(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + stream_interactor.stream_negotiated.connect(on_stream_negotiated); + } + + public void send_file(Conversation conversation, FileTransfer file_transfer) { + Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); + uint8[] buf = new uint8[256]; + Array data = new Array(false, true, 0); + size_t len = -1; + do { + try { + len = file_transfer.input_stream.read(buf); + } catch (IOError error) { + warning(@"HTTP upload: IOError reading stream: $(error.message)"); + file_transfer.state = FileTransfer.State.FAILED; + } + data.append_vals(buf, (uint) len); + } while(len > 0); + + //Create a key and use it to encrypt the file + uint8[] iv = new uint8[16]; + Plugin.get_context().randomize(iv); + uint8[] key = new uint8[32]; + Plugin.get_context().randomize(key); + uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data); + + // Convert iv and key to hex + string iv_and_key = ""; + foreach (uint8 byte in iv) iv_and_key += byte.to_string("%02x"); + foreach (uint8 byte in key) iv_and_key += byte.to_string("%02x"); + + stream_interactor.module_manager.get_module(file_transfer.account, Xmpp.Xep.HttpFileUpload.Module.IDENTITY).request_slot(stream, file_transfer.server_file_name, (int) ciphertext.length, file_transfer.mime_type, + (stream, url_down, url_up) => { + Soup.Message message = new Soup.Message("PUT", url_up); + message.set_request(file_transfer.mime_type, Soup.MemoryUse.COPY, ciphertext); + Soup.Session session = new Soup.Session(); + session.send_async.begin(message, null, (obj, res) => { + try { + session.send_async.end(res); + if (message.status_code >= 200 && message.status_code < 300) { + string aesgcm_link = url_down + "#" + iv_and_key; + aesgcm_link = "aesgcm://" + aesgcm_link.substring(8); // replace https:// by aesgcm:// + + file_transfer.info = aesgcm_link; // store the message content temporarily so the message gets filtered out + Entities.Message xmpp_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(aesgcm_link, conversation); + xmpp_message.encryption = Encryption.OMEMO; + stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(xmpp_message, conversation); + file_transfer.info = xmpp_message.id.to_string(); + + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, xmpp_message.id); + if (content_item != null) { + stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); + } + } else { + warning("HTTP upload status code " + message.status_code.to_string()); + file_transfer.state = FileTransfer.State.FAILED; + } + } catch (Error e) { + warning("HTTP upload error: " + e.message); + file_transfer.state = FileTransfer.State.FAILED; + } + }); + }, + (stream, error) => { + warning("HTTP upload error: " + error); + file_transfer.state = FileTransfer.State.FAILED; + } + ); + } + + public bool can_send(Conversation conversation, FileTransfer file_transfer) { + return file_transfer.encryption == Encryption.OMEMO; + } + + public bool is_upload_available(Conversation conversation) { + lock (max_file_sizes) { + return max_file_sizes.has_key(conversation.account); + } + } + + private void on_stream_negotiated(Account account, XmppStream stream) { + stream_interactor.module_manager.get_module(account, Xmpp.Xep.HttpFileUpload.Module.IDENTITY).feature_available.connect((stream, max_file_size) => { + lock (max_file_sizes) { + max_file_sizes[account] = max_file_size; + } + upload_available(account); + }); + } +} + +} diff --git a/plugins/omemo/src/logic/database.vala b/plugins/omemo/src/logic/database.vala new file mode 100644 index 00000000..bce1d4e6 --- /dev/null +++ b/plugins/omemo/src/logic/database.vala @@ -0,0 +1,249 @@ +using Gee; +using Qlite; + +using Dino.Entities; + +namespace Dino.Plugins.Omemo { + +public class Database : Qlite.Database { + private const int VERSION = 4; + + public class IdentityMetaTable : Table { + public enum TrustLevel { + VERIFIED, + TRUSTED, + UNTRUSTED, + UNKNOWN; + + public string to_string() { + int val = this; + return val.to_string(); + } + } + + //Default to provide backwards compatability + public Column identity_id = new Column.Integer("identity_id") { not_null = true, min_version = 2, default = "-1" }; + public Column address_name = new Column.Text("address_name") { not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column identity_key_public_base64 = new Column.Text("identity_key_public_base64"); + public Column trusted_identity = new Column.BoolInt("trusted_identity") { default = "0", max_version = 1 }; + public Column trust_level = new Column.Integer("trust_level") { default = TrustLevel.UNKNOWN.to_string(), min_version = 2 }; + public Column now_active = new Column.BoolInt("now_active") { default = "1" }; + public Column last_active = new Column.Long("last_active"); + + internal IdentityMetaTable(Database db) { + base(db, "identity_meta"); + init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active}); + index("identity_meta_idx", {identity_id, address_name, device_id}, true); + index("identity_meta_list_idx", {identity_id, address_name}); + } + + public QueryBuilder with_address(int identity_id, string address_name) { + return select().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name); + } + + public void insert_device_list(int32 identity_id, string address_name, ArrayList devices) { + update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform(); + foreach (int32 device_id in devices) { + upsert() + .value(this.identity_id, identity_id, true) + .value(this.address_name, address_name, true) + .value(this.device_id, device_id, true) + .value(this.now_active, true) + .value(this.last_active, (long) new DateTime.now_utc().to_unix()) + .perform(); + } + } + + public int64 insert_device_bundle(int32 identity_id, string address_name, int device_id, Bundle bundle, TrustLevel trust) { + if (bundle == null || bundle.identity_key == null) return -1; + // Do not replace identity_key if it was known before, it should never change! + string identity_key = Base64.encode(bundle.identity_key.serialize()); + RowOption row = with_address(identity_id, address_name).with(this.device_id, "=", device_id).single().row(); + if (row.is_present() && row[identity_key_public_base64] != null && row[identity_key_public_base64] != identity_key) { + error("Tried to change the identity key for a known device id. Likely an attack."); + } + return upsert() + .value(this.identity_id, identity_id, true) + .value(this.address_name, address_name, true) + .value(this.device_id, device_id, true) + .value(this.identity_key_public_base64, identity_key) + .value(this.trust_level, trust).perform(); + } + + public QueryBuilder get_trusted_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with(this.trust_level, "!=", TrustLevel.UNTRUSTED) + .with(this.now_active, "=", true); + } + + public QueryBuilder get_known_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with(this.trust_level, "!=", TrustLevel.UNKNOWN) + .without_null(this.identity_key_public_base64); + } + + public QueryBuilder get_unknown_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with_null(this.identity_key_public_base64); + } + + public QueryBuilder get_new_devices(int identity_id, string address_name) { + return this.with_address(identity_id, address_name) + .with(this.trust_level, "=", TrustLevel.UNKNOWN) + .without_null(this.identity_key_public_base64); + } + + public Row? get_device(int identity_id, string address_name, int device_id) { + return this.with_address(identity_id, address_name) + .with(this.device_id, "=", device_id).single().row().inner; + } + + public QueryBuilder get_with_device_id(int device_id) { + return select().with(this.device_id, "=", device_id); + } + } + + + public class TrustTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.Text("address_name"); + public Column blind_trust = new Column.BoolInt("blind_trust") { default = "1" } ; + + internal TrustTable(Database db) { + base(db, "trust"); + init({identity_id, address_name, blind_trust}); + index("trust_idx", {identity_id, address_name}, true); + } + + public bool get_blind_trust(int32 identity_id, string address_name) { + return this.select().with(this.identity_id, "=", identity_id) + .with(this.address_name, "=", address_name) + .with(this.blind_trust, "=", true).count() > 0; + } + } + + public class IdentityTable : Table { + public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; + public Column account_id = new Column.Integer("account_id") { unique = true, not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column identity_key_private_base64 = new Column.NonNullText("identity_key_private_base64"); + public Column identity_key_public_base64 = new Column.NonNullText("identity_key_public_base64"); + + internal IdentityTable(Database db) { + base(db, "identity"); + init({id, account_id, device_id, identity_key_private_base64, identity_key_public_base64}); + } + + public int get_id(int account_id) { + int id = -1; + Row? row = this.row_with(this.account_id, account_id).inner; + if (row != null) id = ((!)row)[this.id]; + return id; + } + } + + public class SignedPreKeyTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column signed_pre_key_id = new Column.Integer("signed_pre_key_id") { not_null = true }; + public Column record_base64 = new Column.NonNullText("record_base64"); + + internal SignedPreKeyTable(Database db) { + base(db, "signed_pre_key"); + init({identity_id, signed_pre_key_id, record_base64}); + unique({identity_id, signed_pre_key_id}); + index("signed_pre_key_idx", {identity_id, signed_pre_key_id}, true); + } + } + + public class PreKeyTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column pre_key_id = new Column.Integer("pre_key_id") { not_null = true }; + public Column record_base64 = new Column.NonNullText("record_base64"); + + internal PreKeyTable(Database db) { + base(db, "pre_key"); + init({identity_id, pre_key_id, record_base64}); + unique({identity_id, pre_key_id}); + index("pre_key_idx", {identity_id, pre_key_id}, true); + } + } + + public class SessionTable : Table { + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.NonNullText("name"); + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column record_base64 = new Column.NonNullText("record_base64"); + + internal SessionTable(Database db) { + base(db, "session"); + init({identity_id, address_name, device_id, record_base64}); + unique({identity_id, address_name, device_id}); + index("session_idx", {identity_id, address_name, device_id}, true); + } + } + + public class ContentItemMetaTable : Table { + public Column content_item_id = new Column.Integer("message_id") { primary_key = true }; + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.Text("address_name") { not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column trusted_when_received = new Column.BoolInt("trusted_when_received") { not_null = true, default = "1" }; + + internal ContentItemMetaTable(Database db) { + base(db, "content_item_meta"); + init({content_item_id, identity_id, address_name, device_id, trusted_when_received}); + index("content_item_meta_device_idx", {identity_id, device_id, address_name}); + } + + public RowOption with_content_item(ContentItem item) { + return row_with(content_item_id, item.id); + } + + public QueryBuilder with_device(int identity_id, string address_name, int device_id) { + return select() + .with(this.identity_id, "=", identity_id) + .with(this.address_name, "=", address_name) + .with(this.device_id, "=", device_id); + } + } + + public IdentityMetaTable identity_meta { get; private set; } + public TrustTable trust { get; private set; } + public IdentityTable identity { get; private set; } + public SignedPreKeyTable signed_pre_key { get; private set; } + public PreKeyTable pre_key { get; private set; } + public SessionTable session { get; private set; } + public ContentItemMetaTable content_item_meta { get; private set; } + + public Database(string fileName) { + base(fileName, VERSION); + identity_meta = new IdentityMetaTable(this); + trust = new TrustTable(this); + identity = new IdentityTable(this); + signed_pre_key = new SignedPreKeyTable(this); + pre_key = new PreKeyTable(this); + session = new SessionTable(this); + content_item_meta = new ContentItemMetaTable(this); + init({identity_meta, trust, identity, signed_pre_key, pre_key, session, content_item_meta}); + try { + exec("PRAGMA synchronous=0"); + } catch (Error e) { } + } + + public override void migrate(long oldVersion) { + if(oldVersion == 1) { + try { + exec("DROP INDEX identity_meta_idx"); + exec("DROP INDEX identity_meta_list_idx"); + exec("CREATE UNIQUE INDEX identity_meta_idx ON identity_meta (identity_id, address_name, device_id)"); + exec("CREATE INDEX identity_meta_list_idx ON identity_meta (identity_id, address_name)"); + } catch (Error e) { + stderr.printf("Failed to migrate OMEMO database\n"); + Process.exit(-1); + } + } + } +} + +} diff --git a/plugins/omemo/src/logic/encrypt_state.vala b/plugins/omemo/src/logic/encrypt_state.vala new file mode 100644 index 00000000..fd72faf4 --- /dev/null +++ b/plugins/omemo/src/logic/encrypt_state.vala @@ -0,0 +1,24 @@ +namespace Dino.Plugins.Omemo { + +public class EncryptState { + public bool encrypted { get; internal set; } + public int other_devices { get; internal set; } + public int other_success { get; internal set; } + public int other_lost { get; internal set; } + public int other_unknown { get; internal set; } + public int other_failure { get; internal set; } + public int other_waiting_lists { get; internal set; } + + public int own_devices { get; internal set; } + public int own_success { get; internal set; } + public int own_lost { get; internal set; } + public int own_unknown { get; internal set; } + public int own_failure { get; internal set; } + public bool own_list { get; internal set; } + + public string to_string() { + return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; + } +} + +} diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala new file mode 100644 index 00000000..db64c3ee --- /dev/null +++ b/plugins/omemo/src/logic/manager.vala @@ -0,0 +1,386 @@ +using Dino.Entities; +using Signal; +using Qlite; +using Xmpp; +using Gee; + +namespace Dino.Plugins.Omemo { + +public class Manager : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("omemo_manager"); + public string id { get { return IDENTITY.id; } } + + private StreamInteractor stream_interactor; + private Database db; + private TrustManager trust_manager; + private Map message_states = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); + + private class MessageState { + public Entities.Message msg { get; private set; } + public EncryptState last_try { get; private set; } + public int waiting_other_sessions { get; set; } + public int waiting_own_sessions { get; set; } + public bool waiting_own_devicelist { get; set; } + public int waiting_other_devicelists { get; set; } + public bool force_next_attempt { get; set; } + public bool will_send_now { get; private set; } + public bool active_send_attempt { get; set; } + + public MessageState(Entities.Message msg, EncryptState last_try) { + this.msg = msg; + this.last_try = last_try; + update_from_encrypt_status(last_try); + } + + public void update_from_encrypt_status(EncryptState new_try) { + this.last_try = new_try; + this.waiting_other_sessions = new_try.other_unknown; + this.waiting_own_sessions = new_try.own_unknown; + this.waiting_own_devicelist = !new_try.own_list; + this.waiting_other_devicelists = new_try.other_waiting_lists; + this.active_send_attempt = false; + will_send_now = false; + if (new_try.other_failure > 0 || (new_try.other_lost == new_try.other_devices && new_try.other_devices > 0)) { + msg.marked = Entities.Message.Marked.WONTSEND; + } else if (new_try.other_unknown > 0 || new_try.own_unknown > 0 || new_try.other_waiting_lists > 0 || !new_try.own_list || new_try.own_devices == 0) { + msg.marked = Entities.Message.Marked.UNSENT; + } else if (!new_try.encrypted) { + msg.marked = Entities.Message.Marked.WONTSEND; + } else { + will_send_now = true; + } + } + + public bool should_retry_now() { + return !waiting_own_devicelist && waiting_other_devicelists <= 0 && waiting_other_sessions <= 0 && waiting_own_sessions <= 0 && !active_send_attempt; + } + + public string to_string() { + return @"MessageState (waiting=(others=$waiting_other_sessions, own=$waiting_own_sessions, other_lists=$waiting_other_devicelists, own_list=$waiting_own_devicelist))"; + } + } + + private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { + this.stream_interactor = stream_interactor; + this.db = db; + this.trust_manager = trust_manager; + + stream_interactor.stream_negotiated.connect(on_stream_negotiated); + stream_interactor.account_added.connect(on_account_added); + stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); + stream_interactor.get_module(RosterManager.IDENTITY).mutual_subscription.connect(on_mutual_subscription); + } + + private Gee.List get_occupants(Jid jid, Account account){ + Gee.List occupants = new ArrayList(Jid.equals_bare_func); + if(!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid, account)){ + occupants.add(jid); + } + Gee.List? occupant_jids = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(jid, account); + if(occupant_jids == null) { + return occupants; + } + foreach (Jid occupant in occupant_jids) { + if(!occupant.equals(account.bare_jid)){ + occupants.add(occupant.bare_jid); + } + } + return occupants; + } + + private void on_pre_message_send(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) { + if (message.encryption == Encryption.OMEMO) { + XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream == null) { + message.marked = Entities.Message.Marked.UNSENT; + return; + } + StreamModule? module_ = ((!)stream).get_module(StreamModule.IDENTITY); + if (module_ == null) { + message.marked = Entities.Message.Marked.UNSENT; + return; + } + StreamModule module = (!)module_; + + //Get a list of everyone for whom the message should be encrypted + Gee.List recipients; + if (message_stanza.type_ == MessageStanza.TYPE_GROUPCHAT) { + recipients = get_occupants((!)message.to.bare_jid, conversation.account); + if (recipients.size == 0) { + message.marked = Entities.Message.Marked.WONTSEND; + return; + } + } else { + recipients = new ArrayList(Jid.equals_bare_func); + recipients.add(message_stanza.to); + } + + //Attempt to encrypt the message + EncryptState enc_state = trust_manager.encrypt(message_stanza, conversation.account.bare_jid, recipients, stream, conversation.account); + MessageState state; + lock (message_states) { + if (message_states.has_key(message)) { + state = message_states.get(message); + state.update_from_encrypt_status(enc_state); + } else { + state = new MessageState(message, enc_state); + message_states[message] = state; + } + if (state.will_send_now) { + message_states.unset(message); + } + } + + //Encryption failed - need to fetch more information + if (!state.will_send_now) { + if (message.marked == Entities.Message.Marked.WONTSEND) { + debug("message was not sent: %s", state.to_string()); + message_states.unset(message); + } else { + debug("message will be delayed: %s", state.to_string()); + + if (state.waiting_own_sessions > 0) { + module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid)); + } + if (state.waiting_other_sessions > 0 && message.counterpart != null) { + foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { + module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid)); + } + } + if (state.waiting_other_devicelists > 0 && message.counterpart != null) { + foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { + module.request_user_devicelist((!)stream, jid); + } + } + } + } + } + } + + private void on_mutual_subscription(Account account, Jid jid) { + XmppStream? stream = stream_interactor.get_stream(account); + if(stream == null) return; + + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist((!)stream, jid); + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store_created.connect((store) => on_store_created.begin(account, store)); + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices)); + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); + } + + private void on_stream_negotiated(Account account, XmppStream stream) { + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist(stream, account.bare_jid); + } + + private void on_device_list_loaded(Account account, Jid jid, ArrayList device_list) { + debug("received device list for %s from %s", account.bare_jid.to_string(), jid.to_string()); + + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) { + return; + } + StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); + if (module == null) { + return; + } + + int identity_id = db.identity.get_id(account.id); + if (identity_id < 0) return; + + //Update meta database + db.identity_meta.insert_device_list(identity_id, jid.bare_jid.to_string(), device_list); + + //Fetch the bundle for each new device + int inc = 0; + foreach (Row row in db.identity_meta.get_unknown_devices(identity_id, jid.bare_jid.to_string())) { + module.fetch_bundle(stream, Jid.parse(row[db.identity_meta.address_name]), row[db.identity_meta.device_id]); + inc++; + } + if (inc > 0) { + debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string()); + } + + //Create an entry for the jid in the account table if one does not exist already + if (db.trust.select().with(db.trust.identity_id, "=", identity_id).with(db.trust.address_name, "=", jid.bare_jid.to_string()).count() == 0) { + db.trust.insert().value(db.trust.identity_id, identity_id).value(db.trust.address_name, jid.bare_jid.to_string()).value(db.trust.blind_trust, true).perform(); + } + + //Get all messages that needed the devicelist and determine if we can now send them + HashSet send_now = new HashSet(); + lock (message_states) { + foreach (Entities.Message msg in message_states.keys) { + if (!msg.account.equals(account)) continue; + Gee.List occupants = get_occupants(msg.counterpart.bare_jid, account); + MessageState state = message_states[msg]; + if (account.bare_jid.equals(jid)) { + state.waiting_own_devicelist = false; + } else if (msg.counterpart != null && occupants.contains(jid)) { + state.waiting_other_devicelists--; + } + if (state.should_retry_now()) { + send_now.add(msg); + state.active_send_attempt = true; + } + } + } + foreach (Entities.Message msg in send_now) { + if (msg.counterpart == null) continue; + Entities.Conversation? conv = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(((!)msg.counterpart), account); + if (conv == null) continue; + stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); + } + + } + + public void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) { + int identity_id = db.identity.get_id(account.id); + if (identity_id < 0) return; + + bool blind_trust = db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string()); + + //If we don't blindly trust new devices and we haven't seen this key before then don't trust it + bool untrust = !(blind_trust || db.identity_meta.with_address(identity_id, jid.bare_jid.to_string()) + .with(db.identity_meta.device_id, "=", device_id) + .with(db.identity_meta.identity_key_public_base64, "=", Base64.encode(bundle.identity_key.serialize())) + .single().row().is_present()); + + //Get trust information from the database if the device id is known + Row device = db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), device_id); + Database.IdentityMetaTable.TrustLevel trusted = Database.IdentityMetaTable.TrustLevel.UNKNOWN; + if (device != null) { + trusted = (Database.IdentityMetaTable.TrustLevel) device[db.identity_meta.trust_level]; + } + + if(untrust) { + trusted = Database.IdentityMetaTable.TrustLevel.UNKNOWN; + } else if (blind_trust && trusted == Database.IdentityMetaTable.TrustLevel.UNKNOWN) { + trusted = Database.IdentityMetaTable.TrustLevel.TRUSTED; + } + + //Update the database with the appropriate trust information + db.identity_meta.insert_device_bundle(identity_id, jid.bare_jid.to_string(), device_id, bundle, trusted); + + XmppStream? stream = stream_interactor.get_stream(account); + if(stream == null) return; + StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); + if(module == null) return; + + //Get all messages waiting on the bundle and determine if they can now be sent + HashSet send_now = new HashSet(); + lock (message_states) { + foreach (Entities.Message msg in message_states.keys) { + + bool session_created = true; + if (!msg.account.equals(account)) continue; + Gee.List occupants = get_occupants(msg.counterpart.bare_jid, account); + + MessageState state = message_states[msg]; + + if (trusted == Database.IdentityMetaTable.TrustLevel.TRUSTED || trusted == Database.IdentityMetaTable.TrustLevel.VERIFIED) { + if(account.bare_jid.equals(jid) || (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)))) { + session_created = module.start_session(stream, jid, device_id, bundle); + } + } + if (account.bare_jid.equals(jid) && session_created) { + state.waiting_own_sessions--; + } else if (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)) && session_created) { + state.waiting_other_sessions--; + } + if (state.should_retry_now()){ + send_now.add(msg); + state.active_send_attempt = true; + } + } + } + foreach (Entities.Message msg in send_now) { + if (msg.counterpart == null) continue; + Entities.Conversation? conv = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation((!)msg.counterpart, account); + if (conv == null) continue; + stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); + } + } + + private async void on_store_created(Account account, Store store) { + // If the account is not yet persisted, wait for that and then continue - without identity.account_id the entry isn't worth much. + if (account.id == -1) { + account.notify["id"].connect(() => on_store_created.callback()); + yield; + } + Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner; + int identity_id = -1; + bool publish_identity = false; + + if (row == null) { + // OMEMO not yet initialized, starting with empty base + publish_identity = true; + try { + store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX); + + Signal.ECKeyPair key_pair = Plugin.get_context().generate_key_pair(); + store.identity_key_store.identity_key_private = key_pair.private.serialize(); + store.identity_key_store.identity_key_public = key_pair.public.serialize(); + + identity_id = (int) db.identity.insert().or("REPLACE") + .value(db.identity.account_id, account.id) + .value(db.identity.device_id, (int) store.local_registration_id) + .value(db.identity.identity_key_private_base64, Base64.encode(store.identity_key_store.identity_key_private)) + .value(db.identity.identity_key_public_base64, Base64.encode(store.identity_key_store.identity_key_public)) + .perform(); + } catch (Error e) { + // Ignore error + } + } else { + store.identity_key_store.local_registration_id = ((!)row)[db.identity.device_id]; + store.identity_key_store.identity_key_private = Base64.decode(((!)row)[db.identity.identity_key_private_base64]); + store.identity_key_store.identity_key_public = Base64.decode(((!)row)[db.identity.identity_key_public_base64]); + identity_id = ((!)row)[db.identity.id]; + } + + if (identity_id >= 0) { + store.signed_pre_key_store = new BackedSignedPreKeyStore(db, identity_id); + store.pre_key_store = new BackedPreKeyStore(db, identity_id); + store.session_store = new BackedSessionStore(db, identity_id); + } else { + warning("store for %s is not persisted!", account.bare_jid.to_string()); + } + + // Generated new device ID, ensure this gets added to the devicelist + if (publish_identity) { + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) return; + StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); + if(module == null) return; + module.request_user_devicelist(stream, account.bare_jid); + } + } + + + public bool can_encrypt(Entities.Conversation conversation) { + XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream == null) return false; + if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(conversation.counterpart, conversation.account)){ + Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY); + if (flag == null) return false; + if (flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.NON_ANONYMOUS) && flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.MEMBERS_ONLY)) { + foreach(Jid jid in stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account)) { + if (!trust_manager.is_known_address(conversation.account, jid.bare_jid)) { + return false; + } + } + return true; + } else { + return false; + } + } + return trust_manager.is_known_address(conversation.account, conversation.counterpart.bare_jid); + } + + public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { + Manager m = new Manager(stream_interactor, db, trust_manager); + stream_interactor.add_module(m); + } +} + +} diff --git a/plugins/omemo/src/logic/pre_key_store.vala b/plugins/omemo/src/logic/pre_key_store.vala new file mode 100644 index 00000000..716fd32f --- /dev/null +++ b/plugins/omemo/src/logic/pre_key_store.vala @@ -0,0 +1,45 @@ +using Signal; +using Qlite; + +namespace Dino.Plugins.Omemo { + +private class BackedPreKeyStore : SimplePreKeyStore { + private Database db; + private int identity_id; + + public BackedPreKeyStore(Database db, int identity_id) { + this.db = db; + this.identity_id = identity_id; + init(); + } + + private void init() { + try { + foreach (Row row in db.pre_key.select().with(db.pre_key.identity_id, "=", identity_id)) { + store_pre_key(row[db.pre_key.pre_key_id], Base64.decode(row[db.pre_key.record_base64])); + } + } catch (Error e) { + warning("Error while initializing pre key store: %s", e.message); + } + + pre_key_stored.connect(on_pre_key_stored); + pre_key_deleted.connect(on_pre_key_deleted); + } + + public void on_pre_key_stored(PreKeyStore.Key key) { + db.pre_key.insert().or("REPLACE") + .value(db.pre_key.identity_id, identity_id) + .value(db.pre_key.pre_key_id, (int) key.key_id) + .value(db.pre_key.record_base64, Base64.encode(key.record)) + .perform(); + } + + public void on_pre_key_deleted(PreKeyStore.Key key) { + db.pre_key.delete() + .with(db.pre_key.identity_id, "=", identity_id) + .with(db.pre_key.pre_key_id, "=", (int) key.key_id) + .perform(); + } +} + +} diff --git a/plugins/omemo/src/logic/session_store.vala b/plugins/omemo/src/logic/session_store.vala new file mode 100644 index 00000000..654591d1 --- /dev/null +++ b/plugins/omemo/src/logic/session_store.vala @@ -0,0 +1,49 @@ +using Signal; +using Qlite; + +namespace Dino.Plugins.Omemo { + +private class BackedSessionStore : SimpleSessionStore { + private Database db; + private int identity_id; + + public BackedSessionStore(Database db, int identity_id) { + this.db = db; + this.identity_id = identity_id; + init(); + } + + private void init() { + try { + foreach (Row row in db.session.select().with(db.session.identity_id, "=", identity_id)) { + Address addr = new Address(row[db.session.address_name], row[db.session.device_id]); + store_session(addr, Base64.decode(row[db.session.record_base64])); + addr.device_id = 0; + } + } catch (Error e) { + print("Error while initializing session store: %s", e.message); + } + + session_stored.connect(on_session_stored); + session_removed.connect(on_session_deleted); + } + + public void on_session_stored(SessionStore.Session session) { + db.session.insert().or("REPLACE") + .value(db.session.identity_id, identity_id) + .value(db.session.address_name, session.name) + .value(db.session.device_id, session.device_id) + .value(db.session.record_base64, Base64.encode(session.record)) + .perform(); + } + + public void on_session_deleted(SessionStore.Session session) { + db.session.delete() + .with(db.session.identity_id, "=", identity_id) + .with(db.session.address_name, "=", session.name) + .with(db.session.device_id, "=", session.device_id) + .perform(); + } +} + +} diff --git a/plugins/omemo/src/logic/signed_pre_key_store.vala b/plugins/omemo/src/logic/signed_pre_key_store.vala new file mode 100644 index 00000000..8ff54a93 --- /dev/null +++ b/plugins/omemo/src/logic/signed_pre_key_store.vala @@ -0,0 +1,45 @@ +using Qlite; +using Signal; + +namespace Dino.Plugins.Omemo { + +private class BackedSignedPreKeyStore : SimpleSignedPreKeyStore { + private Database db; + private int identity_id; + + public BackedSignedPreKeyStore(Database db, int identity_id) { + this.db = db; + this.identity_id = identity_id; + init(); + } + + private void init() { + try { + foreach (Row row in db.signed_pre_key.select().with(db.signed_pre_key.identity_id, "=", identity_id)) { + store_signed_pre_key(row[db.signed_pre_key.signed_pre_key_id], Base64.decode(row[db.signed_pre_key.record_base64])); + } + } catch (Error e) { + print("Error while initializing signed pre key store: %s", e.message); + } + + signed_pre_key_stored.connect(on_signed_pre_key_stored); + signed_pre_key_deleted.connect(on_signed_pre_key_deleted); + } + + public void on_signed_pre_key_stored(SignedPreKeyStore.Key key) { + db.signed_pre_key.insert().or("REPLACE") + .value(db.signed_pre_key.identity_id, identity_id) + .value(db.signed_pre_key.signed_pre_key_id, (int) key.key_id) + .value(db.signed_pre_key.record_base64, Base64.encode(key.record)) + .perform(); + } + + public void on_signed_pre_key_deleted(SignedPreKeyStore.Key key) { + db.signed_pre_key.delete() + .with(db.signed_pre_key.identity_id, "=", identity_id) + .with(db.signed_pre_key.signed_pre_key_id, "=", (int) key.key_id) + .perform(); + } +} + +} diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala new file mode 100644 index 00000000..d57adc35 --- /dev/null +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -0,0 +1,346 @@ +using Dino.Entities; +using Gee; +using Xmpp; +using Signal; +using Qlite; + +namespace Dino.Plugins.Omemo { + +public class TrustManager { + + private StreamInteractor stream_interactor; + private Database db; + private DecryptMessageListener decrypt_message_listener; + private TagMessageListener tag_message_listener; + + private HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); + + public TrustManager(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; + + decrypt_message_listener = new DecryptMessageListener(stream_interactor, db, message_device_id_map); + tag_message_listener = new TagMessageListener(stream_interactor, db, message_device_id_map); + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener); + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener); + } + + public void set_blind_trust(Account account, Jid jid, bool blind_trust) { + int identity_id = db.identity.get_id(account.id); + if (identity_id < 0) return; + db.trust.update() + .with(db.trust.identity_id, "=", identity_id) + .with(db.trust.address_name, "=", jid.bare_jid.to_string()) + .set(db.trust.blind_trust, blind_trust).perform(); + } + + public void set_device_trust(Account account, Jid jid, int device_id, Database.IdentityMetaTable.TrustLevel trust_level) { + int identity_id = db.identity.get_id(account.id); + db.identity_meta.update() + .with(db.identity_meta.identity_id, "=", identity_id) + .with(db.identity_meta.address_name, "=", jid.bare_jid.to_string()) + .with(db.identity_meta.device_id, "=", device_id) + .set(db.identity_meta.trust_level, trust_level).perform(); + string selection = null; + string[] selection_args = {}; + var app_db = Application.get_default().db; + foreach (Row row in db.content_item_meta.with_device(identity_id, jid.bare_jid.to_string(), device_id).with(db.content_item_meta.trusted_when_received, "=", false)) { + if (selection == null) { + selection = @"$(app_db.content_item.id) = ?"; + } else { + selection += @" OR $(app_db.content_item.id) = ?"; + } + selection_args += row[db.content_item_meta.content_item_id].to_string(); + } + if (selection != null) { + app_db.content_item.update() + .set(app_db.content_item.hide, trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED || trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) + .where(selection, selection_args) + .perform(); + } + } + + private StanzaNode create_encrypted_key(uint8[] key, Address address, Store store) throws GLib.Error { + SessionCipher cipher = store.create_session_cipher(address); + CiphertextMessage device_key = cipher.encrypt(key); + StanzaNode key_node = new StanzaNode.build("key", NS_URI) + .put_attribute("rid", address.device_id.to_string()) + .put_node(new StanzaNode.text(Base64.encode(device_key.serialized))); + if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true"); + return key_node; + } + + public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) { + EncryptState status = new EncryptState(); + if (!Plugin.ensure_context()) return status; + if (message.to == null) return status; + + StreamModule module = stream.get_module(StreamModule.IDENTITY); + + try { + //Check we have the bundles and device lists needed to send the message + if (!is_known_address(account, self_jid)) return status; + status.own_list = true; + status.own_devices = get_trusted_devices(account, self_jid).size; + status.other_waiting_lists = 0; + status.other_devices = 0; + foreach (Jid recipient in recipients) { + if (!is_known_address(account, recipient)) { + status.other_waiting_lists++; + } + if (status.other_waiting_lists > 0) return status; + status.other_devices += get_trusted_devices(account, recipient).size; + } + if (status.own_devices == 0 || status.other_devices == 0) return status; + + //Create a key and use it to encrypt the message + uint8[] key = new uint8[16]; + Plugin.get_context().randomize(key); + uint8[] iv = new uint8[16]; + Plugin.get_context().randomize(iv); + + uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data); + uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16]; + uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length]; + uint8[] keytag = new uint8[key.length + tag.length]; + Memory.copy(keytag, key, key.length); + Memory.copy((uint8*)keytag + key.length, tag, tag.length); + + StanzaNode header; + StanzaNode encrypted = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns() + .put_node(header = new StanzaNode.build("header", NS_URI) + .put_attribute("sid", module.store.local_registration_id.to_string()) + .put_node(new StanzaNode.build("iv", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(iv))))) + .put_node(new StanzaNode.build("payload", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); + + //Encrypt the key for each recipient's device individually + Address address = new Address(message.to.bare_jid.to_string(), 0); + foreach (Jid recipient in recipients) { + foreach(int32 device_id in get_trusted_devices(account, recipient)) { + if (module.is_ignored_device(recipient, device_id)) { + status.other_lost++; + continue; + } + try { + address.name = recipient.bare_jid.to_string(); + address.device_id = (int) device_id; + StanzaNode key_node = create_encrypted_key(keytag, address, module.store); + header.put_node(key_node); + status.other_success++; + } catch (Error e) { + if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; + else status.other_failure++; + } + } + } + address.name = self_jid.bare_jid.to_string(); + foreach(int32 device_id in get_trusted_devices(account, self_jid)) { + if (module.is_ignored_device(self_jid, device_id)) { + status.own_lost++; + continue; + } + if (device_id != module.store.local_registration_id) { + address.device_id = (int) device_id; + try { + StanzaNode key_node = create_encrypted_key(keytag, address, module.store); + header.put_node(key_node); + status.own_success++; + } catch (Error e) { + if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; + else status.own_failure++; + } + } + } + + message.stanza.put_node(encrypted); + Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); + message.body = "[This message is OMEMO encrypted]"; + status.encrypted = true; + } catch (Error e) { + warning(@"Signal error while encrypting message: $(e.message)\n"); + } + return status; + } + + public bool is_known_address(Account account, Jid jid) { + int identity_id = db.identity.get_id(account.id); + if (identity_id < 0) return false; + return db.identity_meta.with_address(identity_id, jid.to_string()).count() > 0; + } + + public Gee.List get_trusted_devices(Account account, Jid jid) { + Gee.List devices = new ArrayList(); + int identity_id = db.identity.get_id(account.id); + if (identity_id < 0) return devices; + foreach (Row device in db.identity_meta.get_trusted_devices(identity_id, jid.bare_jid.to_string())) { + if(device[db.identity_meta.trust_level] != Database.IdentityMetaTable.TrustLevel.UNKNOWN || device[db.identity_meta.identity_key_public_base64] == null) + devices.add(device[db.identity_meta.device_id]); + } + return devices; + } + + private class TagMessageListener : MessageListener { + public string[] after_actions_const = new string[]{ "STORE" }; + public override string action_group { get { return "DECRYPT_TAG"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private StreamInteractor stream_interactor; + private Database db; + private HashMap message_device_id_map; + + public TagMessageListener(StreamInteractor stream_interactor, Database db, HashMap message_device_id_map) { + this.stream_interactor = stream_interactor; + this.db = db; + this.message_device_id_map = message_device_id_map; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + int device_id = 0; + if (message_device_id_map.has_key(message)) { + device_id = message_device_id_map[message]; + message_device_id_map.unset(message); + } + + // TODO: Handling of files + + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); + + if (content_item != null && device_id != 0) { + Jid jid = content_item.jid; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + jid = message.real_jid; + } + + int identity_id = db.identity.get_id(conversation.account.id); + Database.IdentityMetaTable.TrustLevel trust_level = (Database.IdentityMetaTable.TrustLevel) db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), device_id)[db.identity_meta.trust_level]; + if (trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED || trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) { + stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); + } + + db.content_item_meta.insert() + .value(db.content_item_meta.content_item_id, content_item.id) + .value(db.content_item_meta.identity_id, identity_id) + .value(db.content_item_meta.address_name, jid.bare_jid.to_string()) + .value(db.content_item_meta.device_id, device_id) + .value(db.content_item_meta.trusted_when_received, trust_level != Database.IdentityMetaTable.TrustLevel.UNTRUSTED) + .perform(); + } + return false; + } + } + + private class DecryptMessageListener : MessageListener { + public string[] after_actions_const = new string[]{ }; + public override string action_group { get { return "DECRYPT"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private StreamInteractor stream_interactor; + private Database db; + private HashMap message_device_id_map; + + public DecryptMessageListener(StreamInteractor stream_interactor, Database db, HashMap message_device_id_map) { + this.stream_interactor = stream_interactor; + this.db = db; + this.message_device_id_map = message_device_id_map; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + Store store = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY).store; + + StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI); + if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; + StanzaNode encrypted = (!)_encrypted; + if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { + message.body = "[This message is OMEMO encrypted]"; // TODO temporary + }; + if (!Plugin.ensure_context()) return false; + MessageFlag flag = new MessageFlag(); + stanza.add_flag(flag); + StanzaNode? _header = encrypted.get_subnode("header"); + if (_header == null) return false; + StanzaNode header = (!)_header; + int sid = header.get_attribute_int("sid"); + if (sid <= 0) return false; + foreach (StanzaNode key_node in header.get_subnodes("key")) { + if (key_node.get_attribute_int("rid") == store.local_registration_id) { + try { + string? payload = encrypted.get_deep_string_content("payload"); + string? iv_node = header.get_deep_string_content("iv"); + string? key_node_content = key_node.get_string_content(); + if (payload == null || iv_node == null || key_node_content == null) continue; + uint8[] key; + uint8[] ciphertext = Base64.decode((!)payload); + uint8[] iv = Base64.decode((!)iv_node); + Gee.List possible_jids = new ArrayList(); + if (conversation.type_ == Conversation.Type.CHAT) { + possible_jids.add(stanza.from); + } else { + Jid? real_jid = message.real_jid; + if (real_jid != null) { + possible_jids.add(real_jid); + } else { + foreach (Row row in db.identity_meta.get_with_device_id(sid)) { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } + } + } + + foreach (Jid possible_jid in possible_jids) { + try { + Address address = new Address(possible_jid.bare_jid.to_string(), header.get_attribute_int("sid")); + if (key_node.get_attribute_bool("prekey")) { + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_pre_key_signal_message(msg); + } else { + SignalMessage msg = Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_signal_message(msg); + } + //address.device_id = 0; // TODO: Hack to have address obj live longer + + if (key.length >= 32) { + int authtaglength = key.length - 16; + uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; + uint8[] new_key = new uint8[16]; + Memory.copy(new_ciphertext, ciphertext, ciphertext.length); + Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); + Memory.copy(new_key, key, 16); + ciphertext = new_ciphertext; + key = new_key; + } + + message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); + message_device_id_map[message] = address.device_id; + message.encryption = Encryption.OMEMO; + flag.decrypted = true; + } catch (Error e) { + continue; + } + + // If we figured out which real jid a message comes from due to + if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { + message.real_jid = possible_jid; + } + break; + } + } catch (Error e) { + warning(@"Signal error while decrypting message: $(e.message)\n"); + } + } + } + return false; + } + + private string arr_to_str(uint8[] arr) { + // null-terminate the array + uint8[] rarr = new uint8[arr.length+1]; + Memory.copy(rarr, arr, arr.length); + return (string)rarr; + } + } +} + +} diff --git a/plugins/omemo/src/manage_key_dialog.vala b/plugins/omemo/src/manage_key_dialog.vala deleted file mode 100644 index 87d43de8..00000000 --- a/plugins/omemo/src/manage_key_dialog.vala +++ /dev/null @@ -1,166 +0,0 @@ -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/manager.vala b/plugins/omemo/src/manager.vala deleted file mode 100644 index db64c3ee..00000000 --- a/plugins/omemo/src/manager.vala +++ /dev/null @@ -1,386 +0,0 @@ -using Dino.Entities; -using Signal; -using Qlite; -using Xmpp; -using Gee; - -namespace Dino.Plugins.Omemo { - -public class Manager : StreamInteractionModule, Object { - public static ModuleIdentity IDENTITY = new ModuleIdentity("omemo_manager"); - public string id { get { return IDENTITY.id; } } - - private StreamInteractor stream_interactor; - private Database db; - private TrustManager trust_manager; - private Map message_states = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); - - private class MessageState { - public Entities.Message msg { get; private set; } - public EncryptState last_try { get; private set; } - public int waiting_other_sessions { get; set; } - public int waiting_own_sessions { get; set; } - public bool waiting_own_devicelist { get; set; } - public int waiting_other_devicelists { get; set; } - public bool force_next_attempt { get; set; } - public bool will_send_now { get; private set; } - public bool active_send_attempt { get; set; } - - public MessageState(Entities.Message msg, EncryptState last_try) { - this.msg = msg; - this.last_try = last_try; - update_from_encrypt_status(last_try); - } - - public void update_from_encrypt_status(EncryptState new_try) { - this.last_try = new_try; - this.waiting_other_sessions = new_try.other_unknown; - this.waiting_own_sessions = new_try.own_unknown; - this.waiting_own_devicelist = !new_try.own_list; - this.waiting_other_devicelists = new_try.other_waiting_lists; - this.active_send_attempt = false; - will_send_now = false; - if (new_try.other_failure > 0 || (new_try.other_lost == new_try.other_devices && new_try.other_devices > 0)) { - msg.marked = Entities.Message.Marked.WONTSEND; - } else if (new_try.other_unknown > 0 || new_try.own_unknown > 0 || new_try.other_waiting_lists > 0 || !new_try.own_list || new_try.own_devices == 0) { - msg.marked = Entities.Message.Marked.UNSENT; - } else if (!new_try.encrypted) { - msg.marked = Entities.Message.Marked.WONTSEND; - } else { - will_send_now = true; - } - } - - public bool should_retry_now() { - return !waiting_own_devicelist && waiting_other_devicelists <= 0 && waiting_other_sessions <= 0 && waiting_own_sessions <= 0 && !active_send_attempt; - } - - public string to_string() { - return @"MessageState (waiting=(others=$waiting_other_sessions, own=$waiting_own_sessions, other_lists=$waiting_other_devicelists, own_list=$waiting_own_devicelist))"; - } - } - - private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { - this.stream_interactor = stream_interactor; - this.db = db; - this.trust_manager = trust_manager; - - stream_interactor.stream_negotiated.connect(on_stream_negotiated); - stream_interactor.account_added.connect(on_account_added); - stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); - stream_interactor.get_module(RosterManager.IDENTITY).mutual_subscription.connect(on_mutual_subscription); - } - - private Gee.List get_occupants(Jid jid, Account account){ - Gee.List occupants = new ArrayList(Jid.equals_bare_func); - if(!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid, account)){ - occupants.add(jid); - } - Gee.List? occupant_jids = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(jid, account); - if(occupant_jids == null) { - return occupants; - } - foreach (Jid occupant in occupant_jids) { - if(!occupant.equals(account.bare_jid)){ - occupants.add(occupant.bare_jid); - } - } - return occupants; - } - - private void on_pre_message_send(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) { - if (message.encryption == Encryption.OMEMO) { - XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (stream == null) { - message.marked = Entities.Message.Marked.UNSENT; - return; - } - StreamModule? module_ = ((!)stream).get_module(StreamModule.IDENTITY); - if (module_ == null) { - message.marked = Entities.Message.Marked.UNSENT; - return; - } - StreamModule module = (!)module_; - - //Get a list of everyone for whom the message should be encrypted - Gee.List recipients; - if (message_stanza.type_ == MessageStanza.TYPE_GROUPCHAT) { - recipients = get_occupants((!)message.to.bare_jid, conversation.account); - if (recipients.size == 0) { - message.marked = Entities.Message.Marked.WONTSEND; - return; - } - } else { - recipients = new ArrayList(Jid.equals_bare_func); - recipients.add(message_stanza.to); - } - - //Attempt to encrypt the message - EncryptState enc_state = trust_manager.encrypt(message_stanza, conversation.account.bare_jid, recipients, stream, conversation.account); - MessageState state; - lock (message_states) { - if (message_states.has_key(message)) { - state = message_states.get(message); - state.update_from_encrypt_status(enc_state); - } else { - state = new MessageState(message, enc_state); - message_states[message] = state; - } - if (state.will_send_now) { - message_states.unset(message); - } - } - - //Encryption failed - need to fetch more information - if (!state.will_send_now) { - if (message.marked == Entities.Message.Marked.WONTSEND) { - debug("message was not sent: %s", state.to_string()); - message_states.unset(message); - } else { - debug("message will be delayed: %s", state.to_string()); - - if (state.waiting_own_sessions > 0) { - module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid)); - } - if (state.waiting_other_sessions > 0 && message.counterpart != null) { - foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { - module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid)); - } - } - if (state.waiting_other_devicelists > 0 && message.counterpart != null) { - foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { - module.request_user_devicelist((!)stream, jid); - } - } - } - } - } - } - - private void on_mutual_subscription(Account account, Jid jid) { - XmppStream? stream = stream_interactor.get_stream(account); - if(stream == null) return; - - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist((!)stream, jid); - } - - private void on_account_added(Account account) { - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store_created.connect((store) => on_store_created.begin(account, store)); - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices)); - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); - } - - private void on_stream_negotiated(Account account, XmppStream stream) { - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist(stream, account.bare_jid); - } - - private void on_device_list_loaded(Account account, Jid jid, ArrayList device_list) { - debug("received device list for %s from %s", account.bare_jid.to_string(), jid.to_string()); - - XmppStream? stream = stream_interactor.get_stream(account); - if (stream == null) { - return; - } - StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); - if (module == null) { - return; - } - - int identity_id = db.identity.get_id(account.id); - if (identity_id < 0) return; - - //Update meta database - db.identity_meta.insert_device_list(identity_id, jid.bare_jid.to_string(), device_list); - - //Fetch the bundle for each new device - int inc = 0; - foreach (Row row in db.identity_meta.get_unknown_devices(identity_id, jid.bare_jid.to_string())) { - module.fetch_bundle(stream, Jid.parse(row[db.identity_meta.address_name]), row[db.identity_meta.device_id]); - inc++; - } - if (inc > 0) { - debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string()); - } - - //Create an entry for the jid in the account table if one does not exist already - if (db.trust.select().with(db.trust.identity_id, "=", identity_id).with(db.trust.address_name, "=", jid.bare_jid.to_string()).count() == 0) { - db.trust.insert().value(db.trust.identity_id, identity_id).value(db.trust.address_name, jid.bare_jid.to_string()).value(db.trust.blind_trust, true).perform(); - } - - //Get all messages that needed the devicelist and determine if we can now send them - HashSet send_now = new HashSet(); - lock (message_states) { - foreach (Entities.Message msg in message_states.keys) { - if (!msg.account.equals(account)) continue; - Gee.List occupants = get_occupants(msg.counterpart.bare_jid, account); - MessageState state = message_states[msg]; - if (account.bare_jid.equals(jid)) { - state.waiting_own_devicelist = false; - } else if (msg.counterpart != null && occupants.contains(jid)) { - state.waiting_other_devicelists--; - } - if (state.should_retry_now()) { - send_now.add(msg); - state.active_send_attempt = true; - } - } - } - foreach (Entities.Message msg in send_now) { - if (msg.counterpart == null) continue; - Entities.Conversation? conv = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(((!)msg.counterpart), account); - if (conv == null) continue; - stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); - } - - } - - public void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) { - int identity_id = db.identity.get_id(account.id); - if (identity_id < 0) return; - - bool blind_trust = db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string()); - - //If we don't blindly trust new devices and we haven't seen this key before then don't trust it - bool untrust = !(blind_trust || db.identity_meta.with_address(identity_id, jid.bare_jid.to_string()) - .with(db.identity_meta.device_id, "=", device_id) - .with(db.identity_meta.identity_key_public_base64, "=", Base64.encode(bundle.identity_key.serialize())) - .single().row().is_present()); - - //Get trust information from the database if the device id is known - Row device = db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), device_id); - Database.IdentityMetaTable.TrustLevel trusted = Database.IdentityMetaTable.TrustLevel.UNKNOWN; - if (device != null) { - trusted = (Database.IdentityMetaTable.TrustLevel) device[db.identity_meta.trust_level]; - } - - if(untrust) { - trusted = Database.IdentityMetaTable.TrustLevel.UNKNOWN; - } else if (blind_trust && trusted == Database.IdentityMetaTable.TrustLevel.UNKNOWN) { - trusted = Database.IdentityMetaTable.TrustLevel.TRUSTED; - } - - //Update the database with the appropriate trust information - db.identity_meta.insert_device_bundle(identity_id, jid.bare_jid.to_string(), device_id, bundle, trusted); - - XmppStream? stream = stream_interactor.get_stream(account); - if(stream == null) return; - StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); - if(module == null) return; - - //Get all messages waiting on the bundle and determine if they can now be sent - HashSet send_now = new HashSet(); - lock (message_states) { - foreach (Entities.Message msg in message_states.keys) { - - bool session_created = true; - if (!msg.account.equals(account)) continue; - Gee.List occupants = get_occupants(msg.counterpart.bare_jid, account); - - MessageState state = message_states[msg]; - - if (trusted == Database.IdentityMetaTable.TrustLevel.TRUSTED || trusted == Database.IdentityMetaTable.TrustLevel.VERIFIED) { - if(account.bare_jid.equals(jid) || (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)))) { - session_created = module.start_session(stream, jid, device_id, bundle); - } - } - if (account.bare_jid.equals(jid) && session_created) { - state.waiting_own_sessions--; - } else if (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)) && session_created) { - state.waiting_other_sessions--; - } - if (state.should_retry_now()){ - send_now.add(msg); - state.active_send_attempt = true; - } - } - } - foreach (Entities.Message msg in send_now) { - if (msg.counterpart == null) continue; - Entities.Conversation? conv = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation((!)msg.counterpart, account); - if (conv == null) continue; - stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); - } - } - - private async void on_store_created(Account account, Store store) { - // If the account is not yet persisted, wait for that and then continue - without identity.account_id the entry isn't worth much. - if (account.id == -1) { - account.notify["id"].connect(() => on_store_created.callback()); - yield; - } - Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner; - int identity_id = -1; - bool publish_identity = false; - - if (row == null) { - // OMEMO not yet initialized, starting with empty base - publish_identity = true; - try { - store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX); - - Signal.ECKeyPair key_pair = Plugin.get_context().generate_key_pair(); - store.identity_key_store.identity_key_private = key_pair.private.serialize(); - store.identity_key_store.identity_key_public = key_pair.public.serialize(); - - identity_id = (int) db.identity.insert().or("REPLACE") - .value(db.identity.account_id, account.id) - .value(db.identity.device_id, (int) store.local_registration_id) - .value(db.identity.identity_key_private_base64, Base64.encode(store.identity_key_store.identity_key_private)) - .value(db.identity.identity_key_public_base64, Base64.encode(store.identity_key_store.identity_key_public)) - .perform(); - } catch (Error e) { - // Ignore error - } - } else { - store.identity_key_store.local_registration_id = ((!)row)[db.identity.device_id]; - store.identity_key_store.identity_key_private = Base64.decode(((!)row)[db.identity.identity_key_private_base64]); - store.identity_key_store.identity_key_public = Base64.decode(((!)row)[db.identity.identity_key_public_base64]); - identity_id = ((!)row)[db.identity.id]; - } - - if (identity_id >= 0) { - store.signed_pre_key_store = new BackedSignedPreKeyStore(db, identity_id); - store.pre_key_store = new BackedPreKeyStore(db, identity_id); - store.session_store = new BackedSessionStore(db, identity_id); - } else { - warning("store for %s is not persisted!", account.bare_jid.to_string()); - } - - // Generated new device ID, ensure this gets added to the devicelist - if (publish_identity) { - XmppStream? stream = stream_interactor.get_stream(account); - if (stream == null) return; - StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); - if(module == null) return; - module.request_user_devicelist(stream, account.bare_jid); - } - } - - - public bool can_encrypt(Entities.Conversation conversation) { - XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (stream == null) return false; - if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(conversation.counterpart, conversation.account)){ - Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY); - if (flag == null) return false; - if (flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.NON_ANONYMOUS) && flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.MEMBERS_ONLY)) { - foreach(Jid jid in stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account)) { - if (!trust_manager.is_known_address(conversation.account, jid.bare_jid)) { - return false; - } - } - return true; - } else { - return false; - } - } - return trust_manager.is_known_address(conversation.account, conversation.counterpart.bare_jid); - } - - public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { - Manager m = new Manager(stream_interactor, db, trust_manager); - stream_interactor.add_module(m); - } -} - -} diff --git a/plugins/omemo/src/message_flag.vala b/plugins/omemo/src/message_flag.vala deleted file mode 100644 index ba9ea16e..00000000 --- a/plugins/omemo/src/message_flag.vala +++ /dev/null @@ -1,23 +0,0 @@ -using Xmpp; - -namespace Dino.Plugins.Omemo { - -public class MessageFlag : Xmpp.MessageFlag { - public const string id = "omemo"; - - public bool decrypted = false; - - public static MessageFlag? get_flag(MessageStanza message) { - return (MessageFlag) message.get_flag(NS_URI, id); - } - - public override string get_ns() { - return NS_URI; - } - - public override string get_id() { - return id; - } -} - -} \ No newline at end of file diff --git a/plugins/omemo/src/own_notifications.vala b/plugins/omemo/src/own_notifications.vala deleted file mode 100644 index f882d03a..00000000 --- a/plugins/omemo/src/own_notifications.vala +++ /dev/null @@ -1,42 +0,0 @@ -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/pre_key_store.vala b/plugins/omemo/src/pre_key_store.vala deleted file mode 100644 index 716fd32f..00000000 --- a/plugins/omemo/src/pre_key_store.vala +++ /dev/null @@ -1,45 +0,0 @@ -using Signal; -using Qlite; - -namespace Dino.Plugins.Omemo { - -private class BackedPreKeyStore : SimplePreKeyStore { - private Database db; - private int identity_id; - - public BackedPreKeyStore(Database db, int identity_id) { - this.db = db; - this.identity_id = identity_id; - init(); - } - - private void init() { - try { - foreach (Row row in db.pre_key.select().with(db.pre_key.identity_id, "=", identity_id)) { - store_pre_key(row[db.pre_key.pre_key_id], Base64.decode(row[db.pre_key.record_base64])); - } - } catch (Error e) { - warning("Error while initializing pre key store: %s", e.message); - } - - pre_key_stored.connect(on_pre_key_stored); - pre_key_deleted.connect(on_pre_key_deleted); - } - - public void on_pre_key_stored(PreKeyStore.Key key) { - db.pre_key.insert().or("REPLACE") - .value(db.pre_key.identity_id, identity_id) - .value(db.pre_key.pre_key_id, (int) key.key_id) - .value(db.pre_key.record_base64, Base64.encode(key.record)) - .perform(); - } - - public void on_pre_key_deleted(PreKeyStore.Key key) { - db.pre_key.delete() - .with(db.pre_key.identity_id, "=", identity_id) - .with(db.pre_key.pre_key_id, "=", (int) key.key_id) - .perform(); - } -} - -} diff --git a/plugins/omemo/src/protocol/bundle.vala b/plugins/omemo/src/protocol/bundle.vala new file mode 100644 index 00000000..9b01f299 --- /dev/null +++ b/plugins/omemo/src/protocol/bundle.vala @@ -0,0 +1,88 @@ +using Gee; +using Signal; +using Xmpp; + +namespace Dino.Plugins.Omemo { + +public class Bundle { + private StanzaNode? node; + + public Bundle(StanzaNode? node) { + this.node = node; + assert(Plugin.ensure_context()); + } + + public int32 signed_pre_key_id { owned get { + if (node == null) return -1; + string? id = ((!)node).get_deep_attribute("signedPreKeyPublic", "signedPreKeyId"); + if (id == null) return -1; + return int.parse((!)id); + }} + + public ECPublicKey? signed_pre_key { owned get { + if (node == null) return null; + string? key = ((!)node).get_deep_string_content("signedPreKeyPublic"); + if (key == null) return null; + try { + return Plugin.get_context().decode_public_key(Base64.decode((!)key)); + } catch (Error e) { + return null; + } + }} + + public uint8[]? signed_pre_key_signature { owned get { + if (node == null) return null; + string? sig = ((!)node).get_deep_string_content("signedPreKeySignature"); + if (sig == null) return null; + return Base64.decode((!)sig); + }} + + public ECPublicKey? identity_key { owned get { + if (node == null) return null; + string? key = ((!)node).get_deep_string_content("identityKey"); + if (key == null) return null; + try { + return Plugin.get_context().decode_public_key(Base64.decode((!)key)); + } catch (Error e) { + return null; + } + }} + + public ArrayList pre_keys { owned get { + ArrayList list = new ArrayList(); + if (node == null || ((!)node).get_subnode("prekeys") == null) return list; + ((!)node).get_deep_subnodes("prekeys", "preKeyPublic") + .filter((node) => ((!)node).get_attribute("preKeyId") != null) + .map(PreKey.create) + .foreach((key) => list.add(key)); + return list; + }} + + public class PreKey { + private StanzaNode node; + + public static PreKey create(owned StanzaNode node) { + return new PreKey(node); + } + + public PreKey(StanzaNode node) { + this.node = node; + } + + public int32 key_id { owned get { + return int.parse(node.get_attribute("preKeyId") ?? "-1"); + }} + + public ECPublicKey? key { owned get { + string? key = node.get_string_content(); + if (key == null) return null; + try { + return Plugin.get_context().decode_public_key(Base64.decode((!)key)); + } catch (Error e) { + return null; + } + }} + } +} + +} \ No newline at end of file diff --git a/plugins/omemo/src/protocol/message_flag.vala b/plugins/omemo/src/protocol/message_flag.vala new file mode 100644 index 00000000..ba9ea16e --- /dev/null +++ b/plugins/omemo/src/protocol/message_flag.vala @@ -0,0 +1,23 @@ +using Xmpp; + +namespace Dino.Plugins.Omemo { + +public class MessageFlag : Xmpp.MessageFlag { + public const string id = "omemo"; + + public bool decrypted = false; + + public static MessageFlag? get_flag(MessageStanza message) { + return (MessageFlag) message.get_flag(NS_URI, id); + } + + public override string get_ns() { + return NS_URI; + } + + public override string get_id() { + return id; + } +} + +} \ No newline at end of file diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala new file mode 100644 index 00000000..555fd68a --- /dev/null +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -0,0 +1,271 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; +using Signal; + +namespace Dino.Plugins.Omemo { + +private const string NS_URI = "eu.siacs.conversations.axolotl"; +private const string NODE_DEVICELIST = NS_URI + ".devicelist"; +private const string NODE_BUNDLES = NS_URI + ".bundles"; +private const string NODE_VERIFICATION = NS_URI + ".verification"; + +private const int NUM_KEYS_TO_PUBLISH = 100; + +public class StreamModule : XmppStreamModule { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "omemo_module"); + + public Store store { public get; private set; } + private ConcurrentSet active_bundle_requests = new ConcurrentSet(); + private ConcurrentSet active_devicelist_requests = new ConcurrentSet(); + private Map> ignored_devices = new HashMap>(Jid.hash_bare_func, Jid.equals_bare_func); + + public signal void store_created(Store store); + public signal void device_list_loaded(Jid jid, ArrayList devices); + public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); + + public override void attach(XmppStream stream) { + if (!Plugin.ensure_context()) return; + + this.store = Plugin.get_context().create_store(); + store_created(store); + stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => on_devicelist(stream, jid, id, node)); + } + + public override void detach(XmppStream stream) { + } + + public void request_user_devicelist(XmppStream stream, Jid jid) { + if (active_devicelist_requests.add(jid)) { + debug("requesting device list for %s", jid.to_string()); + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => on_devicelist(stream, jid, id, node)); + } + } + + public void on_devicelist(XmppStream stream, Jid jid, string? id, StanzaNode? node_) { + StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns(); + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) return; + if (jid.equals_bare(my_jid) && store.local_registration_id != 0) { + bool am_on_devicelist = false; + foreach (StanzaNode device_node in node.get_subnodes("device")) { + int device_id = device_node.get_attribute_int("id"); + if (store.local_registration_id == device_id) { + am_on_devicelist = true; + } + } + if (!am_on_devicelist) { + debug(@"Not on device list, adding id"); + node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string())); + stream.get_module(Pubsub.Module.IDENTITY).publish(stream, jid, NODE_DEVICELIST, NODE_DEVICELIST, id, node); + } + publish_bundles_if_needed(stream, jid); + } + + ArrayList device_list = new ArrayList(); + foreach (StanzaNode device_node in node.get_subnodes("device")) { + device_list.add(device_node.get_attribute_int("id")); + } + active_devicelist_requests.remove(jid); + device_list_loaded(jid, device_list); + } + + public void fetch_bundles(XmppStream stream, Jid jid, Gee.List devices) { + Address address = new Address(jid.bare_jid.to_string(), 0); + foreach(int32 device_id in devices) { + if (!is_ignored_device(jid, device_id)) { + address.device_id = device_id; + try { + if (!store.contains_session(address)) { + fetch_bundle(stream, jid, device_id); + } + } catch (Error e) { + // Ignore + } + } + } + address.device_id = 0; + } + + public void fetch_bundle(XmppStream stream, Jid jid, int device_id) { + if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) { + debug(@"Asking for bundle from %s: %i", jid.bare_jid.to_string(), device_id); + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => { + on_other_bundle_result(stream, jid, device_id, id, node); + }); + } + } + + public void ignore_device(Jid jid, int32 device_id) { + if (device_id <= 0) return; + lock (ignored_devices) { + if (!ignored_devices.has_key(jid)) { + ignored_devices[jid] = new ArrayList(); + } + ignored_devices[jid].add(device_id); + } + } + + public bool is_ignored_device(Jid jid, int32 device_id) { + if (device_id <= 0) return true; + lock (ignored_devices) { + return ignored_devices.has_key(jid) && ignored_devices[jid].contains(device_id); + } + } + + private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node) { + if (node == null) { + // Device not registered, shouldn't exist + stream.get_module(IDENTITY).ignore_device(jid, device_id); + } else { + Bundle bundle = new Bundle(node); + bundle_fetched(jid, device_id, bundle); + } + stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id"); + } + + public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) { + bool fail = false; + int32 signed_pre_key_id = bundle.signed_pre_key_id; + ECPublicKey? signed_pre_key = bundle.signed_pre_key; + uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature; + ECPublicKey? identity_key = bundle.identity_key; + + ArrayList pre_keys = bundle.pre_keys; + if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) { + fail = true; + } else { + int pre_key_idx = Random.int_range(0, pre_keys.size); + int32 pre_key_id = pre_keys[pre_key_idx].key_id; + ECPublicKey? pre_key = pre_keys[pre_key_idx].key; + if (pre_key_id < 0 || pre_key == null) { + fail = true; + } else { + Address address = new Address(jid.bare_jid.to_string(), device_id); + try { + if (store.contains_session(address)) { + return false; + } + SessionBuilder builder = store.create_session_builder(address); + builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key)); + } catch (Error e) { + fail = true; + } + address.device_id = 0; // TODO: Hack to have address obj live longer + } + } + if (fail) { + stream.get_module(IDENTITY).ignore_device(jid, device_id); + } + return true; + } + + public void publish_bundles_if_needed(XmppStream stream, Jid jid) { + if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) { + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result); + } + } + + private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) { + if (!Plugin.ensure_context()) return; + Map keys = new HashMap(); + ECPublicKey? identity_key = null; + int32 signed_pre_key_id = -1; + ECPublicKey? signed_pre_key = null; + SignedPreKeyRecord? signed_pre_key_record = null; + bool changed = false; + if (node == null) { + identity_key = store.identity_key_pair.public; + changed = true; + } else { + Bundle bundle = new Bundle(node); + foreach (Bundle.PreKey prekey in bundle.pre_keys) { + ECPublicKey? key = prekey.key; + if (key != null) { + keys[prekey.key_id] = (!)key; + } + } + identity_key = bundle.identity_key; + signed_pre_key_id = bundle.signed_pre_key_id;; + signed_pre_key = bundle.signed_pre_key; + } + + try { + // Validate IdentityKey + if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) { + changed = true; + } + IdentityKeyPair identity_key_pair = store.identity_key_pair; + + // Validate signedPreKeyRecord + ID + if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) { + signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number + signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id); + store.store_signed_pre_key((!)signed_pre_key_record); + changed = true; + } else { + signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id); + } + + // Validate PreKeys + Set pre_key_records = new HashSet(); + foreach (var entry in keys.entries) { + if (store.contains_pre_key(entry.key)) { + PreKeyRecord record = store.load_pre_key(entry.key); + if (record.key_pair.public.compare(entry.value) == 0) { + pre_key_records.add(record); + } + } + } + int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size; + if (new_keys > 0) { + int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number + Set new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys); + pre_key_records.add_all(new_records); + foreach (PreKeyRecord record in new_records) { + store.store_pre_key(record); + } + changed = true; + } + + if (changed) { + publish_bundles(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id); + } + } catch (Error e) { + warning(@"Unexpected error while publishing bundle: $(e.message)\n"); + } + stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)"); + } + + public static void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set pre_key_records, int32 device_id) throws Error { + ECKeyPair tmp; + StanzaNode bundle = new StanzaNode.build("bundle", NS_URI) + .add_self_xmlns() + .put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI) + .put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string()) + .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize())))) + .put_node(new StanzaNode.build("signedPreKeySignature", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature)))) + .put_node(new StanzaNode.build("identityKey", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize())))); + StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI); + foreach (PreKeyRecord pre_key_record in pre_key_records) { + prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI) + .put_attribute("preKeyId", pre_key_record.id.to_string()) + .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize())))); + } + bundle.put_node(prekeys); + + stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", @"$NODE_BUNDLES:$device_id", "1", bundle); + } + + public override string get_ns() { + return NS_URI; + } + + public override string get_id() { + return IDENTITY.id; + } +} + +} diff --git a/plugins/omemo/src/session_store.vala b/plugins/omemo/src/session_store.vala deleted file mode 100644 index 654591d1..00000000 --- a/plugins/omemo/src/session_store.vala +++ /dev/null @@ -1,49 +0,0 @@ -using Signal; -using Qlite; - -namespace Dino.Plugins.Omemo { - -private class BackedSessionStore : SimpleSessionStore { - private Database db; - private int identity_id; - - public BackedSessionStore(Database db, int identity_id) { - this.db = db; - this.identity_id = identity_id; - init(); - } - - private void init() { - try { - foreach (Row row in db.session.select().with(db.session.identity_id, "=", identity_id)) { - Address addr = new Address(row[db.session.address_name], row[db.session.device_id]); - store_session(addr, Base64.decode(row[db.session.record_base64])); - addr.device_id = 0; - } - } catch (Error e) { - print("Error while initializing session store: %s", e.message); - } - - session_stored.connect(on_session_stored); - session_removed.connect(on_session_deleted); - } - - public void on_session_stored(SessionStore.Session session) { - db.session.insert().or("REPLACE") - .value(db.session.identity_id, identity_id) - .value(db.session.address_name, session.name) - .value(db.session.device_id, session.device_id) - .value(db.session.record_base64, Base64.encode(session.record)) - .perform(); - } - - public void on_session_deleted(SessionStore.Session session) { - db.session.delete() - .with(db.session.identity_id, "=", identity_id) - .with(db.session.address_name, "=", session.name) - .with(db.session.device_id, "=", session.device_id) - .perform(); - } -} - -} diff --git a/plugins/omemo/src/signed_pre_key_store.vala b/plugins/omemo/src/signed_pre_key_store.vala deleted file mode 100644 index 8ff54a93..00000000 --- a/plugins/omemo/src/signed_pre_key_store.vala +++ /dev/null @@ -1,45 +0,0 @@ -using Qlite; -using Signal; - -namespace Dino.Plugins.Omemo { - -private class BackedSignedPreKeyStore : SimpleSignedPreKeyStore { - private Database db; - private int identity_id; - - public BackedSignedPreKeyStore(Database db, int identity_id) { - this.db = db; - this.identity_id = identity_id; - init(); - } - - private void init() { - try { - foreach (Row row in db.signed_pre_key.select().with(db.signed_pre_key.identity_id, "=", identity_id)) { - store_signed_pre_key(row[db.signed_pre_key.signed_pre_key_id], Base64.decode(row[db.signed_pre_key.record_base64])); - } - } catch (Error e) { - print("Error while initializing signed pre key store: %s", e.message); - } - - signed_pre_key_stored.connect(on_signed_pre_key_stored); - signed_pre_key_deleted.connect(on_signed_pre_key_deleted); - } - - public void on_signed_pre_key_stored(SignedPreKeyStore.Key key) { - db.signed_pre_key.insert().or("REPLACE") - .value(db.signed_pre_key.identity_id, identity_id) - .value(db.signed_pre_key.signed_pre_key_id, (int) key.key_id) - .value(db.signed_pre_key.record_base64, Base64.encode(key.record)) - .perform(); - } - - public void on_signed_pre_key_deleted(SignedPreKeyStore.Key key) { - db.signed_pre_key.delete() - .with(db.signed_pre_key.identity_id, "=", identity_id) - .with(db.signed_pre_key.signed_pre_key_id, "=", (int) key.key_id) - .perform(); - } -} - -} diff --git a/plugins/omemo/src/stream_module.vala b/plugins/omemo/src/stream_module.vala deleted file mode 100644 index 555fd68a..00000000 --- a/plugins/omemo/src/stream_module.vala +++ /dev/null @@ -1,271 +0,0 @@ -using Gee; -using Xmpp; -using Xmpp.Xep; -using Signal; - -namespace Dino.Plugins.Omemo { - -private const string NS_URI = "eu.siacs.conversations.axolotl"; -private const string NODE_DEVICELIST = NS_URI + ".devicelist"; -private const string NODE_BUNDLES = NS_URI + ".bundles"; -private const string NODE_VERIFICATION = NS_URI + ".verification"; - -private const int NUM_KEYS_TO_PUBLISH = 100; - -public class StreamModule : XmppStreamModule { - public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "omemo_module"); - - public Store store { public get; private set; } - private ConcurrentSet active_bundle_requests = new ConcurrentSet(); - private ConcurrentSet active_devicelist_requests = new ConcurrentSet(); - private Map> ignored_devices = new HashMap>(Jid.hash_bare_func, Jid.equals_bare_func); - - public signal void store_created(Store store); - public signal void device_list_loaded(Jid jid, ArrayList devices); - public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); - - public override void attach(XmppStream stream) { - if (!Plugin.ensure_context()) return; - - this.store = Plugin.get_context().create_store(); - store_created(store); - stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => on_devicelist(stream, jid, id, node)); - } - - public override void detach(XmppStream stream) { - } - - public void request_user_devicelist(XmppStream stream, Jid jid) { - if (active_devicelist_requests.add(jid)) { - debug("requesting device list for %s", jid.to_string()); - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => on_devicelist(stream, jid, id, node)); - } - } - - public void on_devicelist(XmppStream stream, Jid jid, string? id, StanzaNode? node_) { - StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns(); - Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; - if (my_jid == null) return; - if (jid.equals_bare(my_jid) && store.local_registration_id != 0) { - bool am_on_devicelist = false; - foreach (StanzaNode device_node in node.get_subnodes("device")) { - int device_id = device_node.get_attribute_int("id"); - if (store.local_registration_id == device_id) { - am_on_devicelist = true; - } - } - if (!am_on_devicelist) { - debug(@"Not on device list, adding id"); - node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string())); - stream.get_module(Pubsub.Module.IDENTITY).publish(stream, jid, NODE_DEVICELIST, NODE_DEVICELIST, id, node); - } - publish_bundles_if_needed(stream, jid); - } - - ArrayList device_list = new ArrayList(); - foreach (StanzaNode device_node in node.get_subnodes("device")) { - device_list.add(device_node.get_attribute_int("id")); - } - active_devicelist_requests.remove(jid); - device_list_loaded(jid, device_list); - } - - public void fetch_bundles(XmppStream stream, Jid jid, Gee.List devices) { - Address address = new Address(jid.bare_jid.to_string(), 0); - foreach(int32 device_id in devices) { - if (!is_ignored_device(jid, device_id)) { - address.device_id = device_id; - try { - if (!store.contains_session(address)) { - fetch_bundle(stream, jid, device_id); - } - } catch (Error e) { - // Ignore - } - } - } - address.device_id = 0; - } - - public void fetch_bundle(XmppStream stream, Jid jid, int device_id) { - if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) { - debug(@"Asking for bundle from %s: %i", jid.bare_jid.to_string(), device_id); - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => { - on_other_bundle_result(stream, jid, device_id, id, node); - }); - } - } - - public void ignore_device(Jid jid, int32 device_id) { - if (device_id <= 0) return; - lock (ignored_devices) { - if (!ignored_devices.has_key(jid)) { - ignored_devices[jid] = new ArrayList(); - } - ignored_devices[jid].add(device_id); - } - } - - public bool is_ignored_device(Jid jid, int32 device_id) { - if (device_id <= 0) return true; - lock (ignored_devices) { - return ignored_devices.has_key(jid) && ignored_devices[jid].contains(device_id); - } - } - - private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node) { - if (node == null) { - // Device not registered, shouldn't exist - stream.get_module(IDENTITY).ignore_device(jid, device_id); - } else { - Bundle bundle = new Bundle(node); - bundle_fetched(jid, device_id, bundle); - } - stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id"); - } - - public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) { - bool fail = false; - int32 signed_pre_key_id = bundle.signed_pre_key_id; - ECPublicKey? signed_pre_key = bundle.signed_pre_key; - uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature; - ECPublicKey? identity_key = bundle.identity_key; - - ArrayList pre_keys = bundle.pre_keys; - if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) { - fail = true; - } else { - int pre_key_idx = Random.int_range(0, pre_keys.size); - int32 pre_key_id = pre_keys[pre_key_idx].key_id; - ECPublicKey? pre_key = pre_keys[pre_key_idx].key; - if (pre_key_id < 0 || pre_key == null) { - fail = true; - } else { - Address address = new Address(jid.bare_jid.to_string(), device_id); - try { - if (store.contains_session(address)) { - return false; - } - SessionBuilder builder = store.create_session_builder(address); - builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key)); - } catch (Error e) { - fail = true; - } - address.device_id = 0; // TODO: Hack to have address obj live longer - } - } - if (fail) { - stream.get_module(IDENTITY).ignore_device(jid, device_id); - } - return true; - } - - public void publish_bundles_if_needed(XmppStream stream, Jid jid) { - if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) { - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result); - } - } - - private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) { - if (!Plugin.ensure_context()) return; - Map keys = new HashMap(); - ECPublicKey? identity_key = null; - int32 signed_pre_key_id = -1; - ECPublicKey? signed_pre_key = null; - SignedPreKeyRecord? signed_pre_key_record = null; - bool changed = false; - if (node == null) { - identity_key = store.identity_key_pair.public; - changed = true; - } else { - Bundle bundle = new Bundle(node); - foreach (Bundle.PreKey prekey in bundle.pre_keys) { - ECPublicKey? key = prekey.key; - if (key != null) { - keys[prekey.key_id] = (!)key; - } - } - identity_key = bundle.identity_key; - signed_pre_key_id = bundle.signed_pre_key_id;; - signed_pre_key = bundle.signed_pre_key; - } - - try { - // Validate IdentityKey - if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) { - changed = true; - } - IdentityKeyPair identity_key_pair = store.identity_key_pair; - - // Validate signedPreKeyRecord + ID - if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) { - signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number - signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id); - store.store_signed_pre_key((!)signed_pre_key_record); - changed = true; - } else { - signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id); - } - - // Validate PreKeys - Set pre_key_records = new HashSet(); - foreach (var entry in keys.entries) { - if (store.contains_pre_key(entry.key)) { - PreKeyRecord record = store.load_pre_key(entry.key); - if (record.key_pair.public.compare(entry.value) == 0) { - pre_key_records.add(record); - } - } - } - int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size; - if (new_keys > 0) { - int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number - Set new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys); - pre_key_records.add_all(new_records); - foreach (PreKeyRecord record in new_records) { - store.store_pre_key(record); - } - changed = true; - } - - if (changed) { - publish_bundles(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id); - } - } catch (Error e) { - warning(@"Unexpected error while publishing bundle: $(e.message)\n"); - } - stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)"); - } - - public static void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set pre_key_records, int32 device_id) throws Error { - ECKeyPair tmp; - StanzaNode bundle = new StanzaNode.build("bundle", NS_URI) - .add_self_xmlns() - .put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI) - .put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string()) - .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize())))) - .put_node(new StanzaNode.build("signedPreKeySignature", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature)))) - .put_node(new StanzaNode.build("identityKey", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize())))); - StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI); - foreach (PreKeyRecord pre_key_record in pre_key_records) { - prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI) - .put_attribute("preKeyId", pre_key_record.id.to_string()) - .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize())))); - } - bundle.put_node(prekeys); - - stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", @"$NODE_BUNDLES:$device_id", "1", bundle); - } - - public override string get_ns() { - return NS_URI; - } - - public override string get_id() { - return IDENTITY.id; - } -} - -} diff --git a/plugins/omemo/src/trust_manager.vala b/plugins/omemo/src/trust_manager.vala deleted file mode 100644 index d57adc35..00000000 --- a/plugins/omemo/src/trust_manager.vala +++ /dev/null @@ -1,346 +0,0 @@ -using Dino.Entities; -using Gee; -using Xmpp; -using Signal; -using Qlite; - -namespace Dino.Plugins.Omemo { - -public class TrustManager { - - private StreamInteractor stream_interactor; - private Database db; - private DecryptMessageListener decrypt_message_listener; - private TagMessageListener tag_message_listener; - - private HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); - - public TrustManager(StreamInteractor stream_interactor, Database db) { - this.stream_interactor = stream_interactor; - this.db = db; - - decrypt_message_listener = new DecryptMessageListener(stream_interactor, db, message_device_id_map); - tag_message_listener = new TagMessageListener(stream_interactor, db, message_device_id_map); - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener); - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener); - } - - public void set_blind_trust(Account account, Jid jid, bool blind_trust) { - int identity_id = db.identity.get_id(account.id); - if (identity_id < 0) return; - db.trust.update() - .with(db.trust.identity_id, "=", identity_id) - .with(db.trust.address_name, "=", jid.bare_jid.to_string()) - .set(db.trust.blind_trust, blind_trust).perform(); - } - - public void set_device_trust(Account account, Jid jid, int device_id, Database.IdentityMetaTable.TrustLevel trust_level) { - int identity_id = db.identity.get_id(account.id); - db.identity_meta.update() - .with(db.identity_meta.identity_id, "=", identity_id) - .with(db.identity_meta.address_name, "=", jid.bare_jid.to_string()) - .with(db.identity_meta.device_id, "=", device_id) - .set(db.identity_meta.trust_level, trust_level).perform(); - string selection = null; - string[] selection_args = {}; - var app_db = Application.get_default().db; - foreach (Row row in db.content_item_meta.with_device(identity_id, jid.bare_jid.to_string(), device_id).with(db.content_item_meta.trusted_when_received, "=", false)) { - if (selection == null) { - selection = @"$(app_db.content_item.id) = ?"; - } else { - selection += @" OR $(app_db.content_item.id) = ?"; - } - selection_args += row[db.content_item_meta.content_item_id].to_string(); - } - if (selection != null) { - app_db.content_item.update() - .set(app_db.content_item.hide, trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED || trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) - .where(selection, selection_args) - .perform(); - } - } - - private StanzaNode create_encrypted_key(uint8[] key, Address address, Store store) throws GLib.Error { - SessionCipher cipher = store.create_session_cipher(address); - CiphertextMessage device_key = cipher.encrypt(key); - StanzaNode key_node = new StanzaNode.build("key", NS_URI) - .put_attribute("rid", address.device_id.to_string()) - .put_node(new StanzaNode.text(Base64.encode(device_key.serialized))); - if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true"); - return key_node; - } - - public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) { - EncryptState status = new EncryptState(); - if (!Plugin.ensure_context()) return status; - if (message.to == null) return status; - - StreamModule module = stream.get_module(StreamModule.IDENTITY); - - try { - //Check we have the bundles and device lists needed to send the message - if (!is_known_address(account, self_jid)) return status; - status.own_list = true; - status.own_devices = get_trusted_devices(account, self_jid).size; - status.other_waiting_lists = 0; - status.other_devices = 0; - foreach (Jid recipient in recipients) { - if (!is_known_address(account, recipient)) { - status.other_waiting_lists++; - } - if (status.other_waiting_lists > 0) return status; - status.other_devices += get_trusted_devices(account, recipient).size; - } - if (status.own_devices == 0 || status.other_devices == 0) return status; - - //Create a key and use it to encrypt the message - uint8[] key = new uint8[16]; - Plugin.get_context().randomize(key); - uint8[] iv = new uint8[16]; - Plugin.get_context().randomize(iv); - - uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data); - uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16]; - uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length]; - uint8[] keytag = new uint8[key.length + tag.length]; - Memory.copy(keytag, key, key.length); - Memory.copy((uint8*)keytag + key.length, tag, tag.length); - - StanzaNode header; - StanzaNode encrypted = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns() - .put_node(header = new StanzaNode.build("header", NS_URI) - .put_attribute("sid", module.store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(iv))))) - .put_node(new StanzaNode.build("payload", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); - - //Encrypt the key for each recipient's device individually - Address address = new Address(message.to.bare_jid.to_string(), 0); - foreach (Jid recipient in recipients) { - foreach(int32 device_id in get_trusted_devices(account, recipient)) { - if (module.is_ignored_device(recipient, device_id)) { - status.other_lost++; - continue; - } - try { - address.name = recipient.bare_jid.to_string(); - address.device_id = (int) device_id; - StanzaNode key_node = create_encrypted_key(keytag, address, module.store); - header.put_node(key_node); - status.other_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; - else status.other_failure++; - } - } - } - address.name = self_jid.bare_jid.to_string(); - foreach(int32 device_id in get_trusted_devices(account, self_jid)) { - if (module.is_ignored_device(self_jid, device_id)) { - status.own_lost++; - continue; - } - if (device_id != module.store.local_registration_id) { - address.device_id = (int) device_id; - try { - StanzaNode key_node = create_encrypted_key(keytag, address, module.store); - header.put_node(key_node); - status.own_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; - else status.own_failure++; - } - } - } - - message.stanza.put_node(encrypted); - Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); - message.body = "[This message is OMEMO encrypted]"; - status.encrypted = true; - } catch (Error e) { - warning(@"Signal error while encrypting message: $(e.message)\n"); - } - return status; - } - - public bool is_known_address(Account account, Jid jid) { - int identity_id = db.identity.get_id(account.id); - if (identity_id < 0) return false; - return db.identity_meta.with_address(identity_id, jid.to_string()).count() > 0; - } - - public Gee.List get_trusted_devices(Account account, Jid jid) { - Gee.List devices = new ArrayList(); - int identity_id = db.identity.get_id(account.id); - if (identity_id < 0) return devices; - foreach (Row device in db.identity_meta.get_trusted_devices(identity_id, jid.bare_jid.to_string())) { - if(device[db.identity_meta.trust_level] != Database.IdentityMetaTable.TrustLevel.UNKNOWN || device[db.identity_meta.identity_key_public_base64] == null) - devices.add(device[db.identity_meta.device_id]); - } - return devices; - } - - private class TagMessageListener : MessageListener { - public string[] after_actions_const = new string[]{ "STORE" }; - public override string action_group { get { return "DECRYPT_TAG"; } } - public override string[] after_actions { get { return after_actions_const; } } - - private StreamInteractor stream_interactor; - private Database db; - private HashMap message_device_id_map; - - public TagMessageListener(StreamInteractor stream_interactor, Database db, HashMap message_device_id_map) { - this.stream_interactor = stream_interactor; - this.db = db; - this.message_device_id_map = message_device_id_map; - } - - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - int device_id = 0; - if (message_device_id_map.has_key(message)) { - device_id = message_device_id_map[message]; - message_device_id_map.unset(message); - } - - // TODO: Handling of files - - ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); - - if (content_item != null && device_id != 0) { - Jid jid = content_item.jid; - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - jid = message.real_jid; - } - - int identity_id = db.identity.get_id(conversation.account.id); - Database.IdentityMetaTable.TrustLevel trust_level = (Database.IdentityMetaTable.TrustLevel) db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), device_id)[db.identity_meta.trust_level]; - if (trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED || trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) { - stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); - } - - db.content_item_meta.insert() - .value(db.content_item_meta.content_item_id, content_item.id) - .value(db.content_item_meta.identity_id, identity_id) - .value(db.content_item_meta.address_name, jid.bare_jid.to_string()) - .value(db.content_item_meta.device_id, device_id) - .value(db.content_item_meta.trusted_when_received, trust_level != Database.IdentityMetaTable.TrustLevel.UNTRUSTED) - .perform(); - } - return false; - } - } - - private class DecryptMessageListener : MessageListener { - public string[] after_actions_const = new string[]{ }; - public override string action_group { get { return "DECRYPT"; } } - public override string[] after_actions { get { return after_actions_const; } } - - private StreamInteractor stream_interactor; - private Database db; - private HashMap message_device_id_map; - - public DecryptMessageListener(StreamInteractor stream_interactor, Database db, HashMap message_device_id_map) { - this.stream_interactor = stream_interactor; - this.db = db; - this.message_device_id_map = message_device_id_map; - } - - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - Store store = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY).store; - - StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI); - if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; - StanzaNode encrypted = (!)_encrypted; - if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { - message.body = "[This message is OMEMO encrypted]"; // TODO temporary - }; - if (!Plugin.ensure_context()) return false; - MessageFlag flag = new MessageFlag(); - stanza.add_flag(flag); - StanzaNode? _header = encrypted.get_subnode("header"); - if (_header == null) return false; - StanzaNode header = (!)_header; - int sid = header.get_attribute_int("sid"); - if (sid <= 0) return false; - foreach (StanzaNode key_node in header.get_subnodes("key")) { - if (key_node.get_attribute_int("rid") == store.local_registration_id) { - try { - string? payload = encrypted.get_deep_string_content("payload"); - string? iv_node = header.get_deep_string_content("iv"); - string? key_node_content = key_node.get_string_content(); - if (payload == null || iv_node == null || key_node_content == null) continue; - uint8[] key; - uint8[] ciphertext = Base64.decode((!)payload); - uint8[] iv = Base64.decode((!)iv_node); - Gee.List possible_jids = new ArrayList(); - if (conversation.type_ == Conversation.Type.CHAT) { - possible_jids.add(stanza.from); - } else { - Jid? real_jid = message.real_jid; - if (real_jid != null) { - possible_jids.add(real_jid); - } else { - foreach (Row row in db.identity_meta.get_with_device_id(sid)) { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } - } - } - - foreach (Jid possible_jid in possible_jids) { - try { - Address address = new Address(possible_jid.bare_jid.to_string(), header.get_attribute_int("sid")); - if (key_node.get_attribute_bool("prekey")) { - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_pre_key_signal_message(msg); - } else { - SignalMessage msg = Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_signal_message(msg); - } - //address.device_id = 0; // TODO: Hack to have address obj live longer - - if (key.length >= 32) { - int authtaglength = key.length - 16; - uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; - uint8[] new_key = new uint8[16]; - Memory.copy(new_ciphertext, ciphertext, ciphertext.length); - Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); - Memory.copy(new_key, key, 16); - ciphertext = new_ciphertext; - key = new_key; - } - - message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); - message_device_id_map[message] = address.device_id; - message.encryption = Encryption.OMEMO; - flag.decrypted = true; - } catch (Error e) { - continue; - } - - // If we figured out which real jid a message comes from due to - if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { - message.real_jid = possible_jid; - } - break; - } - } catch (Error e) { - warning(@"Signal error while decrypting message: $(e.message)\n"); - } - } - } - return false; - } - - private string arr_to_str(uint8[] arr) { - // null-terminate the array - uint8[] rarr = new uint8[arr.length+1]; - Memory.copy(rarr, arr, arr.length); - return (string)rarr; - } - } -} - -} 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 diff --git a/plugins/omemo/src/util.vala b/plugins/omemo/src/util.vala deleted file mode 100644 index 88d30b3b..00000000 --- a/plugins/omemo/src/util.vala +++ /dev/null @@ -1,60 +0,0 @@ -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