From 9840774a87b9d15523ecc04ee4c157270e9abfe5 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 13 May 2017 17:48:13 +0200 Subject: [PATCH] omemo: store and display identity keys of all devices --- plugins/omemo/CMakeLists.txt | 21 ++- plugins/omemo/data/account_settings_dialog.ui | 124 ++++++++++++++++++ .../omemo/src/account_settings_dialog.vala | 53 ++++++++ .../omemo/src/account_settings_widget.vala | 30 ++--- .../omemo/src/contact_details_provider.vala | 37 ++++++ plugins/omemo/src/database.vala | 52 +++++++- plugins/omemo/src/manager.vala | 42 +++++- plugins/omemo/src/plugin.vala | 3 + plugins/omemo/src/stream_module.vala | 24 +++- plugins/omemo/src/util.vala | 60 +++++++++ xmpp-vala/src/module/xep/0060_pubsub.vala | 5 +- 11 files changed, 424 insertions(+), 27 deletions(-) create mode 100644 plugins/omemo/data/account_settings_dialog.ui create mode 100644 plugins/omemo/src/account_settings_dialog.vala create mode 100644 plugins/omemo/src/contact_details_provider.vala create mode 100644 plugins/omemo/src/util.vala diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index 919c569d..eb64cbd2 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -11,11 +11,27 @@ find_packages(OMEMO_PACKAGES REQUIRED GTK3 ) +set(RESOURCE_LIST + account_settings_dialog.ui +) + +compile_gresources( + OMEMO_GRESOURCES_TARGET + OMEMO_GRESOURCES_XML + TARGET ${CMAKE_CURRENT_BINARY_DIR}/resources/resources.c + TYPE EMBED_C + RESOURCES ${RESOURCE_LIST} + PREFIX /im/dino/omemo + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data +) + vala_precompile(OMEMO_VALA_C SOURCES + src/account_settings_dialog.vala src/account_settings_entry.vala src/account_settings_widget.vala src/bundle.vala + src/contact_details_provider.vala src/database.vala src/encrypt_state.vala src/encryption_list_entry.vala @@ -27,6 +43,7 @@ SOURCES src/session_store.vala src/signed_pre_key_store.vala src/stream_module.vala + src/util.vala CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi @@ -34,10 +51,12 @@ CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/dino.vapi PACKAGES ${OMEMO_PACKAGES} +GRESOURCES + ${OMEMO_GRESOURCES_XML} ) add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\") -add_library(omemo SHARED ${OMEMO_VALA_C}) +add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET}) add_dependencies(omemo ${GETTEXT_PACKAGE}-translations) target_link_libraries(omemo libdino signal-protocol-vala ${OMEMO_PACKAGES}) set_target_properties(omemo PROPERTIES PREFIX "") diff --git a/plugins/omemo/data/account_settings_dialog.ui b/plugins/omemo/data/account_settings_dialog.ui new file mode 100644 index 00000000..31996d05 --- /dev/null +++ b/plugins/omemo/data/account_settings_dialog.ui @@ -0,0 +1,124 @@ + + + + \ No newline at end of file diff --git a/plugins/omemo/src/account_settings_dialog.vala b/plugins/omemo/src/account_settings_dialog.vala new file mode 100644 index 00000000..373d02aa --- /dev/null +++ b/plugins/omemo/src/account_settings_dialog.vala @@ -0,0 +1,53 @@ +using Gtk; +using Qlite; +using Dino.Entities; + +namespace Dino.Plugins.Omemo { + +[GtkTemplate (ui = "/im/dino/omemo/account_settings_dialog.ui")] +public class AccountSettingsDialog : Gtk.Dialog { + + private Plugin plugin; + private Account account; + private string fingerprint; + + [GtkChild] private Label own_fingerprint; + [GtkChild] private ListBox other_list; + + public AccountSettingsDialog(Plugin plugin, Account account) { + Object(use_header_bar : 1); + this.plugin = plugin; + + string own_b64 = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.identity_key_public_base64]; + fingerprint = fingerprint_from_base64(own_b64); + own_fingerprint.set_markup(fingerprint_markup(fingerprint)); + + int own_id = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id]; + + int i = 0; + foreach (Row row in plugin.db.identity_meta.with_address(account.bare_jid.to_string())) { + if (row[plugin.db.identity_meta.device_id] == own_id) continue; + if (i == 0) { + other_list.foreach((widget) => { other_list.remove(widget); }); + } + string? other_b64 = row[plugin.db.identity_meta.identity_key_public_base64]; + Label lbl = new Label(other_b64 != null ? fingerprint_markup(fingerprint_from_base64(other_b64)) : _("Unknown device (0x%xd)").printf(row[plugin.db.identity_meta.device_id])) { use_markup = true, visible = true, margin = 8, selectable=true }; + if (row[plugin.db.identity_meta.now_active] && other_b64 != null) { + other_list.insert(lbl, 0); + } else { + lbl.sensitive = false; + other_list.insert(lbl, i); + } + i++; + } + } + + [GtkCallback] + public void copy_button_clicked() { + Clipboard.get_default(get_display()).set_text(fingerprint, fingerprint.length); + } + + +} + +} \ No newline at end of file diff --git a/plugins/omemo/src/account_settings_widget.vala b/plugins/omemo/src/account_settings_widget.vala index 2842c698..da3f6ca2 100644 --- a/plugins/omemo/src/account_settings_widget.vala +++ b/plugins/omemo/src/account_settings_widget.vala @@ -7,6 +7,7 @@ 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; @@ -18,38 +19,31 @@ public class AccountSettingWidget : Plugins.AccountSettingsWidget, Box { fingerprint.visible = true; pack_start(fingerprint); - Button btn = new Button(); + btn = new Button(); btn.image = new Image.from_icon_name("view-list-symbolic", IconSize.BUTTON); btn.relief = ReliefStyle.NONE; - btn.visible = true; + btn.visible = false; btn.valign = Align.CENTER; - btn.clicked.connect(() => { activated(); }); + btn.clicked.connect(() => { + activated(); + AccountSettingsDialog dialog = new AccountSettingsDialog(plugin, account); + 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; try { 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 { - uint8[] arr = Base64.decode(((!)row)[plugin.db.identity.identity_key_public_base64]); - arr = arr[1:arr.length]; - string res = ""; - foreach (uint8 i in arr) { - string s = i.to_string("%x"); - if (s.length == 1) s = "0" + s; - res = res + s; - if ((res.length % 9) == 8) { - if (res.length == 35) { - res += "\n"; - } else { - res += " "; - } - } - } + 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; } } catch (Qlite.DatabaseError e) { fingerprint.set_markup("%s\n%s".printf(_("Own fingerprint"), _("Database error"))); diff --git a/plugins/omemo/src/contact_details_provider.vala b/plugins/omemo/src/contact_details_provider.vala new file mode 100644 index 00000000..8155424d --- /dev/null +++ b/plugins/omemo/src/contact_details_provider.vala @@ -0,0 +1,37 @@ +using Gtk; +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) { + string res = ""; + int i = 0; + foreach (Row row in plugin.db.identity_meta.with_address(conversation.counterpart.to_string())) { + if (row[plugin.db.identity_meta.identity_key_public_base64] != null) { + if (i != 0) { + res += "\n\n"; + } + res += fingerprint_markup(fingerprint_from_base64(row[plugin.db.identity_meta.identity_key_public_base64])); + i++; + } + } + if (i > 0) { + Label label = new Label(res) { use_markup=true, justify=Justification.RIGHT, selectable=true, visible=true }; + contact_details.add(_("Encryption"), _("OMEMO"), n("%d OMEMO device", "%d OMEMO devices", i).printf(i), label); + } + } + } +} + +} diff --git a/plugins/omemo/src/database.vala b/plugins/omemo/src/database.vala index a4a4842b..aa52daf0 100644 --- a/plugins/omemo/src/database.vala +++ b/plugins/omemo/src/database.vala @@ -6,7 +6,48 @@ using Dino.Entities; namespace Dino.Plugins.Omemo { public class Database : Qlite.Database { - private const int VERSION = 0; + private const int VERSION = 1; + + public class IdentityMetaTable : Table { + 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" }; + 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({address_name, device_id, identity_key_public_base64, trusted_identity, now_active, last_active}); + index("identity_meta_idx", {address_name, device_id}, true); + index("identity_meta_list_idx", {address_name}); + } + + public QueryBuilder with_address(string address_name) throws DatabaseError { + return select().with(this.address_name, "=", address_name); + } + + public void insert_device_list(string address_name, ArrayList devices) throws DatabaseError { + update().with(this.address_name, "=", address_name).set(now_active, false).perform(); + foreach (int32 device_id in devices) { + upsert() + .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(string address_name, int device_id, Bundle bundle) throws DatabaseError { + if (bundle == null || bundle.identity_key == null) return -1; + return upsert() + .value(this.address_name, address_name, true) + .value(this.device_id, device_id, true) + .value(this.identity_key_public_base64, Base64.encode(bundle.identity_key.serialize())) + .perform(); + } + } public class IdentityTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -30,6 +71,7 @@ public class Database : Qlite.Database { 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); } } @@ -42,6 +84,7 @@ public class Database : Qlite.Database { 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); } } @@ -55,8 +98,11 @@ public class Database : Qlite.Database { 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 IdentityMetaTable identity_meta { get; private set; } public IdentityTable identity { get; private set; } public SignedPreKeyTable signed_pre_key { get; private set; } public PreKeyTable pre_key { get; private set; } @@ -64,11 +110,13 @@ public class Database : Qlite.Database { public Database(string fileName) throws DatabaseError { base(fileName, VERSION); + identity_meta = new IdentityMetaTable(this); identity = new IdentityTable(this); signed_pre_key = new SignedPreKeyTable(this); pre_key = new PreKeyTable(this); session = new SessionTable(this); - init({identity, signed_pre_key, pre_key, session}); + init({identity_meta, identity, signed_pre_key, pre_key, session}); + exec("PRAGMA synchronous=0"); } public override void migrate(long oldVersion) { diff --git a/plugins/omemo/src/manager.vala b/plugins/omemo/src/manager.vala index e4f0ddf2..9b6f3681 100644 --- a/plugins/omemo/src/manager.vala +++ b/plugins/omemo/src/manager.vala @@ -83,7 +83,12 @@ public class Manager : StreamInteractionModule, Object { message.marked = Entities.Message.Marked.UNSENT; return; } - StreamModule module = ((!)stream).get_module(StreamModule.IDENTITY); + StreamModule? module_ = ((!)stream).get_module(StreamModule.IDENTITY); + if (module_ == null) { + message.marked = Entities.Message.Marked.UNSENT; + return; + } + StreamModule module = (!)module_; EncryptState enc_state = module.encrypt(message_stanza, conversation.account.bare_jid.to_string()); MessageState state; lock (message_states) { @@ -122,6 +127,7 @@ public class Manager : StreamInteractionModule, Object { private void on_account_added(Account account) { stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store_created.connect((store) => on_store_created(account, store)); stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).device_list_loaded.connect((jid) => on_device_list_loaded(account, jid)); + stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).session_started.connect((jid, device_id) => on_session_started(account, jid, false)); stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).session_start_failed.connect((jid, device_id) => on_session_started(account, jid, true)); } @@ -180,6 +186,40 @@ public class Manager : StreamInteractionModule, Object { if (conv == null) continue; stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); } + + // Update meta database + Core.XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) { + return; + } + StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); + if (module == null) { + return; + } + try { + ArrayList device_list = module.get_device_list(jid); + db.identity_meta.insert_device_list(jid, device_list); + int inc = 0; + foreach (Row row in db.identity_meta.with_address(jid).with_null(db.identity_meta.identity_key_public_base64)) { + module.fetch_bundle(stream, row[db.identity_meta.address_name], row[db.identity_meta.device_id]); + inc++; + } + if (inc > 0) { + if (Plugin.DEBUG) print(@"OMEMO: new bundles $inc/$(device_list.size) for $jid\n"); + } + } catch (DatabaseError e) { + // Ignore + print(@"OMEMO: failed to use database: $(e.message)\n"); + } + } + + public void on_bundle_fetched(Account account, string jid, int32 device_id, Bundle bundle) { + try { + db.identity_meta.insert_device_bundle(jid, device_id, bundle); + } catch (DatabaseError e) { + // Ignore + print(@"OMEMO: failed to use database: $(e.message)\n"); + } } private void on_store_created(Account account, Store store) { diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala index e783b7be..18661403 100644 --- a/plugins/omemo/src/plugin.vala +++ b/plugins/omemo/src/plugin.vala @@ -27,6 +27,7 @@ public class Plugin : RootInterface, Object { public Database db; public EncryptionListEntry list_entry; public AccountSettingsEntry settings_entry; + public ContactDetailsProvider contact_details_provider; public void registered(Dino.Application app) { try { @@ -35,8 +36,10 @@ public class Plugin : RootInterface, Object { this.db = new Database(Path.build_filename(Application.get_storage_dir(), "omemo.db")); this.list_entry = new EncryptionListEntry(this); this.settings_entry = new AccountSettingsEntry(this); + this.contact_details_provider = new ContactDetailsProvider(this); this.app.plugin_registry.register_encryption_list_entry(list_entry); this.app.plugin_registry.register_account_settings_entry(settings_entry); + this.app.plugin_registry.register_contact_details_entry(contact_details_provider); this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { list.add(new StreamModule()); }); diff --git a/plugins/omemo/src/stream_module.vala b/plugins/omemo/src/stream_module.vala index 00cddd0a..46bc0ecf 100644 --- a/plugins/omemo/src/stream_module.vala +++ b/plugins/omemo/src/stream_module.vala @@ -24,6 +24,7 @@ public class StreamModule : XmppStreamModule { public signal void store_created(Store store); public signal void device_list_loaded(string jid); + public signal void bundle_fetched(string jid, int device_id, Bundle bundle); public signal void session_started(string jid, int device_id); public signal void session_start_failed(string jid, int device_id); @@ -183,11 +184,11 @@ public class StreamModule : XmppStreamModule { public void request_user_devicelist(XmppStream stream, string jid) { if (active_devicelist_requests.add(jid)) { if (Plugin.DEBUG) print(@"OMEMO: requesting device list for $jid\n"); - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => on_devicelist(stream, jid, id ?? "", node)); + 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, string jid, string id, StanzaNode? node_) { + public void on_devicelist(XmppStream stream, string jid, string? id, StanzaNode? node_) { StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns(); string? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; if (my_jid == null) return; @@ -219,7 +220,6 @@ public class StreamModule : XmppStreamModule { public void start_sessions_with(XmppStream stream, string bare_jid) { if (!device_lists.has_key(bare_jid)) { - // TODO: manually request a device list return; } Address address = new Address(bare_jid, 0); @@ -247,6 +247,23 @@ public class StreamModule : XmppStreamModule { } } + public void fetch_bundle(XmppStream stream, string bare_jid, int device_id) { + if (active_bundle_requests.add(bare_jid + @":$device_id")) { + if (Plugin.DEBUG) print(@"OMEMO: Asking for bundle from $bare_jid:$device_id\n"); + stream.get_module(Pubsub.Module.IDENTITY).request(stream, bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => { + bundle_fetched(jid, device_id, new Bundle(node)); + }); + } + } + + public ArrayList get_device_list(string jid) { + if (is_known_address(jid)) { + return device_lists[jid]; + } else { + return new ArrayList(); + } + } + public bool is_known_address(string name) { return device_lists.has_key(name); } @@ -276,6 +293,7 @@ public class StreamModule : XmppStreamModule { fail = true; } else { Bundle bundle = new Bundle(node); + bundle_fetched(jid, device_id, bundle); 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; diff --git a/plugins/omemo/src/util.vala b/plugins/omemo/src/util.vala new file mode 100644 index 00000000..88d30b3b --- /dev/null +++ b/plugins/omemo/src/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/xmpp-vala/src/module/xep/0060_pubsub.vala b/xmpp-vala/src/module/xep/0060_pubsub.vala index 65c16c3d..45fcb137 100644 --- a/xmpp-vala/src/module/xep/0060_pubsub.vala +++ b/xmpp-vala/src/module/xep/0060_pubsub.vala @@ -29,11 +29,12 @@ namespace Xmpp.Xep.Pubsub { }); } - public void publish(XmppStream stream, string? jid, string node_id, string node, string item_id, StanzaNode content) { + public void publish(XmppStream stream, string? jid, string node_id, string node, string? item_id, StanzaNode content) { StanzaNode pubsub_node = new StanzaNode.build("pubsub", NS_URI).add_self_xmlns(); StanzaNode publish_node = new StanzaNode.build("publish", NS_URI).put_attribute("node", node_id); pubsub_node.put_node(publish_node); - StanzaNode items_node = new StanzaNode.build("item", NS_URI).put_attribute("id", item_id); + StanzaNode items_node = new StanzaNode.build("item", NS_URI); + if (item_id != null) items_node.put_attribute("id", item_id); items_node.put_node(content); publish_node.put_node(items_node); Iq.Stanza iq = new Iq.Stanza.set(pubsub_node);