Merge PR #413 "Improvements to the OMEMO plugin"

This commit is contained in:
Marvin W 2018-11-10 08:05:14 -06:00
commit dfb75e2cda
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
28 changed files with 1738 additions and 518 deletions

View file

@ -22,6 +22,7 @@ Build
* GPGME (For the OpenPGP plugin) * GPGME (For the OpenPGP plugin)
* libgee-0.8 (≥ 0.10) * libgee-0.8 (≥ 0.10)
* libgcrypt (For the OMEMO plugin) * libgcrypt (For the OMEMO plugin)
* libqrencode3 (For the OMEMO plugin)
* libsoup (For the HTTP files plugin) * libsoup (For the HTTP files plugin)
* SQLite3 * SQLite3

View file

@ -82,6 +82,12 @@ public abstract interface ConversationAdditionPopulator : ConversationItemPopula
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
} }
public abstract interface NotificationPopulator : Object {
public abstract string id { get; }
public abstract void init(Conversation conversation, NotificationCollection summary, WidgetType type);
public abstract void close(Conversation conversation);
}
public abstract class MetaConversationItem : Object { public abstract class MetaConversationItem : Object {
public virtual string populator_id { get; set; } public virtual string populator_id { get; set; }
public virtual Jid? jid { get; set; default=null; } public virtual Jid? jid { get; set; default=null; }
@ -99,9 +105,18 @@ public abstract class MetaConversationItem : Object {
public abstract Object? get_widget(WidgetType type); public abstract Object? get_widget(WidgetType type);
} }
public abstract class MetaConversationNotification : Object {
public abstract Object? get_widget(WidgetType type);
}
public interface ConversationItemCollection : Object { public interface ConversationItemCollection : Object {
public signal void insert_item(MetaConversationItem item); public signal void insert_item(MetaConversationItem item);
public signal void remove_item(MetaConversationItem item); public signal void remove_item(MetaConversationItem item);
} }
public interface NotificationCollection : Object {
public signal void add_meta_notification(MetaConversationNotification item);
public signal void remove_meta_notification(MetaConversationNotification item);
}
} }

View file

@ -8,6 +8,7 @@ public class Registry {
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>(); internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>(); internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>(); internal Gee.List<ConversationAdditionPopulator> conversation_addition_populators = new ArrayList<ConversationAdditionPopulator>();
internal Gee.List<NotificationPopulator> notification_populators = new ArrayList<NotificationPopulator>();
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => { internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
if (a.order < b.order) { if (a.order < b.order) {
return -1; return -1;
@ -78,6 +79,16 @@ public class Registry {
return true; return true;
} }
} }
public bool register_notification_populator(NotificationPopulator populator) {
lock (notification_populators) {
foreach(NotificationPopulator p in notification_populators) {
if (p.id == populator.id) return false;
}
notification_populators.add(populator);
return true;
}
}
} }
} }

View file

@ -11,6 +11,7 @@ public class RosterManager : StreamInteractionModule, Object {
public signal void removed_roster_item(Account account, Jid jid, Roster.Item roster_item); public signal void removed_roster_item(Account account, Jid jid, Roster.Item roster_item);
public signal void updated_roster_item(Account account, Jid jid, Roster.Item roster_item); public signal void updated_roster_item(Account account, Jid jid, Roster.Item roster_item);
public signal void mutual_subscription(Account account, Jid jid);
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Database db; private Database db;
@ -66,6 +67,10 @@ public class RosterManager : StreamInteractionModule, Object {
stream_interactor.module_manager.get_module(account, Roster.Module.IDENTITY).item_updated.connect_after( (stream, roster_item) => { stream_interactor.module_manager.get_module(account, Roster.Module.IDENTITY).item_updated.connect_after( (stream, roster_item) => {
on_roster_item_updated(account, roster_item); on_roster_item_updated(account, roster_item);
}); });
stream_interactor.module_manager.get_module(account, Roster.Module.IDENTITY).mutual_subscription.connect_after( (stream, jid) => {
mutual_subscription(account, jid);
});
} }
private void on_roster_item_updated(Account account, Roster.Item roster_item) { private void on_roster_item_updated(Account account, Roster.Item roster_item) {

View file

@ -7,7 +7,7 @@ using Dino.Entities;
namespace Dino.Ui.ConversationSummary { namespace Dino.Ui.ConversationSummary {
[GtkTemplate (ui = "/im/dino/Dino/conversation_summary/view.ui")] [GtkTemplate (ui = "/im/dino/Dino/conversation_summary/view.ui")]
public class ConversationView : Box, Plugins.ConversationItemCollection { public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins.NotificationCollection {
public Conversation? conversation { get; private set; } public Conversation? conversation { get; private set; }
@ -46,6 +46,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
insert_item.connect(filter_insert_item); insert_item.connect(filter_insert_item);
remove_item.connect(do_remove_item); remove_item.connect(do_remove_item);
add_meta_notification.connect(on_add_meta_notification);
remove_meta_notification.connect(on_remove_meta_notification);
Application app = GLib.Application.get_default() as Application; Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor));
@ -134,6 +136,9 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) {
populator.close(conversation); populator.close(conversation);
} }
foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
populator.close(conversation);
}
} }
// Clear data structures // Clear data structures
@ -159,6 +164,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
foreach (ContentMetaItem item in items) { foreach (ContentMetaItem item in items) {
do_insert_item(item); do_insert_item(item);
} }
Application app = GLib.Application.get_default() as Application;
foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
populator.init(conversation, this, Plugins.WidgetType.GTK);
}
Idle.add(() => { on_value_notify(); return false; }); Idle.add(() => { on_value_notify(); return false; });
} }
@ -203,6 +212,20 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
} }
} }
public void on_add_meta_notification(Plugins.MetaConversationNotification notification) {
Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK);
if (widget != null) {
add_notification(widget);
}
}
public void on_remove_meta_notification(Plugins.MetaConversationNotification notification){
Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK);
if (widget != null) {
remove_notification(widget);
}
}
public void add_notification(Widget widget) { public void add_notification(Widget widget) {
notifications.add(widget); notifications.add(widget);
Timeout.add(20, () => { Timeout.add(20, () => {

View file

@ -12,7 +12,8 @@ find_packages(OMEMO_PACKAGES REQUIRED
) )
set(RESOURCE_LIST set(RESOURCE_LIST
account_settings_dialog.ui contact_details_dialog.ui
manage_key_dialog.ui
) )
compile_gresources( compile_gresources(
@ -27,14 +28,17 @@ compile_gresources(
vala_precompile(OMEMO_VALA_C vala_precompile(OMEMO_VALA_C
SOURCES SOURCES
src/account_settings_dialog.vala
src/account_settings_entry.vala src/account_settings_entry.vala
src/account_settings_widget.vala src/account_settings_widget.vala
src/bundle.vala src/bundle.vala
src/contact_details_provider.vala src/contact_details_provider.vala
src/contact_details_dialog.vala
src/database.vala src/database.vala
src/device_notification_populator.vala
src/own_notifications.vala
src/encrypt_state.vala src/encrypt_state.vala
src/encryption_list_entry.vala src/encryption_list_entry.vala
src/manage_key_dialog.vala
src/manager.vala src/manager.vala
src/message_flag.vala src/message_flag.vala
src/plugin.vala src/plugin.vala
@ -43,12 +47,14 @@ SOURCES
src/session_store.vala src/session_store.vala
src/signed_pre_key_store.vala src/signed_pre_key_store.vala
src/stream_module.vala src/stream_module.vala
src/trust_manager.vala
src/util.vala src/util.vala
CUSTOM_VAPIS CUSTOM_VAPIS
${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi ${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
${CMAKE_BINARY_DIR}/exports/qlite.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi
${CMAKE_BINARY_DIR}/exports/dino.vapi ${CMAKE_BINARY_DIR}/exports/dino.vapi
${CMAKE_CURRENT_SOURCE_DIR}/vapi/qrencode.vapi
PACKAGES PACKAGES
${OMEMO_PACKAGES} ${OMEMO_PACKAGES}
GRESOURCES GRESOURCES
@ -58,7 +64,7 @@ GRESOURCES
add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\") add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\")
add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET}) add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET})
add_dependencies(omemo ${GETTEXT_PACKAGE}-translations) add_dependencies(omemo ${GETTEXT_PACKAGE}-translations)
target_link_libraries(omemo libdino signal-protocol-vala ${OMEMO_PACKAGES}) target_link_libraries(omemo libdino signal-protocol-vala qrencode ${OMEMO_PACKAGES})
set_target_properties(omemo PROPERTIES PREFIX "") set_target_properties(omemo PROPERTIES PREFIX "")
set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/) set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)

View file

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="DinoPluginsOmemoAccountSettingsDialog">
<property name="modal">True</property>
<property name="title" translatable="yes">OMEMO Keys</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin-left">40</property>
<property name="margin-right">40</property>
<child>
<object class="GtkBox">
<property name="margin-top">12</property>
<property name="orientation">horizontal</property>
<property name="visible">True</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Own fingerprint</property>
<property name="xalign">0</property>
<property name="yalign">1</property>
<property name="hexpand">True</property>
<property name="margin-bottom">2</property>
<attributes>
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
</attributes>
</object>
</child>
<child>
<object class="GtkButton" id="copy_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<style>
<class name="flat"/>
</style>
<signal name="clicked" handler="copy_button_clicked"/>
<child>
<object class="GtkImage">
<property name="icon-name">edit-copy-symbolic</property>
<property name="icon-size">1</property>
<property name="visible">True</property>
</object>
</child>
</object>
</child>
<!--<child>
<object class="GtkButton" id="qr_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="sensitive">False</property>
<style>
<class name="flat"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">camera-photo-symbolic</property>
<property name="icon-size">1</property>
<property name="visible">True</property>
</object>
</child>
</object>
</child>-->
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="GtkLabel" id="own_fingerprint">
<property name="visible">True</property>
<property name="margin">8</property>
<property name="label">...</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="margin-top">12</property>
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Other devices</property>
<property name="margin-bottom">2</property>
<attributes>
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="margin-bottom">18</property>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">never</property>
<property name="visible">True</property>
<child>
<object class="GtkListBox" id="other_list">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="margin">8</property>
<property name="label" translatable="yes">- None -</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="DinoPluginsOmemoContactDetailsDialog">
<property name="modal">True</property>
<property name="title">OMEMO Key Management</property>
<property name="resizable">False</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin">12</property>
<property name="spacing">12</property>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
<property name="activatable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<property name="margin-top">14</property>
<property name="margin-bottom">14</property>
<property name="spacing">40</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Automatically accept new keys</property>
<attributes>
<attribute name="scale" value="1.1"/>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="max_width_chars">1</property>
<property name="expand">True</property>
<property name="xalign">0</property>
<property name="wrap">True</property>
<property name="label" translatable="yes">When this contact adds new encryption keys to their account, automatically accept them.</property>
<attributes>
<attribute name="scale" value="0.8"/>
</attributes>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSwitch" id="auto_accept_switch">
<property name="visible">True</property>
<property name="halign">end</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="own_fingerprint_container">
<property name="visible">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Own key</property>
<attributes>
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<child>
<object class="GtkListBoxRow">
<property name="visible">True</property>
<property name="activatable">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<property name="margin-top">14</property>
<property name="spacing">40</property>
<property name="margin-bottom">14</property>
<property name="orientation">horizontal</property>
<property name="hexpand">True</property>
<child>
<object class="GtkLabel" id="own_fingerprint_label">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="justify">right</property>
<property name="hexpand">True</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="hexpand">True</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="show_qrcode_button">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="halign">end</property>
<property name="icon-name">camera-photo-symbolic</property>
<property name="icon-size">1</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="copy_button">
<property name="visible">True</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-size">1</property>
<property name="icon-name">edit-copy-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="new_keys_container">
<property name="visible">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="label" translatable="yes">New keys</property>
<attributes>
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<property name="visible">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkListBox" id="new_keys_listbox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="keys_container">
<property name="visible">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Associated keys</property>
<attributes>
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<property name="visible">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkListBox" id="keys_listbox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
<object class="GtkPopover" id="qrcode_popover">
<property name="visible">False</property>
<property name="relative-to">show_qrcode_button</property>
<property name="position">left</property>
<property name="modal">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin">10</property>
<child>
<object class="GtkImage" id="qrcode_image">
<property name="visible">True</property>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="DinoPluginsOmemoManageKeyDialog">
<property name="modal">True</property>
<property name="resizable">False</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="title">Manage Key</property>
<property name="show_close_button">False</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="sensitive">True</property>
<property name="visible">True</property>
</object>
<packing>
<property name="pack_type">start</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ok_button">
<property name="has_default">True</property>
<property name="can_default">True</property>
<property name="label" translatable="yes">Confirm</property>
<property name="sensitive">False</property>
<property name="visible">True</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="visible">True</property>
<child>
<object class="GtkStack" id="manage_stack">
<property name="visible">True</property>
<property name="transition-type">slide-left-right</property>
<child>
<object class="GtkBox" id="main_screen">
<property name="visible">True</property>
<property name="margin">12</property>
<property name="spacing">12</property>
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel" id="main_desc_label">
<property name="visible">True</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
<property name="max-width-chars">1</property>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<child>
<object class="GtkListBox" id="main_action_list">
<property name="visible">True</property>
<property name="selection-mode">none</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">main</property>
</packing>
</child>
<child>
<object class="GtkBox" id="verify_screen">
<property name="visible">True</property>
<property name="margin">12</property>
<property name="spacing">12</property>
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Compare the fingerprint, character by character, with the one shown on your contacts device.</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
<property name="max-width-chars">45</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="margin-left">12</property>
<property name="margin-right">12</property>
<property name="spacing">12</property>
<property name="hexpand">False</property>
<property name="halign">center</property>
<child>
<object class="GtkLabel" id="verify_label">
<property name="visible">True</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="justify">right</property>
</object>
</child>
<child>
<object class="GtkButton" id="verify_no_button">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Not Matching</property>
</object>
</child>
<child>
<object class="GtkButton" id="verify_yes_button">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Matching</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">verify</property>
</packing>
</child>
<child>
<object class="GtkBox" id="confirm_screen">
<property name="visible">True</property>
<property name="margin">12</property>
<property name="spacing">12</property>
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkImage" id="confirm_image">
<property name="visible">True</property>
</object>
</child>
<child>
<object class="GtkLabel" id="confirm_title_label">
<property name="visible">True</property>
<attributes>
<attribute name="scale" value="1.1"/>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel" id="confirm_desc_label">
<property name="visible">True</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
<attributes>
<attribute name="scale" value="0.8"/>
</attributes>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
<packing>
<property name="name">confirm</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -1,54 +0,0 @@
using Gtk;
using Qlite;
using Dino.Entities;
namespace Dino.Plugins.Omemo {
[GtkTemplate (ui = "/im/dino/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;
this.account = account;
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) => { widget.destroy(); });
}
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%.8x)").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);
}
}
}

View file

@ -27,7 +27,7 @@ public class AccountSettingWidget : Plugins.AccountSettingsWidget, Box {
btn.valign = Align.CENTER; btn.valign = Align.CENTER;
btn.clicked.connect(() => { btn.clicked.connect(() => {
activated(); activated();
AccountSettingsDialog dialog = new AccountSettingsDialog(plugin, account); ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, account, account.bare_jid);
dialog.set_transient_for((Window) get_toplevel()); dialog.set_transient_for((Window) get_toplevel());
dialog.present(); dialog.present();
}); });

View file

@ -0,0 +1,237 @@
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 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 : 1);
this.plugin = plugin;
this.account = account;
this.jid = jid;
(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];
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 pixbuf = new QRcode(@"xmpp:$(account.bare_jid)?omemo-sid-$(sid)=$(fingerprint)", 2).to_pixbuf();
pixbuf = pixbuf.scale_simple(150, 150, InterpType.NEAREST);
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("<span color='#1A63D9'>%s</span>".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("<span color='#D91900'>%s</span>".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("<span color='#1A63D9'>%s</span>".printf(_("Verified")));
lbl.get_style_context().remove_class("dim-label");
break;
}
if (!now_active) {
img.icon_name= "appointment-missed-symbolic";
status_lbl.set_markup("<span color='#8b8e8f'>%s</span>".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);
}
}
}

View file

@ -1,4 +1,5 @@
using Gtk; using Gtk;
using Gee;
using Qlite; using Qlite;
using Dino.Entities; using Dino.Entities;
@ -15,20 +16,30 @@ public class ContactDetailsProvider : Plugins.ContactDetailsProvider, Object {
public void populate(Conversation conversation, Plugins.ContactDetails contact_details, WidgetType type) { public void populate(Conversation conversation, Plugins.ContactDetails contact_details, WidgetType type) {
if (conversation.type_ == Conversation.Type.CHAT && type == WidgetType.GTK) { if (conversation.type_ == Conversation.Type.CHAT && type == WidgetType.GTK) {
string res = "";
int identity_id = plugin.db.identity.get_id(conversation.account.id);
if (identity_id < 0) return;
int i = 0; int i = 0;
foreach (Row row in plugin.db.identity_meta.with_address(conversation.counterpart.to_string())) { 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) { 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++; i++;
} }
} }
if (i > 0) { if (i > 0) {
Label label = new Label(res) { use_markup=true, justify=Justification.RIGHT, selectable=true, visible=true }; Button btn = new Button.from_icon_name("view-list-symbolic") { visible = true, valign = Align.CENTER, relief = ReliefStyle.NONE };
contact_details.add(_("Encryption"), "OMEMO", n("%d OMEMO device", "%d OMEMO devices", i).printf(i), label); 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);
} }
} }
} }

View file

@ -6,31 +6,47 @@ using Dino.Entities;
namespace Dino.Plugins.Omemo { namespace Dino.Plugins.Omemo {
public class Database : Qlite.Database { public class Database : Qlite.Database {
private const int VERSION = 1; private const int VERSION = 2;
public class IdentityMetaTable : Table { 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<int> identity_id = new Column.Integer("identity_id") { not_null = true, min_version = 2, default = "-1" };
public Column<string> address_name = new Column.Text("address_name") { not_null = true }; public Column<string> address_name = new Column.Text("address_name") { not_null = true };
public Column<int> device_id = new Column.Integer("device_id") { not_null = true }; public Column<int> device_id = new Column.Integer("device_id") { not_null = true };
public Column<string?> identity_key_public_base64 = new Column.Text("identity_key_public_base64"); public Column<string?> identity_key_public_base64 = new Column.Text("identity_key_public_base64");
public Column<bool> trusted_identity = new Column.BoolInt("trusted_identity") { default = "0" }; public Column<bool> trusted_identity = new Column.BoolInt("trusted_identity") { default = "0", max_version = 1 };
public Column<int> trust_level = new Column.Integer("trust_level") { default = TrustLevel.UNKNOWN.to_string(), min_version = 2 };
public Column<bool> now_active = new Column.BoolInt("now_active") { default = "1" }; public Column<bool> now_active = new Column.BoolInt("now_active") { default = "1" };
public Column<long> last_active = new Column.Long("last_active"); public Column<long> last_active = new Column.Long("last_active");
internal IdentityMetaTable(Database db) { internal IdentityMetaTable(Database db) {
base(db, "identity_meta"); base(db, "identity_meta");
init({address_name, device_id, identity_key_public_base64, trusted_identity, now_active, last_active}); init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active});
index("identity_meta_idx", {address_name, device_id}, true); index("identity_meta_idx", {identity_id, address_name, device_id}, true);
index("identity_meta_list_idx", {address_name}); index("identity_meta_list_idx", {identity_id, address_name});
} }
public QueryBuilder with_address(string address_name) { public QueryBuilder with_address(int identity_id, string address_name) {
return select().with(this.address_name, "=", address_name); return select().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name);
} }
public void insert_device_list(string address_name, ArrayList<int32> devices) { public void insert_device_list(int32 identity_id, string address_name, ArrayList<int32> devices) {
update().with(this.address_name, "=", address_name).set(now_active, false).perform(); update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform();
foreach (int32 device_id in devices) { foreach (int32 device_id in devices) {
upsert() upsert()
.value(this.identity_id, identity_id, true)
.value(this.address_name, address_name, true) .value(this.address_name, address_name, true)
.value(this.device_id, device_id, true) .value(this.device_id, device_id, true)
.value(this.now_active, true) .value(this.now_active, true)
@ -39,13 +55,61 @@ public class Database : Qlite.Database {
} }
} }
public int64 insert_device_bundle(string address_name, int device_id, Bundle bundle) { 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; if (bundle == null || bundle.identity_key == null) return -1;
return upsert() return upsert()
.value(this.identity_id, identity_id, true)
.value(this.address_name, address_name, true) .value(this.address_name, address_name, true)
.value(this.device_id, device_id, true) .value(this.device_id, device_id, true)
.value(this.identity_key_public_base64, Base64.encode(bundle.identity_key.serialize())) .value(this.identity_key_public_base64, Base64.encode(bundle.identity_key.serialize()))
.perform(); .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 class TrustTable : Table {
public Column<int> identity_id = new Column.Integer("identity_id") { not_null = true };
public Column<string> address_name = new Column.Text("address_name");
public Column<bool> 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;
} }
} }
@ -60,6 +124,13 @@ public class Database : Qlite.Database {
base(db, "identity"); base(db, "identity");
init({id, account_id, device_id, identity_key_private_base64, identity_key_public_base64}); 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 class SignedPreKeyTable : Table {
@ -103,6 +174,7 @@ public class Database : Qlite.Database {
} }
public IdentityMetaTable identity_meta { get; private set; } public IdentityMetaTable identity_meta { get; private set; }
public TrustTable trust { get; private set; }
public IdentityTable identity { get; private set; } public IdentityTable identity { get; private set; }
public SignedPreKeyTable signed_pre_key { get; private set; } public SignedPreKeyTable signed_pre_key { get; private set; }
public PreKeyTable pre_key { get; private set; } public PreKeyTable pre_key { get; private set; }
@ -111,18 +183,29 @@ public class Database : Qlite.Database {
public Database(string fileName) { public Database(string fileName) {
base(fileName, VERSION); base(fileName, VERSION);
identity_meta = new IdentityMetaTable(this); identity_meta = new IdentityMetaTable(this);
trust = new TrustTable(this);
identity = new IdentityTable(this); identity = new IdentityTable(this);
signed_pre_key = new SignedPreKeyTable(this); signed_pre_key = new SignedPreKeyTable(this);
pre_key = new PreKeyTable(this); pre_key = new PreKeyTable(this);
session = new SessionTable(this); session = new SessionTable(this);
init({identity_meta, identity, signed_pre_key, pre_key, session}); init({identity_meta, trust, identity, signed_pre_key, pre_key, session});
try { try {
exec("PRAGMA synchronous=0"); exec("PRAGMA synchronous=0");
} catch (Error e) { } } catch (Error e) { }
} }
public override void migrate(long oldVersion) { public override void migrate(long oldVersion) {
// new table columns are added, outdated columns are still present 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);
}
}
} }
} }

View file

@ -0,0 +1,94 @@
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;
}
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;
stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => {
if (jid.equals(conversation.counterpart) && has_new_devices(conversation.counterpart) && conversation.type_ == Conversation.Type.CHAT) {
display_notification();
}
});
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 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;
}
}
}

View file

@ -7,7 +7,7 @@ public class EncryptState {
public int other_lost { get; internal set; } public int other_lost { get; internal set; }
public int other_unknown { get; internal set; } public int other_unknown { get; internal set; }
public int other_failure { get; internal set; } public int other_failure { get; internal set; }
public bool other_list { get; internal set; } public int other_waiting_lists { get; internal set; }
public int own_devices { get; internal set; } public int own_devices { get; internal set; }
public int own_success { get; internal set; } public int own_success { get; internal set; }
@ -17,7 +17,7 @@ public class EncryptState {
public bool own_list { get; internal set; } public bool own_list { get; internal set; }
public string to_string() { public string to_string() {
return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, list=$other_list), own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; 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))";
} }
} }

View file

@ -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 : 1);
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(@"<b>$(device[db.identity_meta.address_name])</b>"));
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(@"<b>$(device[db.identity_meta.address_name])</b>"));
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 assoicated 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("<span color='#1A63D9'>"+_("accepted")+"</span>")+" "+_("This means it can be used by %s to receive and send messages.").printf(@"<b>$(device[db.identity_meta.address_name])</b>"));
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("<span color='#1A63D9'>"+_("verified")+"</span>")+" "+_("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(@"<b>$(device[db.identity_meta.address_name])</b>"));
main_action_list.add(reject_row);
break;
case Database.IdentityMetaTable.TrustLevel.UNTRUSTED:
main_desc_label.set_markup(_("This key is currently %s.").printf("<span color='#D91900'>"+_("rejected")+"</span>")+" "+_("This means it cannot be used by %s to receive messages, and any messages sent by it will be ignored").printf(@"<b>$(device[db.identity_meta.address_name])</b>"));
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(@"<b>$(device[db.identity_meta.address_name])</b>"));
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(@"<b>$(device[db.identity_meta.address_name])</b>"));
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])));
}
}
}

View file

@ -12,6 +12,7 @@ public class Manager : StreamInteractionModule, Object {
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Database db; private Database db;
private TrustManager trust_manager;
private Map<Entities.Message, MessageState> message_states = new HashMap<Entities.Message, MessageState>(Entities.Message.hash_func, Entities.Message.equals_func); private Map<Entities.Message, MessageState> message_states = new HashMap<Entities.Message, MessageState>(Entities.Message.hash_func, Entities.Message.equals_func);
private ReceivedMessageListener received_message_listener = new ReceivedMessageListener(); private ReceivedMessageListener received_message_listener = new ReceivedMessageListener();
@ -21,7 +22,7 @@ public class Manager : StreamInteractionModule, Object {
public int waiting_other_sessions { get; set; } public int waiting_other_sessions { get; set; }
public int waiting_own_sessions { get; set; } public int waiting_own_sessions { get; set; }
public bool waiting_own_devicelist { get; set; } public bool waiting_own_devicelist { get; set; }
public bool waiting_other_devicelist { get; set; } public int waiting_other_devicelists { get; set; }
public bool force_next_attempt { get; set; } public bool force_next_attempt { get; set; }
public bool will_send_now { get; private set; } public bool will_send_now { get; private set; }
public bool active_send_attempt { get; set; } public bool active_send_attempt { get; set; }
@ -37,12 +38,12 @@ public class Manager : StreamInteractionModule, Object {
this.waiting_other_sessions = new_try.other_unknown; this.waiting_other_sessions = new_try.other_unknown;
this.waiting_own_sessions = new_try.own_unknown; this.waiting_own_sessions = new_try.own_unknown;
this.waiting_own_devicelist = !new_try.own_list; this.waiting_own_devicelist = !new_try.own_list;
this.waiting_other_devicelist = !new_try.other_list; this.waiting_other_devicelists = new_try.other_waiting_lists;
this.active_send_attempt = false; this.active_send_attempt = false;
will_send_now = false; will_send_now = false;
if (new_try.other_failure > 0 || (new_try.other_lost == new_try.other_devices && new_try.other_devices > 0)) { 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; msg.marked = Entities.Message.Marked.WONTSEND;
} else if (new_try.other_unknown > 0 || new_try.own_unknown > 0 || !new_try.other_list || !new_try.own_list || new_try.own_devices == 0) { } 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; msg.marked = Entities.Message.Marked.UNSENT;
} else if (!new_try.encrypted) { } else if (!new_try.encrypted) {
msg.marked = Entities.Message.Marked.WONTSEND; msg.marked = Entities.Message.Marked.WONTSEND;
@ -52,22 +53,24 @@ public class Manager : StreamInteractionModule, Object {
} }
public bool should_retry_now() { public bool should_retry_now() {
return !waiting_own_devicelist && !waiting_other_devicelist && waiting_other_sessions <= 0 && waiting_own_sessions <= 0 && !active_send_attempt; return !waiting_own_devicelist && waiting_other_devicelists <= 0 && waiting_other_sessions <= 0 && waiting_own_sessions <= 0 && !active_send_attempt;
} }
public string to_string() { public string to_string() {
return @"MessageState (waiting=(others=$waiting_other_sessions, own=$waiting_own_sessions, other_list=$waiting_other_devicelist, own_list=$waiting_own_devicelist))"; 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) { private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) {
this.stream_interactor = stream_interactor; this.stream_interactor = stream_interactor;
this.db = db; this.db = db;
this.trust_manager = trust_manager;
stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.stream_negotiated.connect(on_stream_negotiated);
stream_interactor.account_added.connect(on_account_added); stream_interactor.account_added.connect(on_account_added);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); 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 class ReceivedMessageListener : MessageListener { private class ReceivedMessageListener : MessageListener {
@ -85,6 +88,23 @@ public class Manager : StreamInteractionModule, Object {
} }
} }
private Gee.List<Jid> get_occupants(Jid jid, Account account){
Gee.List<Jid> occupants = new ArrayList<Jid>(Jid.equals_bare_func);
if(!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid, account)){
occupants.add(jid);
}
Gee.List<Jid>? 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) { private void on_pre_message_send(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
if (message.encryption == Encryption.OMEMO) { if (message.encryption == Encryption.OMEMO) {
XmppStream? stream = stream_interactor.get_stream(conversation.account); XmppStream? stream = stream_interactor.get_stream(conversation.account);
@ -98,7 +118,22 @@ public class Manager : StreamInteractionModule, Object {
return; return;
} }
StreamModule module = (!)module_; StreamModule module = (!)module_;
EncryptState enc_state = module.encrypt(message_stanza, conversation.account.bare_jid);
//Get a list of everyone for whom the message should be encrypted
Gee.List<Jid> 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>(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; MessageState state;
lock (message_states) { lock (message_states) {
if (message_states.has_key(message)) { if (message_states.has_key(message)) {
@ -113,75 +148,93 @@ public class Manager : StreamInteractionModule, Object {
} }
} }
//Encryption failed - need to fetch more information
if (!state.will_send_now) { if (!state.will_send_now) {
if (message.marked == Entities.Message.Marked.WONTSEND) { if (message.marked == Entities.Message.Marked.WONTSEND) {
if (Plugin.DEBUG) print(@"OMEMO: message was not sent: $state\n"); if (Plugin.DEBUG) print(@"OMEMO: message was not sent: $state\n");
message_states.unset(message);
} else { } else {
if (Plugin.DEBUG) print(@"OMEMO: message will be delayed: $state\n"); if (Plugin.DEBUG) print(@"OMEMO: message will be delayed: $state\n");
if (state.waiting_own_sessions > 0) { if (state.waiting_own_sessions > 0) {
module.start_sessions_with((!)stream, conversation.account.bare_jid); 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) { if (state.waiting_other_sessions > 0 && message.counterpart != null) {
module.start_sessions_with((!)stream, ((!)message.counterpart).bare_jid); 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);
} }
if (state.waiting_other_devicelist && message.counterpart != null) {
module.request_user_devicelist((!)stream, ((!)message.counterpart).bare_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) { 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).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).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)); 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));
} }
private void on_stream_negotiated(Account account, XmppStream stream) { 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); stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist(stream, account.bare_jid);
} }
private void on_session_started(Account account, Jid jid, bool failed) { private void on_device_list_loaded(Account account, Jid jid, ArrayList<int32> device_list) {
if (Plugin.DEBUG) print(@"OMEMO: session start between $(account.bare_jid) and $jid $(failed ? "failed" : "successful")\n"); if (Plugin.DEBUG) print(@"OMEMO: received device list for $(account.bare_jid) from $jid\n");
HashSet<Entities.Message> send_now = new HashSet<Entities.Message>();
lock (message_states) { XmppStream? stream = stream_interactor.get_stream(account);
foreach (Entities.Message msg in message_states.keys) { if (stream == null) {
if (!msg.account.equals(account)) continue; return;
MessageState state = message_states[msg];
if (account.bare_jid.equals(jid)) {
state.waiting_own_sessions--;
} else if (msg.counterpart != null && msg.counterpart.equals_bare(jid)) {
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);
} }
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
if (module == null) {
return;
} }
private void on_device_list_loaded(Account account, Jid jid) { int identity_id = db.identity.get_id(account.id);
if (Plugin.DEBUG) print(@"OMEMO: received device list for $(account.bare_jid) from $jid\n"); 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) {
if (Plugin.DEBUG) print(@"OMEMO: new bundles $inc/$(device_list.size) for $jid\n");
}
//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<Entities.Message> send_now = new HashSet<Entities.Message>(); HashSet<Entities.Message> send_now = new HashSet<Entities.Message>();
lock (message_states) { lock (message_states) {
foreach (Entities.Message msg in message_states.keys) { foreach (Entities.Message msg in message_states.keys) {
if (!msg.account.equals(account)) continue; if (!msg.account.equals(account)) continue;
Gee.List<Jid> occupants = get_occupants(msg.counterpart.bare_jid, account);
MessageState state = message_states[msg]; MessageState state = message_states[msg];
if (account.bare_jid.equals(jid)) { if (account.bare_jid.equals(jid)) {
state.waiting_own_devicelist = false; state.waiting_own_devicelist = false;
} else if (msg.counterpart != null && msg.counterpart.equals_bare(jid)) { } else if (msg.counterpart != null && occupants.contains(jid)) {
state.waiting_other_devicelist = false; state.waiting_other_devicelists--;
} }
if (state.should_retry_now()) { if (state.should_retry_now()) {
send_now.add(msg); send_now.add(msg);
@ -196,37 +249,84 @@ public class Manager : StreamInteractionModule, Object {
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true);
} }
// Update meta database
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) {
return;
}
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
if (module == null) {
return;
}
ArrayList<int32> device_list = module.get_device_list(jid);
db.identity_meta.insert_device_list(jid.bare_jid.to_string(), device_list);
int inc = 0;
foreach (Row row in db.identity_meta.with_address(jid.bare_jid.to_string()).with_null(db.identity_meta.identity_key_public_base64)) {
module.fetch_bundle(stream, Jid.parse(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");
}
} }
public void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) { public void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) {
db.identity_meta.insert_device_bundle(jid.bare_jid.to_string(), device_id, 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<Entities.Message> send_now = new HashSet<Entities.Message>();
lock (message_states) {
foreach (Entities.Message msg in message_states.keys) {
bool session_created = true;
if (!msg.account.equals(account)) continue;
Gee.List<Jid> 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 void on_store_created(Account account, Store store) { private void on_store_created(Account account, Store store) {
Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner; Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner;
int identity_id = -1; int identity_id = -1;
bool publish_identity = false;
if (row == null) { if (row == null) {
// OMEMO not yet initialized, starting with empty base // OMEMO not yet initialized, starting with empty base
publish_identity = true;
try { try {
store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX); store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX);
@ -257,19 +357,40 @@ public class Manager : StreamInteractionModule, Object {
} else { } else {
print(@"OMEMO: store for $(account.bare_jid) is not persisted!"); print(@"OMEMO: store for $(account.bare_jid) is not persisted!");
} }
// 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) { public bool can_encrypt(Entities.Conversation conversation) {
XmppStream? stream = stream_interactor.get_stream(conversation.account); XmppStream? stream = stream_interactor.get_stream(conversation.account);
if (stream == null) return false; if (stream == null) return false;
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(conversation.counterpart, conversation.account)){
if (module == null) return false; Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY);
return ((!)module).is_known_address(conversation.counterpart.bare_jid); 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) { public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) {
Manager m = new Manager(stream_interactor, db); Manager m = new Manager(stream_interactor, db, trust_manager);
stream_interactor.add_module(m); stream_interactor.add_module(m);
} }
} }

View file

@ -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);
}
}
}

View file

@ -28,6 +28,9 @@ public class Plugin : RootInterface, Object {
public EncryptionListEntry list_entry; public EncryptionListEntry list_entry;
public AccountSettingsEntry settings_entry; public AccountSettingsEntry settings_entry;
public ContactDetailsProvider contact_details_provider; public ContactDetailsProvider contact_details_provider;
public DeviceNotificationPopulator device_notification_populator;
public OwnNotifications own_notifications;
public TrustManager trust_manager;
public void registered(Dino.Application app) { public void registered(Dino.Application app) {
ensure_context(); ensure_context();
@ -36,13 +39,29 @@ public class Plugin : RootInterface, Object {
this.list_entry = new EncryptionListEntry(this); this.list_entry = new EncryptionListEntry(this);
this.settings_entry = new AccountSettingsEntry(this); this.settings_entry = new AccountSettingsEntry(this);
this.contact_details_provider = new ContactDetailsProvider(this); this.contact_details_provider = new ContactDetailsProvider(this);
this.device_notification_populator = new DeviceNotificationPopulator(this, this.app.stream_interactor);
this.trust_manager = new TrustManager(this.app.stream_interactor, this.db);
this.app.plugin_registry.register_encryption_list_entry(list_entry); 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_account_settings_entry(settings_entry);
this.app.plugin_registry.register_contact_details_entry(contact_details_provider); this.app.plugin_registry.register_contact_details_entry(contact_details_provider);
this.app.plugin_registry.register_notification_populator(device_notification_populator);
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
list.add(new StreamModule()); list.add(new StreamModule());
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
}); });
Manager.start(this.app.stream_interactor, db); Manager.start(this.app.stream_interactor, db, trust_manager);
SimpleAction own_keys_action = new SimpleAction("own-keys", VariantType.INT32);
own_keys_action.activate.connect((variant) => {
foreach(Dino.Entities.Account account in this.app.stream_interactor.get_accounts()) {
if(account.id == variant.get_int32()) {
ContactDetailsDialog dialog = new ContactDetailsDialog(this, account, account.bare_jid);
dialog.set_transient_for((this.app as Gtk.Application).get_active_window());
dialog.present();
}
}
});
this.app.add_action(own_keys_action);
string locales_dir; string locales_dir;
if (app.search_path_generator != null) { if (app.search_path_generator != null) {

View file

@ -16,115 +16,24 @@ private const int NUM_KEYS_TO_PUBLISH = 100;
public class StreamModule : XmppStreamModule { public class StreamModule : XmppStreamModule {
public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "omemo_module"); public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "omemo_module");
private Store store; public Store store { public get; private set; }
private ConcurrentSet<string> active_bundle_requests = new ConcurrentSet<string>(); private ConcurrentSet<string> active_bundle_requests = new ConcurrentSet<string>();
private ConcurrentSet<Jid> active_devicelist_requests = new ConcurrentSet<Jid>(); private ConcurrentSet<Jid> active_devicelist_requests = new ConcurrentSet<Jid>();
private Map<Jid, ArrayList<int32>> device_lists = new HashMap<Jid, ArrayList<int32>>(Jid.hash_bare_func, Jid.equals_bare_func);
private Map<Jid, ArrayList<int32>> ignored_devices = new HashMap<Jid, ArrayList<int32>>(Jid.hash_bare_func, Jid.equals_bare_func); private Map<Jid, ArrayList<int32>> ignored_devices = new HashMap<Jid, ArrayList<int32>>(Jid.hash_bare_func, Jid.equals_bare_func);
private ReceivedPipelineListener received_pipeline_listener;
public signal void store_created(Store store); public signal void store_created(Store store);
public signal void device_list_loaded(Jid jid); public signal void device_list_loaded(Jid jid, ArrayList<int32> devices);
public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle);
public signal void session_started(Jid jid, int device_id);
public signal void session_start_failed(Jid jid, int device_id);
public EncryptState encrypt(MessageStanza message, Jid self_jid) {
EncryptState status = new EncryptState();
if (!Plugin.ensure_context()) return status;
if (message.to == null) return status;
try {
if (!device_lists.has_key(self_jid)) return status;
status.own_list = true;
status.own_devices = device_lists.get(self_jid).size;
if (!device_lists.has_key(message.to)) return status;
status.other_list = true;
status.other_devices = device_lists.get(message.to).size;
if (status.own_devices == 0 || status.other_devices == 0) return status;
uint8[] key = new uint8[16];
Plugin.get_context().randomize(key);
uint8[] iv = new uint8[16];
Plugin.get_context().randomize(iv);
uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
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", 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))));
Address address = new Address(message.to.bare_jid.to_string(), 0);
foreach(int32 device_id in device_lists[message.to]) {
if (is_ignored_device(message.to, device_id)) {
status.other_lost++;
continue;
}
try {
address.device_id = (int) device_id;
StanzaNode key_node = create_encrypted_key(key, address);
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 device_lists[self_jid]) {
if (is_ignored_device(self_jid, device_id)) {
status.own_lost++;
continue;
}
if (device_id != store.local_registration_id) {
address.device_id = (int) device_id;
try {
StanzaNode key_node = create_encrypted_key(key, address);
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) {
if (Plugin.DEBUG) print(@"OMEMO: Signal error while encrypting message: $(e.message)\n");
}
return status;
}
private StanzaNode create_encrypted_key(uint8[] key, Address address) 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 override void attach(XmppStream stream) { public override void attach(XmppStream stream) {
if (!Plugin.ensure_context()) return; if (!Plugin.ensure_context()) return;
this.store = Plugin.get_context().create_store(); this.store = Plugin.get_context().create_store();
store_created(store); store_created(store);
received_pipeline_listener = new ReceivedPipelineListener(store);
stream.get_module(MessageModule.IDENTITY).received_pipeline.connect(received_pipeline_listener);
stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => on_devicelist(stream, jid, id, node)); 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 override void detach(XmppStream stream) {
stream.get_module(MessageModule.IDENTITY).received_pipeline.disconnect(received_pipeline_listener);
} }
public void request_user_devicelist(XmppStream stream, Jid jid) { public void request_user_devicelist(XmppStream stream, Jid jid) {
@ -150,31 +59,26 @@ public class StreamModule : XmppStreamModule {
if (Plugin.DEBUG) print(@"OMEMO: Not on device list, adding id\n"); if (Plugin.DEBUG) print(@"OMEMO: Not on device list, adding id\n");
node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string())); 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); stream.get_module(Pubsub.Module.IDENTITY).publish(stream, jid, NODE_DEVICELIST, NODE_DEVICELIST, id, node);
} else { }
publish_bundles_if_needed(stream, jid); publish_bundles_if_needed(stream, jid);
} }
}
lock(device_lists) { ArrayList<int32> device_list = new ArrayList<int32>();
device_lists[jid] = new ArrayList<int32>();
foreach (StanzaNode device_node in node.get_subnodes("device")) { foreach (StanzaNode device_node in node.get_subnodes("device")) {
device_lists[jid].add(device_node.get_attribute_int("id")); device_list.add(device_node.get_attribute_int("id"));
}
} }
active_devicelist_requests.remove(jid); active_devicelist_requests.remove(jid);
device_list_loaded(jid); device_list_loaded(jid, device_list);
} }
public void start_sessions_with(XmppStream stream, Jid jid) { public void fetch_bundles(XmppStream stream, Jid jid, Gee.List<int32> devices) {
if (!device_lists.has_key(jid)) {
return;
}
Address address = new Address(jid.bare_jid.to_string(), 0); Address address = new Address(jid.bare_jid.to_string(), 0);
foreach(int32 device_id in device_lists[jid]) { foreach(int32 device_id in devices) {
if (!is_ignored_device(jid, device_id)) { if (!is_ignored_device(jid, device_id)) {
address.device_id = device_id; address.device_id = device_id;
try { try {
if (!store.contains_session(address)) { if (!store.contains_session(address)) {
start_session_with(stream, jid, device_id); fetch_bundle(stream, jid, device_id);
} }
} catch (Error e) { } catch (Error e) {
// Ignore // Ignore
@ -184,7 +88,7 @@ public class StreamModule : XmppStreamModule {
address.device_id = 0; address.device_id = 0;
} }
public void start_session_with(XmppStream stream, Jid jid, int device_id) { public void fetch_bundle(XmppStream stream, Jid jid, int device_id) {
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) { if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) {
if (Plugin.DEBUG) print(@"OMEMO: Asking for bundle from $(jid.bare_jid.to_string()):$device_id\n"); if (Plugin.DEBUG) print(@"OMEMO: Asking for bundle from $(jid.bare_jid.to_string()):$device_id\n");
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => { stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => {
@ -193,28 +97,6 @@ public class StreamModule : XmppStreamModule {
} }
} }
public void fetch_bundle(XmppStream stream, Jid jid, int device_id) {
if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) {
if (Plugin.DEBUG) print(@"OMEMO: Asking for bundle from $(jid.bare_jid.to_string()):$device_id\n");
stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => {
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id");
bundle_fetched(jid, device_id, new Bundle(node));
});
}
}
public ArrayList<int32> get_device_list(Jid jid) {
if (is_known_address(jid)) {
return device_lists[jid];
} else {
return new ArrayList<int32>();
}
}
public bool is_known_address(Jid jid) {
return device_lists.has_key(jid);
}
public void ignore_device(Jid jid, int32 device_id) { public void ignore_device(Jid jid, int32 device_id) {
if (device_id <= 0) return; if (device_id <= 0) return;
lock (ignored_devices) { lock (ignored_devices) {
@ -223,7 +105,6 @@ public class StreamModule : XmppStreamModule {
} }
ignored_devices[jid].add(device_id); ignored_devices[jid].add(device_id);
} }
session_start_failed(jid, device_id);
} }
public bool is_ignored_device(Jid jid, int32 device_id) { public bool is_ignored_device(Jid jid, int32 device_id) {
@ -234,13 +115,18 @@ public class StreamModule : XmppStreamModule {
} }
private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node) { private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node) {
bool fail = false;
if (node == null) { if (node == null) {
// Device not registered, shouldn't exist // Device not registered, shouldn't exist
fail = true; stream.get_module(IDENTITY).ignore_device(jid, device_id);
} else { } else {
Bundle bundle = new Bundle(node); Bundle bundle = new Bundle(node);
bundle_fetched(jid, device_id, bundle); 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; int32 signed_pre_key_id = bundle.signed_pre_key_id;
ECPublicKey? signed_pre_key = bundle.signed_pre_key; ECPublicKey? signed_pre_key = bundle.signed_pre_key;
uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature; uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
@ -259,22 +145,20 @@ public class StreamModule : XmppStreamModule {
Address address = new Address(jid.bare_jid.to_string(), device_id); Address address = new Address(jid.bare_jid.to_string(), device_id);
try { try {
if (store.contains_session(address)) { if (store.contains_session(address)) {
return; return false;
} }
SessionBuilder builder = store.create_session_builder(address); 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)); 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));
stream.get_module(IDENTITY).session_started(jid, device_id);
} catch (Error e) { } catch (Error e) {
fail = true; fail = true;
} }
address.device_id = 0; // TODO: Hack to have address obj live longer address.device_id = 0; // TODO: Hack to have address obj live longer
} }
} }
}
if (fail) { if (fail) {
stream.get_module(IDENTITY).ignore_device(jid, device_id); stream.get_module(IDENTITY).ignore_device(jid, device_id);
} }
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id"); return true;
} }
public void publish_bundles_if_needed(XmppStream stream, Jid jid) { public void publish_bundles_if_needed(XmppStream stream, Jid jid) {
@ -385,80 +269,4 @@ public class StreamModule : XmppStreamModule {
} }
} }
public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
private const string[] after_actions_const = {"EXTRACT_MESSAGE_2"};
public override string action_group { get { return "ENCRYPT_BODY"; } }
public override string[] after_actions { get { return after_actions_const; } }
private Store store;
public ReceivedPipelineListener(Store store) {
this.store = store;
}
public override async bool run(XmppStream stream, MessageStanza message) {
StanzaNode? _encrypted = message.stanza.get_subnode("encrypted", NS_URI);
if (_encrypted == null || MessageFlag.get_flag(message) != null || message.from == null) return false;
StanzaNode encrypted = (!)_encrypted;
if (!Plugin.ensure_context()) return false;
MessageFlag flag = new MessageFlag();
message.add_flag(flag);
StanzaNode? _header = encrypted.get_subnode("header");
if (_header == null) return false;
StanzaNode header = (!)_header;
if (header.get_attribute_int("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);
Address address = new Address(message.from.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));
flag.decrypted = true;
} catch (Error e) {
if (Plugin.DEBUG) print(@"OMEMO: 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;
}
}
} }

View file

@ -0,0 +1,253 @@
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 ReceivedMessageListener received_message_listener;
public TrustManager(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
received_message_listener = new ReceivedMessageListener(stream_interactor, db);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_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();
}
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<Jid> 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[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
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(key, 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(key, 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) {
if (Plugin.DEBUG) print(@"OMEMO: 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<int32> get_trusted_devices(Account account, Jid jid) {
Gee.List<int32> devices = new ArrayList<int32>();
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 ReceivedMessageListener : 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;
public ReceivedMessageListener(StreamInteractor stream_interactor, Database db) {
this.stream_interactor = stream_interactor;
this.db = db;
}
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 (!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;
if (header.get_attribute_int("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);
Jid jid = stanza.from;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(jid, conversation.account);
}
Address address = new Address(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));
flag.decrypted = true;
int identity_id = db.identity.get_id(conversation.account.id);
if (identity_id < 0) return false;
Database.IdentityMetaTable.TrustLevel trust_level = (Database.IdentityMetaTable.TrustLevel) db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), header.get_attribute_int("sid"))[db.identity_meta.trust_level];
if (trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED) {
message.body = _("OMEMO message from a rejected device");
message.marked = Message.Marked.WONTSEND;
}
if (trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) {
message.body = _("OMEMO message from an unknown device: ")+message.body;
message.marked = Message.Marked.WONTSEND;
}
} catch (Error e) {
if (Plugin.DEBUG) print(@"OMEMO: 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;
}
}
}
}

View file

@ -0,0 +1,50 @@
using Gdk;
[CCode (cheader_filename = "qrencode.h")]
namespace Qrencode {
[CCode (cname = "QRecLevel", cprefix = "QR_ECLEVEL_")]
public enum ECLevel {
L,
M,
Q,
H
}
[CCode (cname = "QRencodeMode", cprefix = "QR_MODE_")]
public enum EncodeMode {
NUL,
NUM,
AN,
[CCode (cname = "QR_MODE_8")]
EIGHT_BIT,
KANJI,
STRUCTURE,
ECI,
FNC1FIRST,
FNC1SECOND
}
[CCode (cname = "QRcode", free_function = "QRcode_free", has_type_id = false)]
[Compact]
public class QRcode {
private int version;
private int width;
[CCode (array_length = false)]
private uint8[] data;
[CCode (cname = "QRcode_encodeString")]
public QRcode (string str, int version = 0, ECLevel level = ECLevel.L, EncodeMode hint = EncodeMode.EIGHT_BIT, bool casesensitive = true);
public Pixbuf to_pixbuf() {
uint8[] bitmap = new uint8[3*width*width];
for (int i = 0; i < width*width; i++) {
uint8 color = (data[i] & 1) == 1 ? 0 : 255;
bitmap[i*3] = color;
bitmap[i*3+1] = color;
bitmap[i*3+2] = color;
}
return new Pixbuf.from_data(bitmap, Colorspace.RGB, false, 8, width, width, width*3);
}
}
}

View file

@ -375,6 +375,10 @@ public class Store : Object {
return throw_by_code(Protocol.Session.contains_session(native_context, other)) == 1; return throw_by_code(Protocol.Session.contains_session(native_context, other)) == 1;
} }
public void delete_session(Address address) throws Error {
throw_by_code(Protocol.Session.delete_session(native_context, address));
}
public SessionRecord load_session(Address other) throws Error { public SessionRecord load_session(Address other) throws Error {
SessionRecord record; SessionRecord record;
throw_by_code(Protocol.Session.load_session(native_context, out record, other)); throw_by_code(Protocol.Session.load_session(native_context, out record, other));

View file

@ -48,7 +48,7 @@ public class Table {
try { try {
db.exec(@"INSERT INTO _fts_$name(_fts_$name) VALUES('rebuild');"); db.exec(@"INSERT INTO _fts_$name(_fts_$name) VALUES('rebuild');");
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Rebuilding FTS index"); error(@"Qlite Error: Rebuilding FTS index: $(e.message)");
} }
} }
@ -141,23 +141,25 @@ public class Table {
public void create_table_at_version(long version) { public void create_table_at_version(long version) {
ensure_init(); ensure_init();
string sql = @"CREATE TABLE IF NOT EXISTS $name ("; string sql = @"CREATE TABLE IF NOT EXISTS $name (";
bool first = true;
for (int i = 0; i < columns.length; i++) { for (int i = 0; i < columns.length; i++) {
Column c = columns[i]; Column c = columns[i];
if (c.min_version <= version && c.max_version >= version) { if (c.min_version <= version && c.max_version >= version) {
sql += @"$(i > 0 ? "," : "") $(c.to_column_definition())"; sql += @"$(!first ? "," : "") $(c.to_column_definition())";
first = false;
} }
} }
sql += @"$constraints)"; sql += @"$constraints)";
try { try {
db.exec(sql); db.exec(sql);
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Create table at version"); error(@"Qlite Error: Create table at version: $(e.message)");
} }
foreach (string stmt in create_statements) { foreach (string stmt in create_statements) {
try { try {
db.exec(stmt); db.exec(stmt);
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Create table at version"); error(@"Qlite Error: Create table at version: $(e.message)");
} }
} }
} }
@ -169,7 +171,7 @@ public class Table {
try { try {
db.exec(@"ALTER TABLE $name ADD COLUMN $(c.to_column_definition())"); db.exec(@"ALTER TABLE $name ADD COLUMN $(c.to_column_definition())");
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Add columns for version"); error(@"Qlite Error: Add columns for version: $(e.message)");
} }
} }
} }
@ -197,7 +199,7 @@ public class Table {
db.exec(@"INSERT INTO $name ($column_list) SELECT $column_list FROM _$(name)_$old_version"); db.exec(@"INSERT INTO $name ($column_list) SELECT $column_list FROM _$(name)_$old_version");
db.exec(@"DROP TABLE _$(name)_$old_version"); db.exec(@"DROP TABLE _$(name)_$old_version");
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Delete volumns for version change"); error(@"Qlite Error: Delete columns for version change: $(e.message)");
} }
} }
} }
@ -207,7 +209,7 @@ public class Table {
try { try {
db.exec(stmt); db.exec(stmt);
} catch (Error e) { } catch (Error e) {
error("Qlite Error: Post"); error(@"Qlite Error: Post: $(e.message)");
} }
} }
} }

View file

@ -1,3 +1,5 @@
using Gee;
namespace Xmpp.Presence { namespace Xmpp.Presence {
private const string NS_URI = "jabber:client"; private const string NS_URI = "jabber:client";
@ -87,6 +89,8 @@ namespace Xmpp.Presence {
stream.get_flag(Flag.IDENTITY).remove_presence(presence.from); stream.get_flag(Flag.IDENTITY).remove_presence(presence.from);
received_unsubscription(stream, presence.from); received_unsubscription(stream, presence.from);
break; break;
case Presence.Stanza.TYPE_UNSUBSCRIBED:
break;
} }
} }

View file

@ -11,6 +11,7 @@ public class Module : XmppStreamModule, Iq.Handler {
public signal void pre_get_roster(XmppStream stream, Iq.Stanza iq); public signal void pre_get_roster(XmppStream stream, Iq.Stanza iq);
public signal void item_removed(XmppStream stream, Item item, Iq.Stanza iq); public signal void item_removed(XmppStream stream, Item item, Iq.Stanza iq);
public signal void item_updated(XmppStream stream, Item item, Iq.Stanza iq); public signal void item_updated(XmppStream stream, Item item, Iq.Stanza iq);
public signal void mutual_subscription(XmppStream stream, Jid jid);
public bool interested_resource = true; public bool interested_resource = true;
@ -55,8 +56,12 @@ public class Module : XmppStreamModule, Iq.Handler {
item_removed(stream, item, iq); item_removed(stream, item, iq);
break; break;
default: default:
bool is_new = false;
Item old = flag.get_item(item.jid);
is_new = item.subscription == Item.SUBSCRIPTION_BOTH && (old == null || old.subscription == Item.SUBSCRIPTION_BOTH);
flag.roster_items[item.jid] = item; flag.roster_items[item.jid] = item;
item_updated(stream, item, iq); item_updated(stream, item, iq);
if(is_new) mutual_subscription(stream, item.jid);
break; break;
} }
} }