aboutsummaryrefslogtreecommitdiff
path: root/main/src/plugins/openpgp
diff options
context:
space:
mode:
authorfrosty <frostinator33@airmail.cc>2026-10-26 10:21:57 -0400
committerfrosty <frostinator33@airmail.cc>2026-10-26 10:21:57 -0400
commit74aecd8b0986d07b50b7ce310d64e645b8b54e38 (patch)
tree560e6434f183eb54cd779936f3a4c9575146a066 /main/src/plugins/openpgp
parent3d7e90d401da02d430a87db0ec7e1db3a6f1a68f (diff)
parent4436001bb361ad4f9e4e05819214ecf0ea77c063 (diff)
Merge 'upstream/master'
Diffstat (limited to 'main/src/plugins/openpgp')
-rw-r--r--main/src/plugins/openpgp/contact_details_provider.vala51
-rw-r--r--main/src/plugins/openpgp/database.vala137
-rw-r--r--main/src/plugins/openpgp/encryption_list_entry.vala72
-rw-r--r--main/src/plugins/openpgp/encryption_preferences_entry.vala82
-rw-r--r--main/src/plugins/openpgp/file_transfer/file_decryptor.vala50
-rw-r--r--main/src/plugins/openpgp/file_transfer/file_encryptor.vala45
-rw-r--r--main/src/plugins/openpgp/gpgme_fix.c12
-rw-r--r--main/src/plugins/openpgp/gpgme_fix.h12
-rw-r--r--main/src/plugins/openpgp/gpgme_helper.vala185
-rw-r--r--main/src/plugins/openpgp/manager.vala123
-rw-r--r--main/src/plugins/openpgp/plugin.vala47
-rw-r--r--main/src/plugins/openpgp/stream_module.vala159
-rw-r--r--main/src/plugins/openpgp/util.vala29
13 files changed, 1004 insertions, 0 deletions
diff --git a/main/src/plugins/openpgp/contact_details_provider.vala b/main/src/plugins/openpgp/contact_details_provider.vala
new file mode 100644
index 00000000..353c0b85
--- /dev/null
+++ b/main/src/plugins/openpgp/contact_details_provider.vala
@@ -0,0 +1,51 @@
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class ContactDetailsProvider : Plugins.ContactDetailsProvider, Object {
+ public string id { get { return "pgp_info"; } }
+ public string tab { get { return "encryption"; } }
+
+ private StreamInteractor stream_interactor;
+
+ public ContactDetailsProvider(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public void populate(Conversation conversation, Plugins.ContactDetails contact_details, WidgetType type) { }
+
+ public Object? get_widget(Conversation conversation) {
+ var preferences_group = new Adw.PreferencesGroup() { title="OpenPGP" };
+
+ if (conversation.type_ != Conversation.Type.CHAT) return null;
+
+ string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, conversation.counterpart);
+ if (key_id == null) return null;
+
+ Gee.List<GPG.Key>? keys = null;
+ try {
+ keys = GPGHelper.get_keylist(key_id);
+ } catch (Error e) { }
+
+ var str = "";
+ if (keys != null && keys.size > 0) {
+ str = markup_id(keys[0].fpr, true);
+ } else {
+ str = _("Key not in keychain") + "\n" + markup_id(key_id, false);
+ }
+
+ var view = new Adw.ActionRow() {
+ title = _("Fingerprint"),
+ subtitle = str,
+ subtitle_selectable = true
+ };
+
+ preferences_group.add(view);
+
+ return preferences_group;
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/database.vala b/main/src/plugins/openpgp/database.vala
new file mode 100644
index 00000000..4c05571e
--- /dev/null
+++ b/main/src/plugins/openpgp/database.vala
@@ -0,0 +1,137 @@
+using Dino.Entities;
+using Dino.Qlite;
+using Xmpp;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class Database : Qlite.Database {
+ private const int VERSION = 1;
+
+ public class AccountSetting : Table {
+ public Column<int> account_id = new Column.Integer("account_id") { primary_key = true };
+ public Column<string> key = new Column.Text("key") { not_null = true };
+ public Column<string> signed_data = new Column.Text("signed_data") { min_version = 1 };
+ public Column<string> sig = new Column.Text("sig") { min_version = 1 };
+
+ internal AccountSetting(Database db) {
+ base(db, "account_setting");
+ init({account_id, key, signed_data, sig});
+ }
+ }
+
+ public class ContactKey : Table {
+ public Column<string> jid = new Column.Text("jid") { primary_key = true };
+ public Column<string> key = new Column.Text("key");
+ public Column<string> sig = new Column.Text("sig") { min_version = 1 };
+ public Column<string> signed_data = new Column.Text("signed_data") { min_version = 1 };
+
+ internal ContactKey(Database db) {
+ base(db, "contact_key");
+ init({jid, key, sig, signed_data});
+ }
+
+ internal void migrate_from_v0(Database db) {
+ string column_list = "";
+ foreach (Column c in columns) {
+ if (c.min_version <= VERSION && c.max_version >= VERSION) {
+ if (column_list == "") {
+ column_list = c.name;
+ } else {
+ column_list += ", " + c.name;
+ }
+ }
+ }
+ db.exec(@"ALTER TABLE $name RENAME TO _$(name)_1");
+ create_table_at_version(VERSION);
+ db.exec(@"INSERT INTO $name ($column_list) SELECT $column_list FROM _$(name)_1");
+ db.exec(@"DROP TABLE _$(name)_1");
+ }
+ }
+
+ public AccountSetting account_setting_table { get; private set; }
+ public ContactKey contact_key_table { get; private set; }
+
+ public Database(string filename) {
+ base(filename, VERSION);
+ this.account_setting_table = new AccountSetting(this);
+ this.contact_key_table = new ContactKey(this);
+ init({account_setting_table, contact_key_table});
+
+ try {
+ exec("PRAGMA journal_mode = WAL");
+ exec("PRAGMA synchronous = NORMAL");
+ exec("PRAGMA secure_delete = ON");
+ } catch (Error e) {
+ error("Failed to set OpenPGP database properties: %s", e.message);
+ }
+ }
+
+ public void set_contact_signature(Jid jid, string sig, string signed_data) {
+ contact_key_table.upsert()
+ .value(contact_key_table.jid, jid.to_string(), true)
+ .value(contact_key_table.sig, sig)
+ .value(contact_key_table.signed_data, signed_data)
+ .value_null(contact_key_table.key)
+ .perform();
+ }
+
+ public void clear_contact_signature(Jid jid) {
+ contact_key_table.upsert()
+ .value(contact_key_table.jid, jid.to_string(), true)
+ .value_null(contact_key_table.sig)
+ .value_null(contact_key_table.signed_data)
+ .perform();
+ }
+
+ public void set_contact_key(Jid jid, string key) {
+ contact_key_table.upsert()
+ .value(contact_key_table.jid, jid.to_string(), true)
+ .value(contact_key_table.key, key)
+ .perform();
+ }
+
+ public string? get_contact_key(Jid jid) {
+ return get_contact_key_row(jid)[contact_key_table.key];
+ }
+
+ public RowOption get_contact_key_row(Jid jid) {
+ return contact_key_table.select({contact_key_table.key})
+ .with(contact_key_table.jid, "=", jid.to_string()).row();
+ }
+
+ public void set_account_key(Account account, string key) {
+ account_setting_table.upsert()
+ .value(account_setting_table.account_id, account.id, true)
+ .value(account_setting_table.key, key)
+ .value_null(account_setting_table.signed_data)
+ .value_null(account_setting_table.sig)
+ .perform();
+ }
+
+ public void set_account_signature(Account account, string signed_data, string sig) {
+ account_setting_table.update()
+ .with(account_setting_table.account_id, "=", account.id)
+ .set(account_setting_table.signed_data, signed_data)
+ .set(account_setting_table.sig, sig)
+ .perform();
+ }
+
+ public string? get_account_signature(Account account, string signed_data) {
+ return account_setting_table.select({account_setting_table.sig})
+ .with(account_setting_table.account_id, "=", account.id)
+ .with(account_setting_table.signed_data, "=", signed_data)[account_setting_table.sig];
+ }
+
+ public string? get_account_key(Account account) {
+ return account_setting_table.select({account_setting_table.key})
+ .with(account_setting_table.account_id, "=", account.id)[account_setting_table.key];
+ }
+
+ public override void migrate(long oldVersion) {
+ if (oldVersion < 1) {
+ contact_key_table.migrate_from_v0(this);
+ }
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/encryption_list_entry.vala b/main/src/plugins/openpgp/encryption_list_entry.vala
new file mode 100644
index 00000000..cf5da8c4
--- /dev/null
+++ b/main/src/plugins/openpgp/encryption_list_entry.vala
@@ -0,0 +1,72 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Plugins.OpenPgp {
+
+private class EncryptionListEntry : Plugins.EncryptionListEntry, Object {
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+
+ public EncryptionListEntry(StreamInteractor stream_interactor, Database db) {
+ this.stream_interactor = stream_interactor;
+ this.db = db;
+ }
+
+ public Entities.Encryption encryption { get {
+ return Encryption.PGP;
+ }}
+
+ public string name { get {
+ return "OpenPGP";
+ }}
+
+ public Object? get_encryption_icon(Entities.Conversation conversation, ContentItem content_item) {
+ return null;
+ }
+
+ public string? get_encryption_icon_name(Entities.Conversation conversation, ContentItem content_item) {
+ return null;
+ }
+
+ public void encryption_activated(Entities.Conversation conversation, Plugins.SetInputFieldStatus input_status_callback) {
+ try {
+ GPGHelper.get_public_key(db.get_account_key(conversation.account) ?? "");
+ } catch (Error e) {
+ input_status_callback(new Plugins.InputFieldStatus("You didn't configure OpenPGP for this account. You can do that in the Accounts Dialog.", Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND));
+ return;
+ }
+
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, conversation.counterpart);
+ if (key_id == null) {
+ input_status_callback(new Plugins.InputFieldStatus("This contact does not support %s encryption.".printf("OpenPGP"), Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND));
+ return;
+ }
+ try {
+ GPGHelper.get_keylist(key_id);
+ } catch (Error e) {
+ input_status_callback(new Plugins.InputFieldStatus("This contact's OpenPGP key is not in your keyring.", Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND));
+ }
+ } else if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ Gee.List<Jid> muc_jids = new Gee.ArrayList<Jid>();
+ Gee.List<Jid>? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_occupants(conversation.counterpart, conversation.account);
+ if (occupants != null) muc_jids.add_all(occupants);
+ Gee.List<Jid>? offline_members = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account);
+ if (offline_members != null) muc_jids.add_all(offline_members);
+
+ foreach (Jid jid in muc_jids) {
+ string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, jid);
+ if (key_id == null) {
+ input_status_callback(new Plugins.InputFieldStatus("A member's OpenPGP key is not in your keyring: %s / %s.".printf(jid.to_string(), key_id), Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND));
+ return;
+ }
+ }
+ }
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/encryption_preferences_entry.vala b/main/src/plugins/openpgp/encryption_preferences_entry.vala
new file mode 100644
index 00000000..b03f6acd
--- /dev/null
+++ b/main/src/plugins/openpgp/encryption_preferences_entry.vala
@@ -0,0 +1,82 @@
+using Adw;
+using Dino.Entities;
+using Gtk;
+
+namespace Dino.Plugins.OpenPgp {
+
+ public class PgpPreferencesEntry : Plugins.EncryptionPreferencesEntry {
+
+ private Plugin plugin;
+
+ public PgpPreferencesEntry(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ public override Object? get_widget(Account account, WidgetType type) {
+ if (type != WidgetType.GTK4) return null;
+ StringList string_list = new StringList(null);
+ string_list.append(_("Querying GnuPG"));
+
+ Adw.PreferencesGroup preferences_group = new Adw.PreferencesGroup() { title="OpenPGP" };
+ populate_string_list.begin(account, preferences_group);
+
+ return preferences_group;
+ }
+
+ public override string id { get { return "pgp_preferences_encryption"; }}
+
+ private async void populate_string_list(Account account, Adw.PreferencesGroup preferences_group) {
+ var keys = yield get_pgp_keys();
+
+ if (keys == null) {
+ preferences_group.add(new Adw.ActionRow() { title=_("Announce key"), subtitle=_("Error in GnuPG") });
+ return;
+ }
+ if (keys.size == 0) {
+ preferences_group.add(new Adw.ActionRow() { title=_("Announce key"), subtitle= _("No keys available. Generate one!") });
+ return;
+ }
+
+ StringList string_list = new StringList(null);
+ var drop_down = new Adw.ComboRow() { title = _("Announce key") };
+ drop_down.model = string_list;
+ preferences_group.add(drop_down);
+
+ string_list.append(_("Disabled"));
+ for (int i = 0; i < keys.size; i++) {
+ string_list.append(@"$(keys[i].uids[0].uid)\n$(keys[i].fpr.substring(24, 16))");
+ if (keys[i].fpr == plugin.db.get_account_key(account)) {
+ drop_down.selected = i + 1;
+ }
+ }
+
+ drop_down.notify["selected"].connect(() => {
+ var key_id = drop_down.selected == 0 ? "" : keys[(int)drop_down.selected - 1].fpr;
+ string? signed_data = null, sig = null;
+ if (plugin.modules.has_key(account)) {
+ plugin.modules[account].set_private_key_id(key_id, ref signed_data, ref sig);
+ }
+ plugin.db.set_account_key(account, key_id);
+ if (signed_data != null && sig != null) {
+ plugin.db.set_account_signature(account, signed_data, sig);
+ }
+ });
+ }
+
+ private static async Gee.List<GPG.Key> get_pgp_keys() {
+ Gee.List<GPG.Key> keys = null;
+ SourceFunc callback = get_pgp_keys.callback;
+ new Thread<void*> (null, () => { // Querying GnuPG might take some time
+ try {
+ keys = GPGHelper.get_keylist(null, true);
+ } catch (Error e) {
+ warning(e.message);
+ }
+ Idle.add((owned)callback);
+ return null;
+ });
+ yield;
+ return keys;
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/plugins/openpgp/file_transfer/file_decryptor.vala b/main/src/plugins/openpgp/file_transfer/file_decryptor.vala
new file mode 100644
index 00000000..2deb5823
--- /dev/null
+++ b/main/src/plugins/openpgp/file_transfer/file_decryptor.vala
@@ -0,0 +1,50 @@
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class PgpFileDecryptor : FileDecryptor, Object {
+
+ public Encryption get_encryption() {
+ return Encryption.PGP;
+ }
+
+ public FileReceiveData prepare_get_meta_info(Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) {
+ return receive_data;
+ }
+
+ public FileMeta prepare_download_file(Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) {
+ return file_meta;
+ }
+
+ public bool can_decrypt_file(Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) {
+ return file_transfer.file_name.has_suffix("pgp") ||
+ (file_transfer.content_type != null && file_transfer.content_type.get_mime_type() == "application/pgp-encrypted");
+ }
+
+ public async InputStream decrypt_file(InputStream encrypted_stream, Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) throws FileReceiveError {
+ try {
+ uint8[] buf = new uint8[256];
+ ByteArray data = new ByteArray();
+ size_t len = -1;
+ do {
+ len = yield encrypted_stream.read_async(buf);
+ data.append(buf[0:len]);
+ } while(len > 0);
+
+ GPGHelper.DecryptedData clear_data = GPGHelper.decrypt_data(data.data);
+ file_transfer.encryption = Encryption.PGP;
+ if (clear_data.filename != null && clear_data.filename != "") {
+ debug("Decrypting file %s from %s", clear_data.filename, file_transfer.file_name);
+ file_transfer.file_name = clear_data.filename;
+ } else if (file_transfer.file_name.has_suffix(".pgp")) {
+ debug("Decrypting file %s from %s", file_transfer.file_name.substring(0, file_transfer.file_name.length - 4), file_transfer.file_name);
+ file_transfer.file_name = file_transfer.file_name.substring(0, file_transfer.file_name.length - 4);
+ }
+ return new MemoryInputStream.from_data(clear_data.data, GLib.free);
+ } catch (Error e) {
+ throw new FileReceiveError.DECRYPTION_FAILED("PGP file decryption error: %s".printf(e.message));
+ }
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/file_transfer/file_encryptor.vala b/main/src/plugins/openpgp/file_transfer/file_encryptor.vala
new file mode 100644
index 00000000..66e93bd9
--- /dev/null
+++ b/main/src/plugins/openpgp/file_transfer/file_encryptor.vala
@@ -0,0 +1,45 @@
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class PgpFileEncryptor : Dino.FileEncryptor, Object {
+
+ StreamInteractor stream_interactor;
+
+ public PgpFileEncryptor(StreamInteractor stream_interactor) {
+ this.stream_interactor = stream_interactor;
+ }
+
+ public bool can_encrypt_file(Conversation conversation, FileTransfer file_transfer) {
+ return conversation.encryption == Encryption.PGP;
+ }
+
+ public FileMeta encrypt_file(Conversation conversation, FileTransfer file_transfer) throws FileSendError {
+ FileMeta file_meta = new FileMeta();
+
+ try {
+ GPG.Key[] keys = stream_interactor.get_module(Manager.IDENTITY).get_key_fprs(conversation);
+ uint8[] enc_content = GPGHelper.encrypt_file(file_transfer.get_file().get_path(), keys, GPG.EncryptFlags.ALWAYS_TRUST, file_transfer.file_name);
+ file_transfer.input_stream = new MemoryInputStream.from_data(enc_content, GLib.free);
+ file_transfer.encryption = Encryption.PGP;
+ file_transfer.server_file_name = Xmpp.random_uuid() + ".pgp";
+ file_meta.size = enc_content.length;
+ } catch (Error e) {
+ throw new FileSendError.ENCRYPTION_FAILED("PGP file encryption error: %s".printf(e.message));
+ }
+ debug("Encrypting file %s as %s", file_transfer.file_name, file_transfer.server_file_name);
+
+ return file_meta;
+ }
+
+ public FileSendData? preprocess_send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) {
+ HttpFileSendData? send_data = file_send_data as HttpFileSendData;
+ if (send_data == null) return null;
+
+ send_data.encrypt_message = false;
+
+ return file_send_data;
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/gpgme_fix.c b/main/src/plugins/openpgp/gpgme_fix.c
new file mode 100644
index 00000000..a351bcbc
--- /dev/null
+++ b/main/src/plugins/openpgp/gpgme_fix.c
@@ -0,0 +1,12 @@
+#include <src/plugins/openpgp/gpgme_fix.h>
+
+GRecMutex gpgme_global_mutex = {0};
+
+gpgme_key_t gpgme_key_ref_vapi (gpgme_key_t key) {
+ gpgme_key_ref(key);
+ return key;
+}
+gpgme_key_t gpgme_key_unref_vapi (gpgme_key_t key) {
+ gpgme_key_unref(key);
+ return key;
+}
diff --git a/main/src/plugins/openpgp/gpgme_fix.h b/main/src/plugins/openpgp/gpgme_fix.h
new file mode 100644
index 00000000..714614fc
--- /dev/null
+++ b/main/src/plugins/openpgp/gpgme_fix.h
@@ -0,0 +1,12 @@
+#ifndef GPGME_FIX
+#define GPGME_FIX 1
+
+#include <glib.h>
+#include <gpgme.h>
+
+extern GRecMutex gpgme_global_mutex;
+
+gpgme_key_t gpgme_key_ref_vapi (gpgme_key_t key);
+gpgme_key_t gpgme_key_unref_vapi (gpgme_key_t key);
+
+#endif
diff --git a/main/src/plugins/openpgp/gpgme_helper.vala b/main/src/plugins/openpgp/gpgme_helper.vala
new file mode 100644
index 00000000..18d07c06
--- /dev/null
+++ b/main/src/plugins/openpgp/gpgme_helper.vala
@@ -0,0 +1,185 @@
+using Gee;
+using GPG;
+
+namespace GPGHelper {
+
+private static bool initialized = false;
+
+public static string encrypt_armor(string plain, Key[] keys, EncryptFlags flags) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Data plain_data = Data.create_from_memory(plain.data, false);
+ Context context = Context.create();
+ context.set_armor(true);
+ Data enc_data = context.op_encrypt(keys, flags, plain_data);
+ return get_string_from_data(enc_data);
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public static uint8[] encrypt_file(string uri, Key[] keys, EncryptFlags flags, string file_name) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Data plain_data = Data.create_from_file(uri);
+ plain_data.set_file_name(file_name);
+ Context context = Context.create();
+ context.set_armor(true);
+ Data enc_data = context.op_encrypt(keys, flags, plain_data);
+ return get_uint8_from_data(enc_data);
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public static string decrypt(string encr) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Data enc_data = Data.create_from_memory(encr.data, false);
+ Context context = Context.create();
+ Data dec_data = context.op_decrypt(enc_data);
+ return get_string_from_data(dec_data);
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public class DecryptedData {
+ public uint8[] data { get; set; }
+ public string filename { get; set; }
+}
+
+public static DecryptedData decrypt_data(uint8[] data) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Data enc_data = Data.create_from_memory(data, false);
+ Context context = Context.create();
+ Data dec_data = context.op_decrypt(enc_data);
+ DecryptResult* dec_res = context.op_decrypt_result();
+ return new DecryptedData() { data=get_uint8_from_data(dec_data), filename=dec_res->file_name};
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public static string sign(string plain, SigMode mode, Key? key = null) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Data plain_data = Data.create_from_memory(plain.data, false);
+ Context context = Context.create();
+ if (key != null) context.signers_add(key);
+ Data signed_data = context.op_sign(plain_data, mode);
+ return get_string_from_data(signed_data);
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public static string? get_sign_key(string signature, string? text) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Data sig_data = Data.create_from_memory(signature.data, false);
+ Data text_data;
+ if (text != null) {
+ text_data = Data.create_from_memory(text.data, false);
+ } else {
+ text_data = Data.create();
+ }
+ Context context = Context.create();
+ context.op_verify(sig_data, text_data);
+ VerifyResult* verify_res = context.op_verify_result();
+ if (verify_res == null || verify_res.signatures == null) return null;
+ return verify_res.signatures.fpr;
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public static Gee.List<Key> get_keylist(string? pattern = null, bool secret_only = false) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+
+ Gee.List<Key> keys = new ArrayList<Key>();
+ Context context = Context.create();
+ context.op_keylist_start(pattern, secret_only ? 1 : 0);
+ try {
+ while (true) {
+ Key key = context.op_keylist_next();
+ keys.add(key);
+ }
+ } catch (Error e) {
+ if (e.code != GPGError.ErrorCode.EOF) throw e;
+ }
+ context.op_keylist_end();
+ return keys;
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+public static Key? get_public_key(string sig) throws GLib.Error {
+ return get_key(sig, false);
+}
+
+public static Key? get_private_key(string sig) throws GLib.Error {
+ return get_key(sig, true);
+}
+
+private static Key? get_key(string sig, bool priv) throws GLib.Error {
+ global_mutex.lock();
+ try {
+ initialize();
+ Context context = Context.create();
+ Key key = context.get_key(sig, priv);
+ return key;
+ } finally {
+ global_mutex.unlock();
+ }
+}
+
+private static string get_string_from_data(Data data) {
+ const size_t BUF_SIZE = 256;
+ data.seek(0);
+ uint8[] buf = new uint8[BUF_SIZE + 1];
+ ssize_t len = 0;
+ string res = "";
+ do {
+ len = data.read(buf, BUF_SIZE);
+ if (len > 0) {
+ buf[len] = 0;
+ res += (string) buf;
+ }
+ } while (len > 0);
+ return res;
+}
+
+private static uint8[] get_uint8_from_data(Data data) {
+ const size_t BUF_SIZE = 256;
+ data.seek(0);
+ uint8[] buf = new uint8[BUF_SIZE + 1];
+ ssize_t len = 0;
+ ByteArray res = new ByteArray();
+ do {
+ len = data.read(buf, BUF_SIZE);
+ if (len > 0) {
+ res.append(buf[0:len]);
+ }
+ } while (len > 0);
+ return res.data;
+}
+
+private static void initialize() {
+ if (!initialized) {
+ check_version();
+ initialized = true;
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/manager.vala b/main/src/plugins/openpgp/manager.vala
new file mode 100644
index 00000000..23cc93c2
--- /dev/null
+++ b/main/src/plugins/openpgp/manager.vala
@@ -0,0 +1,123 @@
+using Gee;
+using Xmpp;
+
+using Xmpp;
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class Manager : StreamInteractionModule, Object {
+ public static ModuleIdentity<Manager> IDENTITY = new ModuleIdentity<Manager>("pgp_manager");
+ public string id { get { return IDENTITY.id; } }
+
+ public const string MESSAGE_ENCRYPTED = "pgp";
+
+ private StreamInteractor stream_interactor;
+ private Database db;
+ private ReceivedMessageListener received_message_listener = new ReceivedMessageListener();
+
+ 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);
+ stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener);
+ stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(check_encypt);
+ }
+
+ public GPG.Key[] get_key_fprs(Conversation conversation) throws Error {
+ Gee.List<string> keys = new Gee.ArrayList<string>();
+ keys.add(db.get_account_key(conversation.account));
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ Gee.List<Jid> muc_jids = new Gee.ArrayList<Jid>();
+ Gee.List<Jid>? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_occupants(conversation.counterpart, conversation.account);
+ if (occupants != null) muc_jids.add_all(occupants);
+ Gee.List<Jid>? offline_members = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account);
+ if (occupants != null) muc_jids.add_all(offline_members);
+
+ foreach (Jid jid in muc_jids) {
+ string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, jid);
+ if (key_id != null && GPGHelper.get_keylist(key_id).size > 0 && !keys.contains(key_id)) {
+ keys.add(key_id);
+ }
+ }
+ } else {
+ string? key_id = get_key_id(conversation.account, conversation.counterpart);
+ if (key_id != null) {
+ keys.add(key_id);
+ }
+ }
+ GPG.Key[] gpgkeys = new GPG.Key[keys.size];
+ for (int i = 0; i < keys.size; i++) {
+ try {
+ GPG.Key key = GPGHelper.get_public_key(keys[i]);
+ if (key != null) gpgkeys[i] = key;
+ } catch (Error e) {}
+ }
+
+ return gpgkeys;
+ }
+
+ private void check_encypt(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
+ try {
+ if (message.encryption == Encryption.PGP) {
+ GPG.Key[] keys = get_key_fprs(conversation);
+ XmppStream? stream = stream_interactor.get_stream(conversation.account);
+ if (stream != null) {
+ bool encrypted = stream.get_module(Module.IDENTITY).encrypt(message_stanza, keys);
+ if (!encrypted) message.marked = Entities.Message.Marked.WONTSEND;
+ }
+ }
+ } catch (Error e) {
+ message.marked = Entities.Message.Marked.WONTSEND;
+ }
+ }
+
+ public string? get_key_id(Account account, Jid jid) {
+ Jid search_jid = stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account) ? jid : jid.bare_jid;
+ Qlite.RowOption key_row = db.get_contact_key_row(search_jid);
+ if (!key_row.is_present()) return null;
+ string? key_id = key_row.get(db.contact_key_table.key);
+ if (key_id != null) return key_id;
+ string? sig = key_row.get(db.contact_key_table.sig);
+ string? signed_data = key_row.get(db.contact_key_table.signed_data);
+ if (sig == null || signed_data == null) return null;
+ key_id = get_sign_key(sig, signed_data);
+ if (key_id != null) {
+ db.set_contact_key(search_jid, key_id);
+ }
+ return key_id;
+ }
+
+ private void on_account_added(Account account) {
+ stream_interactor.module_manager.get_module(account, Module.IDENTITY).received_jid_presence_signature.connect((stream, jid, sig, signed_data) => {
+ on_jid_signature_received(account, jid, sig, signed_data);
+ });
+ }
+
+ private void on_jid_signature_received(Account account, Jid jid, string sig, string signed_data) {
+ Jid set_jid = stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account) ? jid : jid.bare_jid;
+ db.set_contact_signature(set_jid, sig, signed_data);
+ }
+
+ 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; } }
+
+ public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
+ if (MessageFlag.get_flag(stanza) != null && MessageFlag.get_flag(stanza).decrypted) {
+ message.encryption = Encryption.PGP;
+ }
+ return false;
+ }
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/plugin.vala b/main/src/plugins/openpgp/plugin.vala
new file mode 100644
index 00000000..67b94455
--- /dev/null
+++ b/main/src/plugins/openpgp/plugin.vala
@@ -0,0 +1,47 @@
+using Gee;
+
+using Dino.Entities;
+
+namespace Dino.Plugins.OpenPgp {
+
+public class Plugin : Plugins.RootInterface, Object {
+ public Dino.Application app;
+ public Database db;
+ public HashMap<Account, Module> modules = new HashMap<Account, Module>(Account.hash_func, Account.equals_func);
+
+ private EncryptionListEntry list_entry;
+ private ContactDetailsProvider contact_details_provider;
+
+ public void registered(Dino.Application app) {
+ this.app = app;
+ this.db = new Database(Path.build_filename(Application.get_storage_dir(), "pgp.db"));
+ this.list_entry = new EncryptionListEntry(app.stream_interactor, db);
+ this.contact_details_provider = new ContactDetailsProvider(app.stream_interactor);
+
+ app.plugin_registry.register_encryption_list_entry(list_entry);
+ app.plugin_registry.register_encryption_preferences_entry(new PgpPreferencesEntry(this));
+ app.plugin_registry.register_contact_details_entry(contact_details_provider);
+ app.stream_interactor.module_manager.initialize_account_modules.connect(on_initialize_account_modules);
+
+ Manager.start(app.stream_interactor, db);
+ app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new PgpFileEncryptor(app.stream_interactor));
+ app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new PgpFileDecryptor());
+ JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.PGP, new JingleFileEncryptionHelperTransferOnly());
+ }
+
+ public void shutdown() { }
+
+ private void on_initialize_account_modules(Account account, ArrayList<Xmpp.XmppStreamModule> modules) {
+ string signed_data = "";
+ string? sig = db.get_account_signature(account, signed_data);
+ bool sig_was_unknown = sig == null;
+ Module module = new Module(db.get_account_key(account), ref signed_data, ref sig);
+ if (sig_was_unknown && sig != null) {
+ db.set_account_signature(account, signed_data, sig);
+ }
+ this.modules[account] = module;
+ modules.add(module);
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/stream_module.vala b/main/src/plugins/openpgp/stream_module.vala
new file mode 100644
index 00000000..9985df26
--- /dev/null
+++ b/main/src/plugins/openpgp/stream_module.vala
@@ -0,0 +1,159 @@
+using GPG;
+
+using Xmpp;
+
+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 static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0027_current_pgp_usage");
+
+ public signal void received_jid_presence_signature(XmppStream stream, Jid jid, string sig, string signed_data);
+
+ private string? signed_status = null;
+ private Key? own_key = null;
+ private ReceivedPipelineDecryptListener received_pipeline_decrypt_listener = new ReceivedPipelineDecryptListener();
+
+ public Module(string? own_key_id = null, ref string? signed_data, ref string? sig) {
+ set_private_key_id(own_key_id, ref signed_data, ref sig);
+ }
+
+ public void set_private_key_id(string? own_key_id, ref string? signed_data, ref string? sig) {
+ if (own_key_id != null && sig == null) {
+ try {
+ own_key = GPGHelper.get_private_key(own_key_id);
+ if (own_key == null) warning("Can't get PGP private key");
+ } catch (Error e) { }
+ if (own_key != null) {
+ if (signed_data == null) signed_data = "";
+ sig = gpg_sign(signed_data, own_key);
+ }
+ }
+ signed_status = own_key_id != null ? sig : null;
+ }
+
+ public bool encrypt(MessageStanza message, GPG.Key[] keys) {
+ string? enc_body = gpg_encrypt(message.body, keys);
+ 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)]";
+ Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI_ENCRYPTED);
+ return true;
+ }
+ return false;
+ }
+
+ public override void attach(XmppStream 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);
+ stream.get_module(MessageModule.IDENTITY).received_pipeline.connect(received_pipeline_decrypt_listener);
+ }
+
+ 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(MessageModule.IDENTITY).received_pipeline.disconnect(received_pipeline_decrypt_listener);
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return IDENTITY.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) return;
+ string? sig = x_node.get_string_content();
+ if (sig == null) return;
+ string signed_data = presence.status == null ? "" : presence.status;
+ received_jid_presence_signature(stream, presence.from, sig, signed_data);
+ }
+
+ 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 static string? gpg_encrypt(string plain, GPG.Key[] keys) {
+ string encr;
+ try {
+ 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_sign(string str, Key key) {
+ string signed;
+ try {
+ signed = GPGHelper.sign(str, GPG.SigMode.CLEAR, key);
+ } 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 : Xmpp.MessageFlag {
+ public const string id = "pgp";
+
+ public bool decrypted = false;
+
+ public static MessageFlag? get_flag(MessageStanza message) {
+ return (MessageFlag) message.get_flag(NS_URI, id);
+ }
+
+ public override string get_ns() { return NS_URI; }
+ public override string get_id() { return id; }
+ }
+
+public class ReceivedPipelineDecryptListener : StanzaListener<MessageStanza> {
+
+ private string[] after_actions_const = {"MODIFY_BODY"};
+
+ public override string action_group { get { return "ENCRYPT_BODY"; } }
+ public override string[] after_actions { get { return after_actions_const; } }
+
+ public override async bool run(XmppStream stream, MessageStanza message) {
+ string? encrypted = get_cyphertext(message);
+ if (encrypted != null) {
+ MessageFlag flag = new MessageFlag();
+ message.add_flag(flag);
+ string? decrypted = yield gpg_decrypt(encrypted);
+ if (decrypted != null) {
+ flag.decrypted = true;
+ message.body = decrypted;
+ }
+ }
+ return false;
+ }
+
+ private static async string? gpg_decrypt(string enc) {
+ SourceFunc callback = gpg_decrypt.callback;
+ string? res = null;
+ new Thread<void*> (null, () => {
+ string armor = "-----BEGIN PGP MESSAGE-----\n\n" + enc + "\n-----END PGP MESSAGE-----";
+ try {
+ res = GPGHelper.decrypt(armor);
+ } catch (Error e) {
+ res = null;
+ }
+ Idle.add((owned) callback);
+ return null;
+ });
+ yield;
+ return res;
+ }
+
+ private string? get_cyphertext(MessageStanza message) {
+ StanzaNode? x_node = message.stanza.get_subnode("x", NS_URI_ENCRYPTED);
+ return x_node == null ? null : x_node.get_string_content();
+ }
+}
+
+}
diff --git a/main/src/plugins/openpgp/util.vala b/main/src/plugins/openpgp/util.vala
new file mode 100644
index 00000000..3c7a7594
--- /dev/null
+++ b/main/src/plugins/openpgp/util.vala
@@ -0,0 +1,29 @@
+using Gtk;
+
+using Dino.Entities;
+using Xmpp.Util;
+
+namespace Dino.Plugins.OpenPgp {
+
+public static string markup_id(string s, bool is_fingerprint) {
+ string markup = is_fingerprint ? "" : "0x";
+ for (int i = 0; i < s.length; i += 4) {
+ string four_chars = s.substring(i, 4).down();
+
+ if (i == 4 * 5) markup += "\n";
+ markup += four_chars;
+ if (is_fingerprint) markup += " ";
+ }
+ return "<span font_family='monospace' font='9'>" + markup + "</span>";
+}
+
+private static string? get_sign_key(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;
+}
+
+}