From 9ea16b6d8568cb383eb1f469d1dc54bfcad4f188 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 16 Oct 2017 00:23:51 +0200 Subject: [PATCH] PGP encrypted file transfers --- libdino/src/entity/file_transfer.vala | 9 +++ libdino/src/service/file_manager.vala | 45 +++++++++-- .../conversation_summary/file_populator.vala | 4 +- plugins/gpgme-vala/src/gpgme_helper.vala | 43 ++++++++++- plugins/gpgme-vala/vapi/gpgme.vapi | 8 +- plugins/http-files/src/file_provider.vala | 2 +- plugins/http-files/src/manager.vala | 4 +- .../http-files/src/upload_stream_module.vala | 17 +++-- plugins/openpgp/CMakeLists.txt | 2 + plugins/openpgp/src/in_file_processor.vala | 28 +++++++ plugins/openpgp/src/manager.vala | 75 ++++++++++--------- plugins/openpgp/src/out_file_processor.vala | 27 +++++++ plugins/openpgp/src/plugin.vala | 2 + plugins/openpgp/src/stream_module.vala | 13 +--- 14 files changed, 209 insertions(+), 70 deletions(-) create mode 100644 plugins/openpgp/src/in_file_processor.vala create mode 100644 plugins/openpgp/src/out_file_processor.vala diff --git a/libdino/src/entity/file_transfer.vala b/libdino/src/entity/file_transfer.vala index 10fe99f5..e26b723c 100644 --- a/libdino/src/entity/file_transfer.vala +++ b/libdino/src/entity/file_transfer.vala @@ -25,6 +25,11 @@ public class FileTransfer : Object { public OutputStream output_stream { get; set; } public string file_name { get; set; } + private string? server_file_name_ = null; + public string server_file_name { + get { return server_file_name_ ?? file_name; } + set { server_file_name_ = value; } + } public string path { get; set; } public string mime_type { get; set; } public int size { get; set; } @@ -90,6 +95,10 @@ public class FileTransfer : Object { notify.connect(on_update); } + public string get_uri() { + return Path.build_filename(Dino.get_storage_dir(), "files", path); + } + private void on_update(Object o, ParamSpec sp) { Qlite.UpdateBuilder update_builder = db.file_transfer.update().with(db.file_transfer.id, "=", id); switch (sp.name) { diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 6517e9a5..92ca7506 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -16,6 +16,8 @@ public class FileManager : StreamInteractionModule, Object { private StreamInteractor stream_interactor; private Database db; private Gee.List file_senders = new ArrayList(); + private Gee.List incomming_processors = new ArrayList(); + private Gee.List outgoing_processors = new ArrayList(); public static void start(StreamInteractor stream_interactor, Database db) { FileManager m = new FileManager(stream_interactor, db); @@ -46,12 +48,19 @@ public class FileManager : StreamInteractionModule, Object { file_transfer.encryption = Encryption.NONE; file_transfer.file_name = file_info.get_display_name(); file_transfer.input_stream = file.read(); + file_transfer.mime_type = file_info.get_content_type(); file_transfer.size = (int)file_info.get_size(); save_file(file_transfer); file_transfer.persist(db); + foreach (OutgoingFileProcessor processor in outgoing_processors) { + if (processor.can_process(conversation, file_transfer)) { + processor.process(conversation, file_transfer); + } + } + foreach (FileSender file_sender in file_senders) { if (file_sender.can_send(conversation, file_transfer)) { file_sender.send_file(conversation, file_transfer); @@ -88,11 +97,7 @@ public class FileManager : StreamInteractionModule, Object { } public void add_provider(FileProvider file_provider) { - file_provider.file_incoming.connect((file_transfer) => { - save_file(file_transfer); - file_transfer.persist(db); - received_file(file_transfer); - }); + file_provider.file_incoming.connect(handle_incomming_file); } public void add_sender(FileSender file_sender) { @@ -102,6 +107,30 @@ public class FileManager : StreamInteractionModule, Object { }); } + public void add_incomming_processor(IncommingFileProcessor processor) { + incomming_processors.add(processor); + } + + public void add_outgoing_processor(OutgoingFileProcessor processor) { + outgoing_processors.add(processor); + } + + private void handle_incomming_file(FileTransfer file_transfer) { + foreach (IncommingFileProcessor processor in incomming_processors) { + if (processor.can_process(file_transfer)) { + processor.process(file_transfer); + } + } + save_file(file_transfer); + + File file = File.new_for_path(file_transfer.get_uri()); + FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); + file_transfer.mime_type = file_info.get_content_type(); + + file_transfer.persist(db); + received_file(file_transfer); + } + private void save_file(FileTransfer file_transfer) { string filename = Random.next_int().to_string("%x") + "_" + file_transfer.file_name; File file = File.new_for_path(Path.build_filename(get_storage_dir(), filename)); @@ -132,12 +161,12 @@ public interface FileSender : Object { public interface IncommingFileProcessor : Object { public abstract bool can_process(FileTransfer file_transfer); - public abstract FileTransfer process(FileTransfer file_transfer); + public abstract void process(FileTransfer file_transfer); } public interface OutgoingFileProcessor : Object { - public abstract bool can_process(FileTransfer file_transfer); - public abstract FileTransfer process(FileTransfer file_transfer); + public abstract bool can_process(Conversation conversation, FileTransfer file_transfer); + public abstract void process(Conversation conversation, FileTransfer file_transfer); } } diff --git a/main/src/ui/conversation_summary/file_populator.vala b/main/src/ui/conversation_summary/file_populator.vala index d1a26d12..99185f6b 100644 --- a/main/src/ui/conversation_summary/file_populator.vala +++ b/main/src/ui/conversation_summary/file_populator.vala @@ -41,7 +41,7 @@ class FilePopulator : Plugins.ConversationItemPopulator, Object { public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } private void insert_file(FileTransfer transfer) { - if (transfer.mime_type.has_prefix("image")) { + if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) { item_collection.insert_item(new ImageItem(stream_interactor, transfer)); } } @@ -81,7 +81,7 @@ public class ImageItem : Plugins.MetaConversationItem { public override Object get_widget(Plugins.WidgetType widget_type) { Image image = new Image() { halign=Align.START, visible = true }; - Gdk.Pixbuf pixbuf = new Gdk.Pixbuf.from_stream(file_transfer.input_stream); + Gdk.Pixbuf pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_uri()); int max_scaled_height = MAX_HEIGHT * image.scale_factor; if (pixbuf.height > max_scaled_height) { diff --git a/plugins/gpgme-vala/src/gpgme_helper.vala b/plugins/gpgme-vala/src/gpgme_helper.vala index 709b9e0c..cc013164 100644 --- a/plugins/gpgme-vala/src/gpgme_helper.vala +++ b/plugins/gpgme-vala/src/gpgme_helper.vala @@ -19,6 +19,20 @@ public static string encrypt_armor(string plain, Key[] keys, EncryptFlags flags) } } +public static uint8[] encrypt_file(string uri, Key[] keys, EncryptFlags flags) throws GLib.Error { + global_mutex.lock(); + try { + initialize(); + Data plain_data = Data.create_from_file(uri); + 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 { @@ -32,6 +46,19 @@ public static string decrypt(string encr) throws GLib.Error { } } +public static uint8[] 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); + return get_uint8_from_data(dec_data); + } finally { + global_mutex.unlock(); + } +} + public static string sign(string plain, SigMode mode, Key? key = null) throws GLib.Error { global_mutex.lock(); try { @@ -125,6 +152,20 @@ private static string get_string_from_data(Data data) { return res; } +private static uint8[] get_uint8_from_data(Data data) { + data.seek(0); + uint8[] buf = new uint8[256]; + ssize_t? len = null; + Array res = new Array(false, true, 0); + do { + len = data.read(buf); + if (len > 0) { + res.append_vals(buf, (int)len); + } + } while (len > 0); + return res.data; +} + private static void initialize() { if (!initialized) { check_version(); @@ -132,4 +173,4 @@ private static void initialize() { } } -} \ No newline at end of file +} diff --git a/plugins/gpgme-vala/vapi/gpgme.vapi b/plugins/gpgme-vala/vapi/gpgme.vapi index 0b14185c..51823a15 100644 --- a/plugins/gpgme-vala/vapi/gpgme.vapi +++ b/plugins/gpgme-vala/vapi/gpgme.vapi @@ -464,7 +464,13 @@ namespace GPG { } [CCode (cname = "gpgme_data_new_from_file")] - public static GPGError.Error create_from_file(out Data d, string filename, int copy = 1); + public static GPGError.Error new_from_file(out Data d, string filename, int copy = 1); + + public static Data create_from_file(string filename, int copy = 1) { + Data data; + throw_if_error(new_from_file(out data, filename, copy)); + return data; + } [CCode (cname = "gpgme_data_release_and_get_mem")] public string release_and_get_mem(out size_t len); diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala index d327ec5f..9e677a92 100644 --- a/plugins/http-files/src/file_provider.vala +++ b/plugins/http-files/src/file_provider.vala @@ -47,7 +47,7 @@ public class FileProvider : Dino.FileProvider, Object { if (name == "Content-Type") content_type = val; if (name == "Content-Length") content_length = val; }); - if (content_type != null && content_type.has_prefix("image") && content_length != null && int.parse(content_length) < 5000000) { + if (/*content_type != null && content_type.has_prefix("image") &&*/ content_length != null && int.parse(content_length) < 5000000) { Soup.Request request = session.request (message.body); FileTransfer file_transfer = new FileTransfer(); file_transfer.account = conversation.account; diff --git a/plugins/http-files/src/manager.vala b/plugins/http-files/src/manager.vala index 9abf9843..dd168b3d 100644 --- a/plugins/http-files/src/manager.vala +++ b/plugins/http-files/src/manager.vala @@ -23,9 +23,7 @@ public class Manager : StreamInteractionModule, FileSender, Object { public void send_file(Conversation conversation, FileTransfer file_transfer) { Xmpp.Core.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); if (stream != null) { - file_transfer.provider = 0; - uploading(file_transfer); - stream_interactor.module_manager.get_module(file_transfer.account, UploadStreamModule.IDENTITY).upload(stream, Path.build_filename(FileManager.get_storage_dir(), file_transfer.path), + stream_interactor.module_manager.get_module(file_transfer.account, UploadStreamModule.IDENTITY).upload(stream, file_transfer.input_stream, file_transfer.server_file_name, file_transfer.size, file_transfer.mime_type, (stream, url_down) => { uploaded(file_transfer, url_down); stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(url_down, conversation); diff --git a/plugins/http-files/src/upload_stream_module.vala b/plugins/http-files/src/upload_stream_module.vala index c4fa3d2f..ee70e49d 100644 --- a/plugins/http-files/src/upload_stream_module.vala +++ b/plugins/http-files/src/upload_stream_module.vala @@ -14,16 +14,19 @@ public class UploadStreamModule : XmppStreamModule { public delegate void OnUploadOk(XmppStream stream, string url_down); public delegate void OnError(XmppStream stream, string error); - public void upload(XmppStream stream, string file_uri, owned OnUploadOk listener, owned OnError error_listener) { - File file = File.new_for_path(file_uri); - FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); - request_slot(stream, file.get_basename(), (int)file_info.get_size(), file_info.get_content_type(), + public void upload(XmppStream stream, InputStream input_stream, string file_name, int file_size, string file_content_type, owned OnUploadOk listener, owned OnError error_listener) { + request_slot(stream, file_name, file_size, file_content_type, (stream, url_down, url_up) => { - uint8[] data; - FileUtils.get_data(file_uri, out data); + uint8[] buf = new uint8[256]; + Array data = new Array(false, true, 0); + size_t len = -1; + do { + len = input_stream.read(buf); + data.append_vals(buf, (uint) len); + } while(len > 0); Soup.Message message = new Soup.Message("PUT", url_up); - message.set_request(file_info.get_content_type(), Soup.MemoryUse.COPY, data); + message.set_request(file_content_type, Soup.MemoryUse.COPY, data.data); Soup.Session session = new Soup.Session(); session.send_async.begin(message, null, (obj, res) => { try { diff --git a/plugins/openpgp/CMakeLists.txt b/plugins/openpgp/CMakeLists.txt index 32c9ab75..037a4b51 100644 --- a/plugins/openpgp/CMakeLists.txt +++ b/plugins/openpgp/CMakeLists.txt @@ -33,7 +33,9 @@ SOURCES src/contact_details_provider.vala src/database.vala src/encryption_list_entry.vala + src/in_file_processor.vala src/manager.vala + src/out_file_processor.vala src/plugin.vala src/register_plugin.vala src/stream_flag.vala diff --git a/plugins/openpgp/src/in_file_processor.vala b/plugins/openpgp/src/in_file_processor.vala new file mode 100644 index 00000000..2a06bbdf --- /dev/null +++ b/plugins/openpgp/src/in_file_processor.vala @@ -0,0 +1,28 @@ +using Dino.Entities; + +namespace Dino.Plugins.OpenPgp { + +public class InFileProcessor : IncommingFileProcessor, Object { + public bool can_process(FileTransfer file_transfer) { + return file_transfer.file_name.has_suffix("pgp") || file_transfer.mime_type == "application/pgp-encrypted"; + } + + public void process(FileTransfer file_transfer) { + uint8[] buf = new uint8[256]; + Array data = new Array(false, true, 0); + size_t len = -1; + do { + len = file_transfer.input_stream.read(buf); + data.append_vals(buf, (uint) len); + } while(len > 0); + + uint8[] clear_data = GPGHelper.decrypt_data(data.data); + file_transfer.input_stream = new MemoryInputStream.from_data(clear_data, GLib.free); + file_transfer.encryption = Encryption.PGP; + if (file_transfer.file_name.has_suffix(".pgp")) { + file_transfer.file_name = file_transfer.file_name.substring(0, file_transfer.file_name.length - 4); + } + } +} + +} diff --git a/plugins/openpgp/src/manager.vala b/plugins/openpgp/src/manager.vala index 4c8b6d13..74f6027c 100644 --- a/plugins/openpgp/src/manager.vala +++ b/plugins/openpgp/src/manager.vala @@ -30,6 +30,39 @@ public class Manager : StreamInteractionModule, Object { stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(check_encypt); } + public GPG.Key[] get_key_fprs(Conversation conversation) { + Gee.List keys = new Gee.ArrayList(); + keys.add(db.get_account_key(conversation.account)); + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + Gee.List muc_jids = new Gee.ArrayList(); + Gee.List? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_occupants(conversation.counterpart, conversation.account); + if (occupants != null) muc_jids.add_all(occupants); + Gee.List? 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 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; @@ -38,45 +71,13 @@ public class Manager : StreamInteractionModule, Object { private void check_encypt(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { if (message.encryption == Encryption.PGP) { - bool encrypted = false; - if (conversation.type_ == Conversation.Type.CHAT) { - encrypted = encrypt_for_chat(message, message_stanza, conversation); - } else if (conversation.type_ == Conversation.Type.GROUPCHAT) { - encrypted = encrypt_for_groupchat(message, message_stanza, conversation); - } - if (!encrypted) message.marked = Entities.Message.Marked.WONTSEND; - } - } - - private bool encrypt_for_chat(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { - Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (stream == null) return false; - - string? key_id = get_key_id(conversation.account, message.counterpart); - if (key_id != null) { - return stream.get_module(Module.IDENTITY).encrypt(message_stanza, new Gee.ArrayList.wrap(new string[]{key_id})); - } - return false; - } - - private bool encrypt_for_groupchat(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { - Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (stream == null) return false; - - Gee.List muc_jids = new Gee.ArrayList(); - Gee.List? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_occupants(conversation.counterpart, conversation.account); - if (occupants != null) muc_jids.add_all(occupants); - Gee.List? offline_members = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account); - if (occupants != null) muc_jids.add_all(offline_members); - - Gee.List keys = new Gee.ArrayList(); - 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); + GPG.Key[] keys = get_key_fprs(conversation); + Core.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; } } - return stream.get_module(Module.IDENTITY).encrypt(message_stanza, keys); } public string? get_key_id(Account account, Jid jid) { diff --git a/plugins/openpgp/src/out_file_processor.vala b/plugins/openpgp/src/out_file_processor.vala new file mode 100644 index 00000000..81c53b16 --- /dev/null +++ b/plugins/openpgp/src/out_file_processor.vala @@ -0,0 +1,27 @@ +using Dino.Entities; + +namespace Dino.Plugins.OpenPgp { + +public class OutFileProcessor : OutgoingFileProcessor, Object { + + StreamInteractor stream_interactor; + + public OutFileProcessor(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public bool can_process(Conversation conversation, FileTransfer file_transfer) { + return conversation.encryption == Encryption.PGP; + } + + public void process(Conversation conversation, FileTransfer file_transfer) { + string uri = file_transfer.get_uri(); + GPG.Key[] keys = stream_interactor.get_module(Manager.IDENTITY).get_key_fprs(conversation); + uint8[] enc_content = GPGHelper.encrypt_file(uri, keys, GPG.EncryptFlags.ALWAYS_TRUST); + file_transfer.input_stream = new MemoryInputStream.from_data(enc_content, GLib.free); + file_transfer.encryption = Encryption.PGP; + file_transfer.server_file_name = file_transfer.server_file_name + ".pgp"; + } +} + +} diff --git a/plugins/openpgp/src/plugin.vala b/plugins/openpgp/src/plugin.vala index 2f664656..7ec6c357 100644 --- a/plugins/openpgp/src/plugin.vala +++ b/plugins/openpgp/src/plugin.vala @@ -29,6 +29,8 @@ public class Plugin : Plugins.RootInterface, Object { 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_outgoing_processor(new OutFileProcessor(app.stream_interactor)); + app.stream_interactor.get_module(FileManager.IDENTITY).add_incomming_processor(new InFileProcessor()); internationalize(GETTEXT_PACKAGE, app.search_path_generator.get_locale_path(GETTEXT_PACKAGE, LOCALE_INSTALL_DIR)); } diff --git a/plugins/openpgp/src/stream_module.vala b/plugins/openpgp/src/stream_module.vala index 6c55cdc5..068370fd 100644 --- a/plugins/openpgp/src/stream_module.vala +++ b/plugins/openpgp/src/stream_module.vala @@ -33,11 +33,8 @@ namespace Dino.Plugins.OpenPgp { } } - public bool encrypt(Message.Stanza message, Gee.List fprs) { - string[] encrypt_to = new string[fprs.size + 1]; - for (int i = 0; i < fprs.size; i++) encrypt_to[i] = fprs[i]; - encrypt_to[encrypt_to.length - 1] = own_key.fpr; - string? enc_body = gpg_encrypt(message.body, encrypt_to); + public bool encrypt(Message.Stanza 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)]"; @@ -105,13 +102,9 @@ namespace Dino.Plugins.OpenPgp { } } - private static string? gpg_encrypt(string plain, string[] key_ids) { - GPG.Key[] keys = new GPG.Key[key_ids.length]; + private static string? gpg_encrypt(string plain, GPG.Key[] keys) { 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;