aboutsummaryrefslogtreecommitdiff
path: root/plugins/openpgp
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/openpgp')
-rw-r--r--plugins/openpgp/CMakeLists.txt61
-rw-r--r--plugins/openpgp/data/account_settings_item.ui31
-rw-r--r--plugins/openpgp/src/account_settings_entry.vala18
-rw-r--r--plugins/openpgp/src/account_settings_widget.vala61
-rw-r--r--plugins/openpgp/src/encryption_list_entry.vala26
-rw-r--r--plugins/openpgp/src/manager.vala78
-rw-r--r--plugins/openpgp/src/plugin.vala29
-rw-r--r--plugins/openpgp/src/register_plugin.vala3
-rw-r--r--plugins/openpgp/src/xmpp_flag.vala25
-rw-r--r--plugins/openpgp/src/xmpp_module.vala154
10 files changed, 486 insertions, 0 deletions
diff --git a/plugins/openpgp/CMakeLists.txt b/plugins/openpgp/CMakeLists.txt
new file mode 100644
index 00000000..a230872e
--- /dev/null
+++ b/plugins/openpgp/CMakeLists.txt
@@ -0,0 +1,61 @@
+find_package(Vala REQUIRED)
+find_package(PkgConfig REQUIRED)
+include(${VALA_USE_FILE})
+include(GlibCompileResourcesSupport)
+
+set(OPENPGP_PACKAGES
+ gee-0.8
+ glib-2.0
+ gtk+-3.0
+ gmodule-2.0
+ sqlite3
+)
+
+pkg_check_modules(OPENPGP REQUIRED ${OPENPGP_PACKAGES})
+
+set(RESOURCE_LIST
+ account_settings_item.ui
+)
+
+compile_gresources(
+ OPENPGP_GRESOURCES_TARGET
+ OPENPGP_GRESOURCES_XML
+ TARGET ${CMAKE_CURRENT_BINARY_DIR}/resources/resources.c
+ TYPE EMBED_C
+ RESOURCES ${RESOURCE_LIST}
+ PREFIX /org/dino-im
+ SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data
+)
+
+vala_precompile(OPENPGP_VALA_C
+SOURCES
+ src/account_settings_entry.vala
+ src/account_settings_widget.vala
+ src/encryption_list_entry.vala
+ src/manager.vala
+ src/plugin.vala
+ src/register_plugin.vala
+ src/xmpp_flag.vala
+ src/xmpp_module.vala
+CUSTOM_VAPIS
+ ${CMAKE_BINARY_DIR}/exports/gpgme.vapi
+ ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
+ ${CMAKE_BINARY_DIR}/exports/qlite.vapi
+ ${CMAKE_BINARY_DIR}/exports/dino.vapi
+PACKAGES
+ ${OPENPGP_PACKAGES}
+GRESOURCES
+ ${OPENPGP_GRESOURCES_XML}
+OPTIONS
+ --target-glib=2.38
+ ${GLOBAL_DEBUG_FLAGS}
+ --thread
+)
+
+set(CFLAGS ${VALA_CFLAGS} ${OPENPGP_CFLAGS})
+add_definitions(${CFLAGS})
+add_library(openpgp SHARED ${OPENPGP_VALA_C} ${OPENPGP_GRESOURCES_TARGET})
+add_dependencies(openpgp dino-vapi gpgme-vapi)
+target_link_libraries(openpgp libdino gpgme-vala)
+set_target_properties(openpgp PROPERTIES PREFIX "")
+set_target_properties(openpgp PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
diff --git a/plugins/openpgp/data/account_settings_item.ui b/plugins/openpgp/data/account_settings_item.ui
new file mode 100644
index 00000000..95f09046
--- /dev/null
+++ b/plugins/openpgp/data/account_settings_item.ui
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="DinoPluginsOpenPgpAccountSettingsWidget">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="pgp_button">
+ <property name="relief">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="pgp_label">
+ <property name="xalign">0</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">label</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="pgp_combobox">
+ <property name="hexpand">True</property>
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">entry</property>
+ </packing>
+ </child>
+ </template>
+</interface> \ No newline at end of file
diff --git a/plugins/openpgp/src/account_settings_entry.vala b/plugins/openpgp/src/account_settings_entry.vala
new file mode 100644
index 00000000..1deef763
--- /dev/null
+++ b/plugins/openpgp/src/account_settings_entry.vala
@@ -0,0 +1,18 @@
+namespace Dino.Plugins.OpenPgp {
+
+public class AccountSettingsEntry : Plugins.AccountSettingsEntry {
+
+ public override string id { get {
+ return "pgp_key_picker";
+ }}
+
+ public override string name { get {
+ return "OpenPGP";
+ }}
+
+ public override Plugins.AccountSettingsWidget get_widget() {
+ return new AccountSettingsWidget();
+ }
+}
+
+} \ No newline at end of file
diff --git a/plugins/openpgp/src/account_settings_widget.vala b/plugins/openpgp/src/account_settings_widget.vala
new file mode 100644
index 00000000..b9e6edbd
--- /dev/null
+++ b/plugins/openpgp/src/account_settings_widget.vala
@@ -0,0 +1,61 @@
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+[GtkTemplate (ui = "/org/dino-im/account_settings_item.ui")]
+
+private class AccountSettingsWidget : Gtk.Stack, Plugins.AccountSettingsWidget {
+ [GtkChild] private Gtk.Label pgp_label;
+ [GtkChild] private Gtk.Button pgp_button;
+ [GtkChild] private Gtk.ComboBox pgp_combobox;
+
+ private Gtk.ListStore list_store = new Gtk.ListStore(2, typeof(string), typeof(string?));
+
+ public AccountSettingsWidget() {
+ Gtk.CellRendererText renderer = new Gtk.CellRendererText();
+ renderer.set_padding(0, 0);
+ pgp_combobox.pack_start(renderer, true);
+ pgp_combobox.add_attribute(renderer, "markup", 0);
+ pgp_button.clicked.connect(() => { activated(); this.set_visible_child_name("entry"); pgp_combobox.popup(); });
+ }
+
+ public void deactivate() {
+ this.set_visible_child_name("label");
+ }
+
+ private void key_changed() {
+ Gtk.TreeIter selected;
+ pgp_combobox.get_active_iter(out selected);
+ Value text;
+ list_store.get_value(selected, 0, out text);
+ pgp_label.set_markup((string) text);
+ deactivate();
+ }
+
+ public void set_account(Account account) {
+ populate_pgp_combobox(account);
+ }
+
+ private void populate_pgp_combobox(Account account) {
+ pgp_combobox.changed.disconnect(key_changed);
+
+ Gtk.TreeIter iter;
+ pgp_combobox.set_model(list_store);
+
+ list_store.clear();
+ list_store.append(out iter);
+ pgp_label.set_markup("Disabled\n<span font='9'>Select key</span>");
+ list_store.set(iter, 0, "Disabled\n<span font='9'>Select key</span>", 1, null);
+ Gee.List<GPG.Key> list = GPGHelper.get_keylist(null, true);
+ foreach (GPG.Key key in list) {
+ list_store.append(out iter);
+ list_store.set(iter, 0, @"<span font='11'>$(Markup.escape_text(key.uids[0].uid))</span>\n<span font='9'>0x$(Markup.escape_text(key.fpr[0:16]))</span>");
+ list_store.set(iter, 1, key.fpr);
+ }
+
+ pgp_combobox.set_active(0);
+ pgp_combobox.changed.connect(key_changed);
+ }
+}
+
+} \ No newline at end of file
diff --git a/plugins/openpgp/src/encryption_list_entry.vala b/plugins/openpgp/src/encryption_list_entry.vala
new file mode 100644
index 00000000..96607e1e
--- /dev/null
+++ b/plugins/openpgp/src/encryption_list_entry.vala
@@ -0,0 +1,26 @@
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+private class EncryptionListEntry : Plugins.EncryptionListEntry, Object {
+
+ private StreamInteractor stream_interactor;
+
+ public EncryptionListEntry(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public Entities.Encryption encryption { get {
+ return Encryption.PGP;
+ }}
+
+ public string name { get {
+ return "OpenPGP";
+ }}
+
+ public bool can_encrypt(Entities.Conversation conversation) {
+ return Manager.get_instance(stream_interactor).get_key_id(conversation.account, conversation.counterpart) != null;
+ }
+}
+
+} \ No newline at end of file
diff --git a/plugins/openpgp/src/manager.vala b/plugins/openpgp/src/manager.vala
new file mode 100644
index 00000000..81077088
--- /dev/null
+++ b/plugins/openpgp/src/manager.vala
@@ -0,0 +1,78 @@
+using Gee;
+using Xmpp;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+ public class Manager : StreamInteractionModule, Object {
+ public const string id = "pgp_manager";
+
+ public const string MESSAGE_ENCRYPTED = "pgp";
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+ private HashMap<Jid, string> pgp_key_ids = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
+
+ public static void start(StreamInteractor stream_interactor, Database db) {
+ Manager m = new Manager(stream_interactor, db);
+ stream_interactor.add_module(m);
+ }
+
+ private Manager(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+
+ stream_interactor.account_added.connect(on_account_added);
+ MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_pre_message_received);
+ MessageManager.get_instance(stream_interactor).pre_message_send.connect(on_pre_message_send);
+ }
+
+ private void on_pre_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
+ if (MessageFlag.get_flag(message_stanza) != null && MessageFlag.get_flag(message_stanza).decrypted) {
+ message.encryption = Encryption.PGP;
+ }
+ }
+
+ private void on_pre_message_send(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
+ if (message.encryption == Encryption.PGP) {
+ string? key_id = get_key_id(conversation.account, message.counterpart);
+ bool encrypted = false;
+ if (key_id != null) {
+ encrypted = stream_interactor.get_stream(conversation.account).get_module(Module.IDENTITY).encrypt(message_stanza, key_id);
+ }
+ if (!encrypted) {
+ message.marked = Entities.Message.Marked.WONTSEND;
+ }
+ }
+ }
+
+ public string? get_key_id(Account account, Jid jid) {
+ return db.get_pgp_key(jid);
+ }
+
+ public static Manager? get_instance(StreamInteractor stream_interactor) {
+ return (Manager) stream_interactor.get_module(id);
+ }
+
+ internal string get_id() {
+ return id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.get_module(account, Module.IDENTITY).received_jid_key_id.connect((stream, jid, key_id) => {
+ on_jid_key_received(account, new Jid(jid), key_id);
+ });
+ }
+
+ private void on_jid_key_received(Account account, Jid jid, string key_id) {
+ if (!pgp_key_ids.has_key(jid) || pgp_key_ids[jid] != key_id) {
+ if (!MucManager.get_instance(stream_interactor).is_groupchat_occupant(jid, account)) {
+ db.set_pgp_key(jid.bare_jid, key_id);
+ }
+ }
+ pgp_key_ids[jid] = key_id;
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/openpgp/src/plugin.vala b/plugins/openpgp/src/plugin.vala
new file mode 100644
index 00000000..d25c8cd0
--- /dev/null
+++ b/plugins/openpgp/src/plugin.vala
@@ -0,0 +1,29 @@
+namespace Dino.Plugins.OpenPgp {
+
+ public class Plugin : Plugins.RootInterface, Object {
+ public Dino.Application app;
+ public Database db;
+
+ private Module module;
+ private EncryptionListEntry list_entry;
+ private AccountSettingsEntry settings_entry;
+
+ public void registered(Dino.Application app) {
+ this.app = app;
+ this.module = new Module();
+ this.list_entry = new EncryptionListEntry(app.stream_interaction);
+ this.settings_entry = new AccountSettingsEntry();
+ app.plugin_registry.register_encryption_list_entry(list_entry);
+ app.plugin_registry.register_account_settings_entry(settings_entry);
+ app.stream_interaction.module_manager.initialize_account_modules.connect((account, list) => {
+ list.add(new Module());
+ });
+ Manager.start(app.stream_interaction, app.db);
+ }
+
+ public void shutdown() {
+ // Nothing to do
+ }
+ }
+
+}
diff --git a/plugins/openpgp/src/register_plugin.vala b/plugins/openpgp/src/register_plugin.vala
new file mode 100644
index 00000000..c5d811a7
--- /dev/null
+++ b/plugins/openpgp/src/register_plugin.vala
@@ -0,0 +1,3 @@
+public Type register_plugin(Module module) {
+ return typeof (Dino.Plugins.OpenPgp.Plugin);
+} \ No newline at end of file
diff --git a/plugins/openpgp/src/xmpp_flag.vala b/plugins/openpgp/src/xmpp_flag.vala
new file mode 100644
index 00000000..5ace26bd
--- /dev/null
+++ b/plugins/openpgp/src/xmpp_flag.vala
@@ -0,0 +1,25 @@
+using Gee;
+
+using Xmpp;
+using Xmpp.Core;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class Flag : XmppStreamFlag {
+ public const string ID = "pgp";
+ public HashMap<string, string> key_ids = new HashMap<string, string>();
+
+ public string? get_key_id(string jid) { return key_ids[get_bare_jid(jid)]; }
+
+ public void set_key_id(string jid, string key) { key_ids[get_bare_jid(jid)] = key; }
+
+ public static Flag? get_flag(XmppStream stream) { return (Flag?) stream.get_flag(NS_URI, ID); }
+
+ public static bool has_flag(XmppStream stream) { return get_flag(stream) != null; }
+
+ public override string get_ns() { return NS_URI; }
+
+ public override string get_id() { return ID; }
+}
+
+} \ No newline at end of file
diff --git a/plugins/openpgp/src/xmpp_module.vala b/plugins/openpgp/src/xmpp_module.vala
new file mode 100644
index 00000000..440be5f1
--- /dev/null
+++ b/plugins/openpgp/src/xmpp_module.vala
@@ -0,0 +1,154 @@
+using GPG;
+
+using Xmpp;
+using Xmpp.Core;
+
+namespace Dino.Plugins.OpenPgp {
+ private const string NS_URI = "jabber:x";
+ private const string NS_URI_ENCRYPTED = NS_URI + ":encrypted";
+ private const string NS_URI_SIGNED = NS_URI + ":signed";
+
+ public class Module : XmppStreamModule {
+ public const string ID = "0027_current_pgp_usage";
+ public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, ID);
+
+ public signal void received_jid_key_id(XmppStream stream, string jid, string key_id);
+
+ private string? signed_status;
+ private string? own_key_id;
+
+ public Module() {
+ signed_status = gpg_sign("");
+ if (signed_status != null) own_key_id = gpg_verify(signed_status, "");
+ }
+
+ public bool encrypt(Message.Stanza message, string key_id) {
+ string? enc_body = gpg_encrypt(message.body, new string[] {key_id, own_key_id});
+ if (enc_body != null) {
+ message.stanza.put_node(new StanzaNode.build("x", NS_URI_ENCRYPTED).add_self_xmlns().put_node(new StanzaNode.text(enc_body)));
+ message.body = "[This message is OpenPGP encrypted (see XEP-0027)]";
+ return true;
+ }
+ return false;
+ }
+
+ public string? get_cyphertext(Message.Stanza message) {
+ StanzaNode? x_node = message.stanza.get_subnode("x", NS_URI_ENCRYPTED);
+ return x_node == null ? null : x_node.get_string_content();
+ }
+
+ public override void attach(XmppStream stream) {
+ Presence.Module.require(stream);
+ stream.get_module(Presence.Module.IDENTITY).received_presence.connect(on_received_presence);
+ stream.get_module(Presence.Module.IDENTITY).pre_send_presence_stanza.connect(on_pre_send_presence_stanza);
+ Message.Module.require(stream);
+ stream.get_module(Message.Module.IDENTITY).pre_received_message.connect(on_pre_received_message);
+ stream.add_flag(new Flag());
+ }
+
+ public override void detach(XmppStream stream) {
+ stream.get_module(Presence.Module.IDENTITY).received_presence.disconnect(on_received_presence);
+ stream.get_module(Presence.Module.IDENTITY).pre_send_presence_stanza.disconnect(on_pre_send_presence_stanza);
+ stream.get_module(Message.Module.IDENTITY).pre_received_message.disconnect(on_pre_received_message);
+ }
+
+ public static void require(XmppStream stream) {
+ if (stream.get_module(IDENTITY) == null) stream.add_module(new Module());
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return ID; }
+
+ private void on_received_presence(XmppStream stream, Presence.Stanza presence) {
+ StanzaNode x_node = presence.stanza.get_subnode("x", NS_URI_SIGNED);
+ if (x_node != null) {
+ string? sig = x_node.get_string_content();
+ if (sig != null) {
+ string signed_data = presence.status == null ? "" : presence.status;
+ string? key_id = gpg_verify(sig, signed_data);
+ if (key_id != null) {
+ Flag.get_flag(stream).set_key_id(presence.from, key_id);
+ received_jid_key_id(stream, presence.from, key_id);
+ }
+ }
+ }
+ }
+
+ private void on_pre_send_presence_stanza(XmppStream stream, Presence.Stanza presence) {
+ if (presence.type_ == Presence.Stanza.TYPE_AVAILABLE && signed_status != null) {
+ presence.stanza.put_node(new StanzaNode.build("x", NS_URI_SIGNED).add_self_xmlns().put_node(new StanzaNode.text(signed_status)));
+ }
+ }
+
+ private void on_pre_received_message(XmppStream stream, Message.Stanza message) {
+ string? encrypted = get_cyphertext(message);
+ if (encrypted != null) {
+ MessageFlag flag = new MessageFlag();
+ message.add_flag(flag);
+ string? decrypted = gpg_decrypt(encrypted);
+ if (decrypted != null) {
+ flag.decrypted = true;
+ message.body = decrypted;
+ }
+ }
+ }
+
+ private static string? gpg_encrypt(string plain, string[] key_ids) {
+ GPG.Key[] keys = new GPG.Key[key_ids.length];
+ string encr;
+ try {
+ for (int i = 0; i < key_ids.length; i++) {
+ keys[i] = GPGHelper.get_public_key(key_ids[i]);
+ }
+ encr = GPGHelper.encrypt_armor(plain, keys, GPG.EncryptFlags.ALWAYS_TRUST);
+ } catch (Error e) {
+ return null;
+ }
+ int encryption_start = encr.index_of("\n\n") + 2;
+ return encr.substring(encryption_start, encr.length - "\n-----END PGP MESSAGE-----".length - encryption_start);
+ }
+
+ private static string? gpg_decrypt(string enc) {
+ string armor = "-----BEGIN PGP MESSAGE-----\n\n" + enc + "\n-----END PGP MESSAGE-----";
+ string? decr = null;
+ try {
+ decr = GPGHelper.decrypt(armor);
+ } catch (Error e) { }
+ return decr;
+ }
+
+ private static string? gpg_verify(string sig, string signed_text) {
+ string armor = "-----BEGIN PGP MESSAGE-----\n\n" + sig + "\n-----END PGP MESSAGE-----";
+ string? sign_key = null;
+ try {
+ sign_key = GPGHelper.get_sign_key(armor, signed_text);
+ } catch (Error e) { }
+ return sign_key;
+ }
+
+ private static string? gpg_sign(string str) {
+ string signed;
+ try {
+ signed = GPGHelper.sign(str, GPG.SigMode.CLEAR);
+ } catch (Error e) {
+ return null;
+ }
+ int signature_start = signed.index_of("-----BEGIN PGP SIGNATURE-----");
+ signature_start = signed.index_of("\n\n", signature_start) + 2;
+ return signed.substring(signature_start, signed.length - "\n-----END PGP SIGNATURE-----".length - signature_start);
+ }
+ }
+
+ public class MessageFlag : Message.MessageFlag {
+ public const string id = "pgp";
+
+ public bool decrypted = false;
+
+ public static MessageFlag? get_flag(Message.Stanza message) {
+ return (MessageFlag) message.get_flag(NS_URI, id);
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return id; }
+ }
+}