diff options
Diffstat (limited to 'main/src/plugins/openpgp')
| -rw-r--r-- | main/src/plugins/openpgp/contact_details_provider.vala | 51 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/database.vala | 137 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/encryption_list_entry.vala | 72 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/encryption_preferences_entry.vala | 82 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/file_transfer/file_decryptor.vala | 50 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/file_transfer/file_encryptor.vala | 45 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/gpgme_fix.c | 12 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/gpgme_fix.h | 12 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/gpgme_helper.vala | 185 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/manager.vala | 123 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/plugin.vala | 47 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/stream_module.vala | 159 | ||||
| -rw-r--r-- | main/src/plugins/openpgp/util.vala | 29 |
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; +} + +} |
