Merge PR #413 "Improvements to the OMEMO plugin"
This commit is contained in:
commit
dfb75e2cda
|
@ -22,6 +22,7 @@ Build
|
|||
* GPGME (For the OpenPGP plugin)
|
||||
* libgee-0.8 (≥ 0.10)
|
||||
* libgcrypt (For the OMEMO plugin)
|
||||
* libqrencode3 (For the OMEMO plugin)
|
||||
* libsoup (For the HTTP files plugin)
|
||||
* SQLite3
|
||||
|
||||
|
|
|
@ -82,6 +82,12 @@ public abstract interface ConversationAdditionPopulator : ConversationItemPopula
|
|||
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 virtual string populator_id { get; set; }
|
||||
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 class MetaConversationNotification : Object {
|
||||
public abstract Object? get_widget(WidgetType type);
|
||||
}
|
||||
|
||||
public interface ConversationItemCollection : Object {
|
||||
public signal void insert_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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ public class Registry {
|
|||
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
|
||||
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
|
||||
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) => {
|
||||
if (a.order < b.order) {
|
||||
return -1;
|
||||
|
@ -78,6 +79,16 @@ public class Registry {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 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 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) => {
|
||||
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) {
|
||||
|
|
|
@ -7,7 +7,7 @@ using Dino.Entities;
|
|||
namespace Dino.Ui.ConversationSummary {
|
||||
|
||||
[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; }
|
||||
|
||||
|
@ -46,6 +46,8 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
|
||||
insert_item.connect(filter_insert_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;
|
||||
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) {
|
||||
populator.close(conversation);
|
||||
}
|
||||
foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) {
|
||||
populator.close(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear data structures
|
||||
|
@ -159,6 +164,10 @@ public class ConversationView : Box, Plugins.ConversationItemCollection {
|
|||
foreach (ContentMetaItem item in items) {
|
||||
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; });
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
notifications.add(widget);
|
||||
Timeout.add(20, () => {
|
||||
|
|
|
@ -12,7 +12,8 @@ find_packages(OMEMO_PACKAGES REQUIRED
|
|||
)
|
||||
|
||||
set(RESOURCE_LIST
|
||||
account_settings_dialog.ui
|
||||
contact_details_dialog.ui
|
||||
manage_key_dialog.ui
|
||||
)
|
||||
|
||||
compile_gresources(
|
||||
|
@ -27,14 +28,17 @@ compile_gresources(
|
|||
|
||||
vala_precompile(OMEMO_VALA_C
|
||||
SOURCES
|
||||
src/account_settings_dialog.vala
|
||||
src/account_settings_entry.vala
|
||||
src/account_settings_widget.vala
|
||||
src/bundle.vala
|
||||
src/contact_details_provider.vala
|
||||
src/contact_details_dialog.vala
|
||||
src/database.vala
|
||||
src/device_notification_populator.vala
|
||||
src/own_notifications.vala
|
||||
src/encrypt_state.vala
|
||||
src/encryption_list_entry.vala
|
||||
src/manage_key_dialog.vala
|
||||
src/manager.vala
|
||||
src/message_flag.vala
|
||||
src/plugin.vala
|
||||
|
@ -43,12 +47,14 @@ SOURCES
|
|||
src/session_store.vala
|
||||
src/signed_pre_key_store.vala
|
||||
src/stream_module.vala
|
||||
src/trust_manager.vala
|
||||
src/util.vala
|
||||
CUSTOM_VAPIS
|
||||
${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi
|
||||
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
||||
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
||||
${CMAKE_BINARY_DIR}/exports/dino.vapi
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vapi/qrencode.vapi
|
||||
PACKAGES
|
||||
${OMEMO_PACKAGES}
|
||||
GRESOURCES
|
||||
|
@ -58,7 +64,7 @@ GRESOURCES
|
|||
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_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 LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
||||
|
||||
|
|
|
@ -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>
|
264
plugins/omemo/data/contact_details_dialog.ui
Normal file
264
plugins/omemo/data/contact_details_dialog.ui
Normal 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>
|
174
plugins/omemo/data/manage_key_dialog.ui
Normal file
174
plugins/omemo/data/manage_key_dialog.ui
Normal 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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -27,7 +27,7 @@ public class AccountSettingWidget : Plugins.AccountSettingsWidget, Box {
|
|||
btn.valign = Align.CENTER;
|
||||
btn.clicked.connect(() => {
|
||||
activated();
|
||||
AccountSettingsDialog dialog = new AccountSettingsDialog(plugin, account);
|
||||
ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, account, account.bare_jid);
|
||||
dialog.set_transient_for((Window) get_toplevel());
|
||||
dialog.present();
|
||||
});
|
||||
|
|
237
plugins/omemo/src/contact_details_dialog.vala
Normal file
237
plugins/omemo/src/contact_details_dialog.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using Gtk;
|
||||
using Gee;
|
||||
using Qlite;
|
||||
using Dino.Entities;
|
||||
|
||||
|
@ -15,20 +16,30 @@ public class ContactDetailsProvider : Plugins.ContactDetailsProvider, Object {
|
|||
|
||||
public void populate(Conversation conversation, Plugins.ContactDetails contact_details, WidgetType type) {
|
||||
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;
|
||||
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 (i != 0) {
|
||||
res += "\n\n";
|
||||
}
|
||||
res += fingerprint_markup(fingerprint_from_base64(row[plugin.db.identity_meta.identity_key_public_base64]));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
Label label = new Label(res) { use_markup=true, justify=Justification.RIGHT, selectable=true, visible=true };
|
||||
contact_details.add(_("Encryption"), "OMEMO", n("%d OMEMO device", "%d OMEMO devices", i).printf(i), label);
|
||||
Button btn = new Button.from_icon_name("view-list-symbolic") { visible = true, valign = Align.CENTER, relief = ReliefStyle.NONE };
|
||||
btn.clicked.connect(() => {
|
||||
btn.activate();
|
||||
ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, conversation.counterpart);
|
||||
dialog.set_transient_for((Window) btn.get_toplevel());
|
||||
dialog.response.connect((response_type) => {
|
||||
plugin.device_notification_populator.should_hide();
|
||||
});
|
||||
dialog.present();
|
||||
});
|
||||
|
||||
contact_details.add(_("Encryption"), "OMEMO", n("%d OMEMO device", "%d OMEMO devices", i).printf(i), btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,31 +6,47 @@ using Dino.Entities;
|
|||
namespace Dino.Plugins.Omemo {
|
||||
|
||||
public class Database : Qlite.Database {
|
||||
private const int VERSION = 1;
|
||||
private const int VERSION = 2;
|
||||
|
||||
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<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<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<long> last_active = new Column.Long("last_active");
|
||||
|
||||
internal IdentityMetaTable(Database db) {
|
||||
base(db, "identity_meta");
|
||||
init({address_name, device_id, identity_key_public_base64, trusted_identity, now_active, last_active});
|
||||
index("identity_meta_idx", {address_name, device_id}, true);
|
||||
index("identity_meta_list_idx", {address_name});
|
||||
init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active});
|
||||
index("identity_meta_idx", {identity_id, address_name, device_id}, true);
|
||||
index("identity_meta_list_idx", {identity_id, address_name});
|
||||
}
|
||||
|
||||
public QueryBuilder with_address(string address_name) {
|
||||
return select().with(this.address_name, "=", address_name);
|
||||
public QueryBuilder with_address(int identity_id, string address_name) {
|
||||
return select().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name);
|
||||
}
|
||||
|
||||
public void insert_device_list(string address_name, ArrayList<int32> devices) {
|
||||
update().with(this.address_name, "=", address_name).set(now_active, false).perform();
|
||||
public void insert_device_list(int32 identity_id, string address_name, ArrayList<int32> devices) {
|
||||
update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform();
|
||||
foreach (int32 device_id in devices) {
|
||||
upsert()
|
||||
.value(this.identity_id, identity_id, true)
|
||||
.value(this.address_name, address_name, true)
|
||||
.value(this.device_id, device_id, true)
|
||||
.value(this.now_active, true)
|
||||
|
@ -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;
|
||||
return upsert()
|
||||
.value(this.identity_id, identity_id, true)
|
||||
.value(this.address_name, address_name, true)
|
||||
.value(this.device_id, device_id, true)
|
||||
.value(this.identity_key_public_base64, 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");
|
||||
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 {
|
||||
|
@ -103,6 +174,7 @@ public class Database : Qlite.Database {
|
|||
}
|
||||
|
||||
public IdentityMetaTable identity_meta { get; private set; }
|
||||
public TrustTable trust { get; private set; }
|
||||
public IdentityTable identity { get; private set; }
|
||||
public SignedPreKeyTable signed_pre_key { get; private set; }
|
||||
public PreKeyTable pre_key { get; private set; }
|
||||
|
@ -111,18 +183,29 @@ public class Database : Qlite.Database {
|
|||
public Database(string fileName) {
|
||||
base(fileName, VERSION);
|
||||
identity_meta = new IdentityMetaTable(this);
|
||||
trust = new TrustTable(this);
|
||||
identity = new IdentityTable(this);
|
||||
signed_pre_key = new SignedPreKeyTable(this);
|
||||
pre_key = new PreKeyTable(this);
|
||||
session = new SessionTable(this);
|
||||
init({identity_meta, identity, signed_pre_key, pre_key, session});
|
||||
init({identity_meta, trust, identity, signed_pre_key, pre_key, session});
|
||||
try {
|
||||
exec("PRAGMA synchronous=0");
|
||||
} catch (Error e) { }
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
94
plugins/omemo/src/device_notification_populator.vala
Normal file
94
plugins/omemo/src/device_notification_populator.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@ public class EncryptState {
|
|||
public int other_lost { get; internal set; }
|
||||
public int other_unknown { 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_success { get; internal set; }
|
||||
|
@ -17,7 +17,7 @@ public class EncryptState {
|
|||
public bool own_list { get; internal set; }
|
||||
|
||||
public string to_string() {
|
||||
return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, 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))";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
166
plugins/omemo/src/manage_key_dialog.vala
Normal file
166
plugins/omemo/src/manage_key_dialog.vala
Normal 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])));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,7 @@ public class Manager : StreamInteractionModule, Object {
|
|||
|
||||
private StreamInteractor stream_interactor;
|
||||
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 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_own_sessions { 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 will_send_now { get; private 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_own_sessions = new_try.own_unknown;
|
||||
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;
|
||||
will_send_now = false;
|
||||
if (new_try.other_failure > 0 || (new_try.other_lost == new_try.other_devices && new_try.other_devices > 0)) {
|
||||
msg.marked = Entities.Message.Marked.WONTSEND;
|
||||
} else if (new_try.other_unknown > 0 || new_try.own_unknown > 0 || !new_try.other_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;
|
||||
} else if (!new_try.encrypted) {
|
||||
msg.marked = Entities.Message.Marked.WONTSEND;
|
||||
|
@ -52,22 +53,24 @@ public class Manager : StreamInteractionModule, Object {
|
|||
}
|
||||
|
||||
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() {
|
||||
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.db = db;
|
||||
this.trust_manager = trust_manager;
|
||||
|
||||
stream_interactor.stream_negotiated.connect(on_stream_negotiated);
|
||||
stream_interactor.account_added.connect(on_account_added);
|
||||
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
|
||||
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 {
|
||||
|
@ -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) {
|
||||
if (message.encryption == Encryption.OMEMO) {
|
||||
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||
|
@ -98,7 +118,22 @@ public class Manager : StreamInteractionModule, Object {
|
|||
return;
|
||||
}
|
||||
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;
|
||||
lock (message_states) {
|
||||
if (message_states.has_key(message)) {
|
||||
|
@ -113,48 +148,161 @@ public class Manager : StreamInteractionModule, Object {
|
|||
}
|
||||
}
|
||||
|
||||
//Encryption failed - need to fetch more information
|
||||
if (!state.will_send_now) {
|
||||
if (message.marked == Entities.Message.Marked.WONTSEND) {
|
||||
if (Plugin.DEBUG) print(@"OMEMO: message was not sent: $state\n");
|
||||
message_states.unset(message);
|
||||
} else {
|
||||
if (Plugin.DEBUG) print(@"OMEMO: message will be delayed: $state\n");
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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).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) {
|
||||
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) {
|
||||
if (Plugin.DEBUG) print(@"OMEMO: session start between $(account.bare_jid) and $jid $(failed ? "failed" : "successful")\n");
|
||||
private void on_device_list_loaded(Account account, Jid jid, ArrayList<int32> device_list) {
|
||||
if (Plugin.DEBUG) print(@"OMEMO: received device list for $(account.bare_jid) from $jid\n");
|
||||
|
||||
XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if (stream == null) {
|
||||
return;
|
||||
}
|
||||
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
|
||||
if (module == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int identity_id = db.identity.get_id(account.id);
|
||||
if (identity_id < 0) return;
|
||||
|
||||
//Update meta database
|
||||
db.identity_meta.insert_device_list(identity_id, jid.bare_jid.to_string(), device_list);
|
||||
|
||||
//Fetch the bundle for each new device
|
||||
int inc = 0;
|
||||
foreach (Row row in db.identity_meta.get_unknown_devices(identity_id, jid.bare_jid.to_string())) {
|
||||
module.fetch_bundle(stream, Jid.parse(row[db.identity_meta.address_name]), row[db.identity_meta.device_id]);
|
||||
inc++;
|
||||
}
|
||||
if (inc > 0) {
|
||||
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>();
|
||||
lock (message_states) {
|
||||
foreach (Entities.Message msg in message_states.keys) {
|
||||
if (!msg.account.equals(account)) continue;
|
||||
Gee.List<Jid> occupants = get_occupants(msg.counterpart.bare_jid, account);
|
||||
MessageState state = message_states[msg];
|
||||
if (account.bare_jid.equals(jid)) {
|
||||
state.waiting_own_devicelist = false;
|
||||
} else if (msg.counterpart != null && occupants.contains(jid)) {
|
||||
state.waiting_other_devicelists--;
|
||||
}
|
||||
if (state.should_retry_now()) {
|
||||
send_now.add(msg);
|
||||
state.active_send_attempt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (Entities.Message msg in send_now) {
|
||||
if (msg.counterpart == null) continue;
|
||||
Entities.Conversation? conv = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(((!)msg.counterpart), account);
|
||||
if (conv == null) continue;
|
||||
stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) {
|
||||
int identity_id = db.identity.get_id(account.id);
|
||||
if (identity_id < 0) return;
|
||||
|
||||
bool blind_trust = db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string());
|
||||
|
||||
//If we don't blindly trust new devices and we haven't seen this key before then don't trust it
|
||||
bool untrust = !(blind_trust || db.identity_meta.with_address(identity_id, jid.bare_jid.to_string())
|
||||
.with(db.identity_meta.device_id, "=", device_id)
|
||||
.with(db.identity_meta.identity_key_public_base64, "=", Base64.encode(bundle.identity_key.serialize()))
|
||||
.single().row().is_present());
|
||||
|
||||
//Get trust information from the database if the device id is known
|
||||
Row device = db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), device_id);
|
||||
Database.IdentityMetaTable.TrustLevel trusted = Database.IdentityMetaTable.TrustLevel.UNKNOWN;
|
||||
if (device != null) {
|
||||
trusted = (Database.IdentityMetaTable.TrustLevel) device[db.identity_meta.trust_level];
|
||||
}
|
||||
|
||||
if(untrust) {
|
||||
trusted = Database.IdentityMetaTable.TrustLevel.UNKNOWN;
|
||||
} else if (blind_trust && trusted == Database.IdentityMetaTable.TrustLevel.UNKNOWN) {
|
||||
trusted = Database.IdentityMetaTable.TrustLevel.TRUSTED;
|
||||
}
|
||||
|
||||
//Update the database with the appropriate trust information
|
||||
db.identity_meta.insert_device_bundle(identity_id, jid.bare_jid.to_string(), device_id, bundle, trusted);
|
||||
|
||||
XmppStream? stream = stream_interactor.get_stream(account);
|
||||
if(stream == null) return;
|
||||
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
|
||||
if(module == null) return;
|
||||
|
||||
//Get all messages waiting on the bundle and determine if they can now be sent
|
||||
HashSet<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)) {
|
||||
} else if (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)) && session_created) {
|
||||
state.waiting_other_sessions--;
|
||||
}
|
||||
if (state.should_retry_now()){
|
||||
|
@ -171,62 +319,14 @@ public class Manager : StreamInteractionModule, Object {
|
|||
}
|
||||
}
|
||||
|
||||
private void on_device_list_loaded(Account account, Jid jid) {
|
||||
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) {
|
||||
foreach (Entities.Message msg in message_states.keys) {
|
||||
if (!msg.account.equals(account)) continue;
|
||||
MessageState state = message_states[msg];
|
||||
if (account.bare_jid.equals(jid)) {
|
||||
state.waiting_own_devicelist = false;
|
||||
} else if (msg.counterpart != null && msg.counterpart.equals_bare(jid)) {
|
||||
state.waiting_other_devicelist = false;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
db.identity_meta.insert_device_bundle(jid.bare_jid.to_string(), device_id, bundle);
|
||||
}
|
||||
|
||||
private void on_store_created(Account account, Store store) {
|
||||
Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner;
|
||||
int identity_id = -1;
|
||||
bool publish_identity = false;
|
||||
|
||||
if (row == null) {
|
||||
// OMEMO not yet initialized, starting with empty base
|
||||
publish_identity = true;
|
||||
try {
|
||||
store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX);
|
||||
|
||||
|
@ -257,19 +357,40 @@ public class Manager : StreamInteractionModule, Object {
|
|||
} else {
|
||||
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) {
|
||||
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||
if (stream == null) return false;
|
||||
StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
|
||||
if (module == null) return false;
|
||||
return ((!)module).is_known_address(conversation.counterpart.bare_jid);
|
||||
if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(conversation.counterpart, conversation.account)){
|
||||
Xep.Muc.Flag? flag = stream.get_flag(Xep.Muc.Flag.IDENTITY);
|
||||
if (flag == null) return false;
|
||||
if (flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.NON_ANONYMOUS) && flag.has_room_feature(conversation.counterpart, Xep.Muc.Feature.MEMBERS_ONLY)) {
|
||||
foreach(Jid jid in stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account)) {
|
||||
if (!trust_manager.is_known_address(conversation.account, jid.bare_jid)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return trust_manager.is_known_address(conversation.account, conversation.counterpart.bare_jid);
|
||||
}
|
||||
|
||||
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||
Manager m = new Manager(stream_interactor, db);
|
||||
public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) {
|
||||
Manager m = new Manager(stream_interactor, db, trust_manager);
|
||||
stream_interactor.add_module(m);
|
||||
}
|
||||
}
|
||||
|
|
42
plugins/omemo/src/own_notifications.vala
Normal file
42
plugins/omemo/src/own_notifications.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,9 @@ public class Plugin : RootInterface, Object {
|
|||
public EncryptionListEntry list_entry;
|
||||
public AccountSettingsEntry settings_entry;
|
||||
public ContactDetailsProvider contact_details_provider;
|
||||
public DeviceNotificationPopulator device_notification_populator;
|
||||
public OwnNotifications own_notifications;
|
||||
public TrustManager trust_manager;
|
||||
|
||||
public void registered(Dino.Application app) {
|
||||
ensure_context();
|
||||
|
@ -36,13 +39,29 @@ public class Plugin : RootInterface, Object {
|
|||
this.list_entry = new EncryptionListEntry(this);
|
||||
this.settings_entry = new AccountSettingsEntry(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_account_settings_entry(settings_entry);
|
||||
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) => {
|
||||
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;
|
||||
if (app.search_path_generator != null) {
|
||||
|
|
|
@ -16,115 +16,24 @@ private const int NUM_KEYS_TO_PUBLISH = 100;
|
|||
public class StreamModule : XmppStreamModule {
|
||||
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<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 ReceivedPipelineListener received_pipeline_listener;
|
||||
|
||||
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 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) {
|
||||
if (!Plugin.ensure_context()) return;
|
||||
|
||||
this.store = Plugin.get_context().create_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));
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -150,31 +59,26 @@ public class StreamModule : XmppStreamModule {
|
|||
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()));
|
||||
stream.get_module(Pubsub.Module.IDENTITY).publish(stream, jid, NODE_DEVICELIST, NODE_DEVICELIST, id, node);
|
||||
} else {
|
||||
}
|
||||
publish_bundles_if_needed(stream, jid);
|
||||
}
|
||||
}
|
||||
lock(device_lists) {
|
||||
device_lists[jid] = new ArrayList<int32>();
|
||||
|
||||
ArrayList<int32> device_list = new ArrayList<int32>();
|
||||
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);
|
||||
device_list_loaded(jid);
|
||||
device_list_loaded(jid, device_list);
|
||||
}
|
||||
|
||||
public void start_sessions_with(XmppStream stream, Jid jid) {
|
||||
if (!device_lists.has_key(jid)) {
|
||||
return;
|
||||
}
|
||||
public void fetch_bundles(XmppStream stream, Jid jid, Gee.List<int32> devices) {
|
||||
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)) {
|
||||
address.device_id = device_id;
|
||||
try {
|
||||
if (!store.contains_session(address)) {
|
||||
start_session_with(stream, jid, device_id);
|
||||
fetch_bundle(stream, jid, device_id);
|
||||
}
|
||||
} catch (Error e) {
|
||||
// Ignore
|
||||
|
@ -184,7 +88,7 @@ public class StreamModule : XmppStreamModule {
|
|||
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 (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) => {
|
||||
|
@ -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) {
|
||||
if (device_id <= 0) return;
|
||||
lock (ignored_devices) {
|
||||
|
@ -223,7 +105,6 @@ public class StreamModule : XmppStreamModule {
|
|||
}
|
||||
ignored_devices[jid].add(device_id);
|
||||
}
|
||||
session_start_failed(jid, 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) {
|
||||
bool fail = false;
|
||||
if (node == null) {
|
||||
// Device not registered, shouldn't exist
|
||||
fail = true;
|
||||
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
||||
} else {
|
||||
Bundle bundle = new Bundle(node);
|
||||
bundle_fetched(jid, device_id, bundle);
|
||||
}
|
||||
stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id");
|
||||
}
|
||||
|
||||
public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) {
|
||||
bool fail = false;
|
||||
int32 signed_pre_key_id = bundle.signed_pre_key_id;
|
||||
ECPublicKey? signed_pre_key = bundle.signed_pre_key;
|
||||
uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
|
||||
|
@ -259,22 +145,20 @@ public class StreamModule : XmppStreamModule {
|
|||
Address address = new Address(jid.bare_jid.to_string(), device_id);
|
||||
try {
|
||||
if (store.contains_session(address)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
SessionBuilder builder = store.create_session_builder(address);
|
||||
builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key));
|
||||
stream.get_module(IDENTITY).session_started(jid, device_id);
|
||||
} catch (Error e) {
|
||||
fail = true;
|
||||
}
|
||||
address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
stream.get_module(IDENTITY).ignore_device(jid, device_id);
|
||||
}
|
||||
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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
253
plugins/omemo/src/trust_manager.vala
Normal file
253
plugins/omemo/src/trust_manager.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
50
plugins/omemo/vapi/qrencode.vapi
Normal file
50
plugins/omemo/vapi/qrencode.vapi
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -375,6 +375,10 @@ public class Store : Object {
|
|||
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 {
|
||||
SessionRecord record;
|
||||
throw_by_code(Protocol.Session.load_session(native_context, out record, other));
|
||||
|
|
|
@ -48,7 +48,7 @@ public class Table {
|
|||
try {
|
||||
db.exec(@"INSERT INTO _fts_$name(_fts_$name) VALUES('rebuild');");
|
||||
} 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) {
|
||||
ensure_init();
|
||||
string sql = @"CREATE TABLE IF NOT EXISTS $name (";
|
||||
bool first = true;
|
||||
for (int i = 0; i < columns.length; i++) {
|
||||
Column c = columns[i];
|
||||
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)";
|
||||
try {
|
||||
db.exec(sql);
|
||||
} 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) {
|
||||
try {
|
||||
db.exec(stmt);
|
||||
} 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 {
|
||||
db.exec(@"ALTER TABLE $name ADD COLUMN $(c.to_column_definition())");
|
||||
} 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(@"DROP TABLE _$(name)_$old_version");
|
||||
} 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 {
|
||||
db.exec(stmt);
|
||||
} catch (Error e) {
|
||||
error("Qlite Error: Post");
|
||||
error(@"Qlite Error: Post: $(e.message)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
using Gee;
|
||||
|
||||
namespace Xmpp.Presence {
|
||||
private const string NS_URI = "jabber:client";
|
||||
|
||||
|
@ -87,6 +89,8 @@ namespace Xmpp.Presence {
|
|||
stream.get_flag(Flag.IDENTITY).remove_presence(presence.from);
|
||||
received_unsubscription(stream, presence.from);
|
||||
break;
|
||||
case Presence.Stanza.TYPE_UNSUBSCRIBED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ public class Module : XmppStreamModule, Iq.Handler {
|
|||
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_updated(XmppStream stream, Item item, Iq.Stanza iq);
|
||||
public signal void mutual_subscription(XmppStream stream, Jid jid);
|
||||
|
||||
public bool interested_resource = true;
|
||||
|
||||
|
@ -55,8 +56,12 @@ public class Module : XmppStreamModule, Iq.Handler {
|
|||
item_removed(stream, item, iq);
|
||||
break;
|
||||
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;
|
||||
item_updated(stream, item, iq);
|
||||
if(is_new) mutual_subscription(stream, item.jid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue