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 @@
+
+
+
+ True
+ OMEMO Keys
+
+
+
+
+
\ 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);