diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index e0a417ee..841a6b53 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -71,45 +71,44 @@ public class FileManager : StreamInteractionModule, Object { file_meta.size = file_transfer.size; file_meta.mime_type = file_transfer.mime_type; - bool encrypted = false; - foreach (FileEncryptor file_encryptor in file_encryptors) { - if (file_encryptor.can_encrypt_file(conversation, file_transfer)) { - file_meta = file_encryptor.encrypt_file(conversation, file_transfer); - encrypted = true; - break; - } - } - if (conversation.encryption != Encryption.NONE && !encrypted) { - throw new FileSendError.ENCRYPTION_FAILED("File was not encrypted"); - } - - FileSendData file_send_data = null; - foreach (FileSender file_sender in file_senders) { - if (file_sender.can_send(conversation, file_transfer)) { - file_send_data = yield file_sender.prepare_send_file(conversation, file_transfer, file_meta); - break; + FileSender file_sender = null; + FileEncryptor file_encryptor = null; + foreach (FileSender sender in file_senders) { + if (sender.can_send(conversation, file_transfer)) { + if (file_transfer.encryption == Encryption.NONE || sender.can_encrypt(conversation, file_transfer)) { + file_sender = sender; + break; + } else { + foreach (FileEncryptor encryptor in file_encryptors) { + if (encryptor.can_encrypt_file(conversation, file_transfer)) { + file_encryptor = encryptor; + break; + } + } + if (file_encryptor != null) { + file_sender = sender; + break; + } + } } } - foreach (FileEncryptor file_encryptor in file_encryptors) { - if (file_encryptor.can_encrypt_file(conversation, file_transfer)) { - file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta); - break; - } + if (file_sender == null) { + throw new FileSendError.UPLOAD_FAILED("No sender/encryptor combination available"); } - bool sent = false; - foreach (FileSender file_sender in file_senders) { - if (file_sender.can_send(conversation, file_transfer)) { - yield file_sender.send_file(conversation, file_transfer, file_send_data); - sent = true; - break; - } + if (file_encryptor != null) { + file_meta = file_encryptor.encrypt_file(conversation, file_transfer); } - if (!sent) { - throw new FileSendError.UPLOAD_FAILED("File was not sent"); + + FileSendData file_send_data = yield file_sender.prepare_send_file(conversation, file_transfer, file_meta); + + if (file_encryptor != null) { + file_send_data = file_encryptor.preprocess_send_file(conversation, file_transfer, file_send_data, file_meta); } + yield file_sender.send_file(conversation, file_transfer, file_send_data, file_meta); + conversation.last_active = file_transfer.time; } catch (Error e) { warning("Send file error: %s", e.message); @@ -130,7 +129,9 @@ public class FileManager : StreamInteractionModule, Object { yield download_file_internal(file_provider, file_transfer, conversation); } - public bool is_upload_available(Conversation conversation) { + public bool is_upload_available(Conversation? conversation) { + if (conversation == null) return false; + foreach (FileSender file_sender in file_senders) { if (file_sender.is_upload_available(conversation)) return true; } @@ -230,12 +231,18 @@ public class FileManager : StreamInteractionModule, Object { try { // Get meta info FileReceiveData receive_data = file_provider.get_file_receive_data(file_transfer); - foreach (FileDecryptor file_decryptor in file_decryptors) { - if (file_decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { - receive_data = file_decryptor.prepare_get_meta_info(conversation, file_transfer, receive_data); + FileDecryptor? file_decryptor = null; + foreach (FileDecryptor decryptor in file_decryptors) { + if (decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { + file_decryptor = decryptor; break; } } + + if (file_decryptor != null) { + receive_data = file_decryptor.prepare_get_meta_info(conversation, file_transfer, receive_data); + } + FileMeta file_meta = yield get_file_meta(file_provider, file_transfer, conversation, receive_data); @@ -244,34 +251,21 @@ public class FileManager : StreamInteractionModule, Object { // Download and decrypt file file_transfer.state = FileTransfer.State.IN_PROGRESS; - foreach (FileDecryptor file_decryptor in file_decryptors) { - if (file_decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { - file_meta = file_decryptor.prepare_download_file(conversation, file_transfer, receive_data, file_meta); - break; - } + if (file_decryptor != null) { + file_meta = file_decryptor.prepare_download_file(conversation, file_transfer, receive_data, file_meta); } input_stream = yield file_provider.download(file_transfer, receive_data, file_meta); - - foreach (FileDecryptor file_decryptor in file_decryptors) { - if (file_decryptor.can_decrypt_file(conversation, file_transfer, receive_data)) { - input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data); - break; - } + if (file_decryptor != null) { + input_stream = yield file_decryptor.decrypt_file(input_stream, conversation, file_transfer, receive_data); } // Save file - string filename = Random.next_int().to_string("%x") + "_" + file_meta.file_name; + 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)); - if (file_transfer.encryption == Encryption.PGP || file.get_path().has_suffix(".pgp")) { - file = File.new_for_path(file.get_path().substring(0, file.get_path().length - 4)); - } - OutputStream os = file.create(FileCreateFlags.REPLACE_DESTINATION); yield os.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET); - file_transfer.size = (int)file_meta.size; - file_transfer.file_name = file_meta.file_name; file_transfer.path = file.get_basename(); file_transfer.input_stream = yield file.read_async(); @@ -392,7 +386,8 @@ public interface FileSender : Object { public abstract bool is_upload_available(Conversation conversation); public abstract bool can_send(Conversation conversation, FileTransfer file_transfer); public abstract async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError; - public abstract async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data) throws FileSendError; + public abstract async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError; + public abstract bool can_encrypt(Conversation conversation, FileTransfer file_transfer); public abstract int get_id(); public abstract float get_priority(); diff --git a/libdino/src/service/jingle_file_transfers.vala b/libdino/src/service/jingle_file_transfers.vala index 0a93979b..182213bb 100644 --- a/libdino/src/service/jingle_file_transfers.vala +++ b/libdino/src/service/jingle_file_transfers.vala @@ -6,6 +6,56 @@ using Dino.Entities; namespace Dino { +public interface JingleFileEncryptionHelper : Object { + public abstract bool can_transfer(Conversation conversation); + public abstract bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid = null); + public abstract string? get_precondition_name(Conversation conversation, FileTransfer file_transfer); + public abstract Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer); + public abstract FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer); +} + +public class JingleFileEncryptionHelperTransferOnly : JingleFileEncryptionHelper, Object { + public bool can_transfer(Conversation conversation) { + return true; + } + public bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid) { + return false; + } + public string? get_precondition_name(Conversation conversation, FileTransfer file_transfer) { + return null; + } + public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) { + return null; + } + public FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) { + return file_meta; + } +} + +public class JingleFileHelperRegistry { + private static JingleFileHelperRegistry INSTANCE; + public static JingleFileHelperRegistry instance { get { + if (INSTANCE == null) { + INSTANCE = new JingleFileHelperRegistry(); + INSTANCE.add_encryption_helper(Encryption.NONE, new JingleFileEncryptionHelperTransferOnly()); + } + return INSTANCE; + } } + + internal HashMap encryption_helpers = new HashMap(); + + public void add_encryption_helper(Encryption encryption, JingleFileEncryptionHelper helper) { + encryption_helpers[encryption] = helper; + } + + public JingleFileEncryptionHelper? get_encryption_helper(Encryption encryption) { + if (encryption_helpers.has_key(encryption)) { + return encryption_helpers[encryption]; + } + return null; + } +} + public class JingleFileProvider : FileProvider, Object { private StreamInteractor stream_interactor; @@ -29,7 +79,15 @@ public class JingleFileProvider : FileProvider, Object { } public async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError { - return file_meta; + Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info]; + if (jingle_file_transfer == null) { + throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore"); + } + FileMeta meta = file_meta; + foreach (JingleFileEncryptionHelper helper in JingleFileHelperRegistry.instance.encryption_helpers.values) { + meta = helper.complete_meta(file_transfer, receive_data, meta, jingle_file_transfer); + } + return meta; } public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError { @@ -39,6 +97,9 @@ public class JingleFileProvider : FileProvider, Object { if (jingle_file_transfer == null) { throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore"); } + foreach (JingleFileEncryptionHelper helper in JingleFileHelperRegistry.instance.encryption_helpers.values) { + helper.complete_meta(file_transfer, receive_data, file_meta, jingle_file_transfer); + } try { jingle_file_transfer.accept(stream); } catch (IOError e) { @@ -83,6 +144,10 @@ public class JingleFileSender : FileSender, Object { } public bool is_upload_available(Conversation conversation) { + JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(conversation.encryption); + if (helper == null) return false; + if (!helper.can_transfer(conversation)) return false; + XmppStream? stream = stream_interactor.get_stream(conversation.account); if (stream == null) return false; @@ -98,32 +163,46 @@ public class JingleFileSender : FileSender, Object { } public bool can_send(Conversation conversation, FileTransfer file_transfer) { - if (conversation.encryption != Encryption.NONE) return false; + // No file specific restrictions apply to Jingle file transfers + return is_upload_available(conversation); + } - XmppStream? stream = stream_interactor.get_stream(file_transfer.account); - if (stream == null) return false; - - foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) { - if (stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) { - return true; - } - } - return false; + public bool can_encrypt(Conversation conversation, FileTransfer file_transfer) { + JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption); + if (helper == null) return false; + return helper.can_encrypt(conversation, file_transfer); } public async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError { + if (file_meta is HttpFileMeta) { + throw new FileSendError.UPLOAD_FAILED("Cannot upload http file meta over Jingle"); + } return new FileSendData(); } - public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data) throws FileSendError { - // TODO(hrxi) What should happen if `stream == null`? + public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError { XmppStream? stream = stream_interactor.get_stream(file_transfer.account); + if (stream == null) throw new FileSendError.UPLOAD_FAILED("No stream available"); + JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption); + bool must_encrypt = helper != null && helper.can_encrypt(conversation, file_transfer); foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) { // TODO(hrxi): Prioritization of transports (and resources?). if (!stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) { continue; } - stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).offer_file_stream.begin(stream, full_jid, file_transfer.input_stream, file_transfer.file_name, file_transfer.size); + if (must_encrypt && !helper.can_encrypt(conversation, file_transfer, full_jid)) { + continue; + } + string? precondition_name = null; + Object? precondition_options = null; + if (must_encrypt) { + precondition_name = helper.get_precondition_name(conversation, file_transfer); + precondition_options = helper.get_precondition_options(conversation, file_transfer); + if (precondition_name == null) { + throw new FileSendError.ENCRYPTION_FAILED("Should have created a precondition, but did not"); + } + } + yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).offer_file_stream(stream, full_jid, file_transfer.input_stream, file_transfer.server_file_name, file_meta.size, precondition_name, precondition_options); return; } } diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index 6a07a146..2b1567a0 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -84,6 +84,7 @@ public class ModuleManager { module_map[account].add(new Xep.JingleSocks5Bytestreams.Module()); module_map[account].add(new Xep.JingleInBandBytestreams.Module()); module_map[account].add(new Xep.JingleFileTransfer.Module()); + module_map[account].add(new Xep.Jet.Module()); initialize_account_modules(account, module_map[account]); } } diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala index 74fa044e..24297e6a 100644 --- a/main/src/ui/chat_input/view.vala +++ b/main/src/ui/chat_input/view.vala @@ -57,6 +57,7 @@ public class View : Box { scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); encryption_widget.get_style_context().add_class("dino-chatinput-button"); + encryption_widget.encryption_changed.connect(update_file_transfer_availability); // Emoji button for emoji picker (recents don't work < 3.22.19, category icons don't work <3.23.2) if (Gtk.get_major_version() >= 3 && Gtk.get_minor_version() >= 24) { @@ -83,15 +84,17 @@ public class View : Box { return this; } - public void initialize_for_conversation(Conversation conversation) { - - - if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text; - this.conversation = conversation; - + private void update_file_transfer_availability() { bool upload_available = stream_interactor.get_module(FileManager.IDENTITY).is_upload_available(conversation); file_button.visible = upload_available; file_separator.visible = upload_available; + } + + public void initialize_for_conversation(Conversation conversation) { + if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text; + this.conversation = conversation; + + update_file_transfer_availability(); text_input.buffer.text = ""; if (entry_cache.has_key(conversation)) { diff --git a/plugins/gpgme-vala/src/gpgme_helper.vala b/plugins/gpgme-vala/src/gpgme_helper.vala index c0121842..4a6d94fa 100644 --- a/plugins/gpgme-vala/src/gpgme_helper.vala +++ b/plugins/gpgme-vala/src/gpgme_helper.vala @@ -163,11 +163,11 @@ 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); + ByteArray res = new ByteArray(); do { len = data.read(buf); if (len > 0) { - res.append_vals(buf, (int)len); + res.append(buf[0:len]); } } while (len > 0); return res.data; diff --git a/plugins/http-files/src/file_sender.vala b/plugins/http-files/src/file_sender.vala index 3ab0d736..65b33eb4 100644 --- a/plugins/http-files/src/file_sender.vala +++ b/plugins/http-files/src/file_sender.vala @@ -35,11 +35,11 @@ public class HttpFileSender : FileSender, Object { return send_data; } - public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data) throws FileSendError { + public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError { HttpFileSendData? send_data = file_send_data as HttpFileSendData; if (send_data == null) return; - yield upload(file_transfer, send_data); + yield upload(file_transfer, send_data, file_meta); file_transfer.info = send_data.url_down; // store the message content temporarily so the message gets filtered out @@ -62,6 +62,10 @@ public class HttpFileSender : FileSender, Object { return file_transfer.size < max_file_sizes[conversation.account]; } + public bool can_encrypt(Conversation conversation, FileTransfer file_transfer) { + return false; + } + public bool is_upload_available(Conversation conversation) { lock (max_file_sizes) { return max_file_sizes.has_key(conversation.account); @@ -74,24 +78,27 @@ public class HttpFileSender : FileSender, Object { } } - private async void upload(FileTransfer file_transfer, HttpFileSendData file_send_data) throws FileSendError { + private static void transfer_more_bytes(InputStream stream, Soup.MessageBody body) { + uint8[] bytes = new uint8[4096]; + ssize_t read = stream.read(bytes); + if (read == 0) { + body.complete(); + return; + } + bytes.length = (int)read; + body.append_buffer(new Soup.Buffer.take(bytes)); + } + + private async void upload(FileTransfer file_transfer, HttpFileSendData file_send_data, FileMeta file_meta) throws FileSendError { Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); if (stream == null) return; - uint8[] buf = new uint8[256]; - Array data = new Array(false, true, 0); - size_t len = -1; - do { - try { - len = file_transfer.input_stream.read(buf); - } catch (IOError e) { - throw new FileSendError.UPLOAD_FAILED("HTTP upload: IOError reading stream: %s".printf(e.message)); - } - data.append_vals(buf, (uint) len); - } while(len > 0); - Soup.Message message = new Soup.Message("PUT", file_send_data.url_up); - message.set_request(file_transfer.mime_type, Soup.MemoryUse.COPY, data.data); + message.request_headers.set_content_type(file_meta.mime_type, null); + message.request_headers.set_content_length(file_meta.size); + message.request_body.set_accumulate(false); + message.wrote_headers.connect(() => transfer_more_bytes(file_transfer.input_stream, message.request_body)); + message.wrote_chunk.connect(() => transfer_more_bytes(file_transfer.input_stream, message.request_body)); Soup.Session session = new Soup.Session(); try { yield session.send_async(message); diff --git a/plugins/omemo/src/file_transfer/file_decryptor.vala b/plugins/omemo/src/file_transfer/file_decryptor.vala index bc6f8592..d60ecdc8 100644 --- a/plugins/omemo/src/file_transfer/file_decryptor.vala +++ b/plugins/omemo/src/file_transfer/file_decryptor.vala @@ -1,5 +1,6 @@ using Dino.Entities; +using Crypto; using Signal; namespace Dino.Plugins.Omemo { @@ -56,20 +57,17 @@ public class OmemoFileDecryptor : FileDecryptor, Object { key = iv_and_key[16:48]; } - // Read data - uint8[] buf = new uint8[256]; - Array data = new Array(false, true, 0); - size_t len = -1; - do { - len = yield encrypted_stream.read_async(buf); - data.append_vals(buf, (uint) len); - } while(len > 0); - - // Decrypt - uint8[] cleartext = Signal.aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data); file_transfer.encryption = Encryption.OMEMO; - return new MemoryInputStream.from_data(cleartext); - } catch (Error e) { + debug("Decrypting file %s from %s", file_transfer.file_name, file_transfer.server_file_name); + + SymmetricCipher cipher = new SymmetricCipher("AES-GCM"); + cipher.set_key(key); + cipher.set_iv(iv); + return new ConverterInputStream(encrypted_stream, new SymmetricCipherDecrypter((owned) cipher)); + + } catch (Crypto.Error e) { + throw new FileReceiveError.DECRYPTION_FAILED("OMEMO file decryption error: %s".printf(e.message)); + } catch (GLib.Error e) { throw new FileReceiveError.DECRYPTION_FAILED("OMEMO file decryption error: %s".printf(e.message)); } } diff --git a/plugins/omemo/src/file_transfer/file_encryptor.vala b/plugins/omemo/src/file_transfer/file_encryptor.vala index a5445153..5b4e4d96 100644 --- a/plugins/omemo/src/file_transfer/file_encryptor.vala +++ b/plugins/omemo/src/file_transfer/file_encryptor.vala @@ -1,6 +1,7 @@ using Gee; using Gtk; +using Crypto; using Dino.Entities; using Xmpp; using Signal; @@ -22,30 +23,29 @@ public class OmemoFileEncryptor : Dino.FileEncryptor, Object { var omemo_http_file_meta = new OmemoHttpFileMeta(); try { - 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); - //Create a key and use it to encrypt the file uint8[] iv = new uint8[16]; Plugin.get_context().randomize(iv); uint8[] key = new uint8[32]; Plugin.get_context().randomize(key); - uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, data.data); + + SymmetricCipher cipher = new SymmetricCipher("AES-GCM"); + cipher.set_key(key); + cipher.set_iv(iv); omemo_http_file_meta.iv = iv; omemo_http_file_meta.key = key; - omemo_http_file_meta.size = ciphertext.length; - omemo_http_file_meta.mime_type = "pgp"; - file_transfer.input_stream = new MemoryInputStream.from_data(ciphertext, GLib.free); - } catch (Error error) { - throw new FileSendError.ENCRYPTION_FAILED("HTTP upload: Error encrypting stream: %s".printf(error.message)); + omemo_http_file_meta.size = file_transfer.size; + omemo_http_file_meta.mime_type = "omemo"; + file_transfer.input_stream = new ConverterInputStream(file_transfer.input_stream, new SymmetricCipherEncrypter((owned) cipher)); + } catch (Crypto.Error error) { + throw new FileSendError.ENCRYPTION_FAILED("OMEMO file encryption error: %s".printf(error.message)); + } catch (GLib.Error error) { + throw new FileSendError.ENCRYPTION_FAILED("OMEMO file encryption error: %s".printf(error.message)); } + debug("Encrypting file %s as %s", file_transfer.file_name, file_transfer.server_file_name); + return omemo_http_file_meta; } diff --git a/plugins/openpgp/CMakeLists.txt b/plugins/openpgp/CMakeLists.txt index eb50dc71..3f7c1974 100644 --- a/plugins/openpgp/CMakeLists.txt +++ b/plugins/openpgp/CMakeLists.txt @@ -53,7 +53,7 @@ GRESOURCES ${OPENPGP_GRESOURCES_XML} ) -add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\") +add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="OpenPGP" -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\") add_library(openpgp SHARED ${OPENPGP_VALA_C} ${OPENPGP_GRESOURCES_TARGET}) add_dependencies(openpgp ${GETTEXT_PACKAGE}-translations) target_link_libraries(openpgp libdino gpgme-vala ${OPENPGP_PACKAGES}) diff --git a/plugins/openpgp/src/file_transfer/file_decryptor.vala b/plugins/openpgp/src/file_transfer/file_decryptor.vala index 97eb9f43..455f853d 100644 --- a/plugins/openpgp/src/file_transfer/file_decryptor.vala +++ b/plugins/openpgp/src/file_transfer/file_decryptor.vala @@ -19,18 +19,20 @@ public class PgpFileDecryptor : FileDecryptor, Object { public async InputStream decrypt_file(InputStream encrypted_stream, Conversation conversation, FileTransfer file_transfer, FileReceiveData receive_data) throws FileReceiveError { try { uint8[] buf = new uint8[256]; - Array data = new Array(false, true, 0); + ByteArray data = new ByteArray(); size_t len = -1; do { - len = encrypted_stream.read(buf); - data.append_vals(buf, (uint) len); + 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); diff --git a/plugins/openpgp/src/file_transfer/file_encryptor.vala b/plugins/openpgp/src/file_transfer/file_encryptor.vala index 7d51be60..66e93bd9 100644 --- a/plugins/openpgp/src/file_transfer/file_encryptor.vala +++ b/plugins/openpgp/src/file_transfer/file_encryptor.vala @@ -15,17 +15,21 @@ public class PgpFileEncryptor : Dino.FileEncryptor, Object { } 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 new FileMeta(); + return file_meta; } public FileSendData? preprocess_send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) { diff --git a/plugins/openpgp/src/plugin.vala b/plugins/openpgp/src/plugin.vala index b4581f31..324b8652 100644 --- a/plugins/openpgp/src/plugin.vala +++ b/plugins/openpgp/src/plugin.vala @@ -31,6 +31,7 @@ public class Plugin : Plugins.RootInterface, Object { 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()); internationalize(GETTEXT_PACKAGE, app.search_path_generator.get_locale_path(GETTEXT_PACKAGE, LOCALE_INSTALL_DIR)); } diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index e0f01723..b945f9d0 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -78,6 +78,7 @@ SOURCES "src/module/xep/0363_http_file_upload.vala" "src/module/xep/0368_srv_records_tls.vala" "src/module/xep/0380_explicit_encryption.vala" + "src/module/xep/0391_jingle_encrypted_transports.vala" "src/module/xep/pixbuf_storage.vala" "src/util.vala" @@ -95,7 +96,7 @@ DEPENDS ${CMAKE_BINARY_DIR}/exports/xmpp-vala.deps ) -add_definitions(${VALA_CFLAGS}) +add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="xmpp-vala") add_library(xmpp-vala SHARED ${ENGINE_VALA_C}) add_dependencies(xmpp-vala xmpp-vala-vapi) target_link_libraries(xmpp-vala ${ENGINE_PACKAGES}) diff --git a/xmpp-vala/src/module/xep/0166_jingle.vala b/xmpp-vala/src/module/xep/0166_jingle.vala index 86396f30..85663929 100644 --- a/xmpp-vala/src/module/xep/0166_jingle.vala +++ b/xmpp-vala/src/module/xep/0166_jingle.vala @@ -43,6 +43,7 @@ public errordomain Error { BAD_REQUEST, INVALID_PARAMETERS, UNSUPPORTED_TRANSPORT, + UNSUPPORTED_SECURITY, NO_SHARED_PROTOCOLS, TRANSPORT_ERROR, } @@ -69,6 +70,7 @@ class ContentNode { public string name; public StanzaNode? description; public StanzaNode? transport; + public StanzaNode? security; } ContentNode get_single_content_node(StanzaNode jingle) throws IqError { @@ -94,6 +96,7 @@ ContentNode get_single_content_node(StanzaNode jingle) throws IqError { string? name = content.get_attribute("name"); StanzaNode? description = get_single_node_anyns(content, "description"); StanzaNode? transport = get_single_node_anyns(content, "transport"); + StanzaNode? security = get_single_node_anyns(content, "security"); if (name == null || creator == null) { throw new IqError.BAD_REQUEST("missing name or creator"); } @@ -102,7 +105,8 @@ ContentNode get_single_content_node(StanzaNode jingle) throws IqError { creator=creator, name=name, description=description, - transport=transport + transport=transport, + security=security }; } @@ -112,6 +116,7 @@ public class Module : XmppStreamModule, Iq.Handler { private HashMap content_types = new HashMap(); private HashMap transports = new HashMap(); + private HashMap security_preconditions = new HashMap(); private XmppStream? current_stream = null; @@ -163,6 +168,16 @@ public class Module : XmppStreamModule, Iq.Handler { } return result; } + public void register_security_precondition(SecurityPrecondition precondition) { + security_preconditions[precondition.security_ns_uri()] = precondition; + } + public SecurityPrecondition? get_security_precondition(string? ns_uri) { + if (ns_uri == null) return null; + if (!security_preconditions.has_key(ns_uri)) { + return null; + } + return security_preconditions[ns_uri]; + } private bool is_jingle_available(XmppStream stream, Jid full_jid) { bool? has_jingle = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI); @@ -173,7 +188,7 @@ public class Module : XmppStreamModule, Iq.Handler { return is_jingle_available(stream, full_jid) && select_transport(stream, type, full_jid, Set.empty()) != null; } - public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description) throws Error { + public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description, string? precondition_name = null, Object? precondation_options = null) throws Error { if (!is_jingle_available(stream, receiver_full_jid)) { throw new Error.NO_SHARED_PROTOCOLS("No Jingle support"); } @@ -181,18 +196,26 @@ public class Module : XmppStreamModule, Iq.Handler { if (transport == null) { throw new Error.NO_SHARED_PROTOCOLS("No suitable transports"); } + SecurityPrecondition? precondition = get_security_precondition(precondition_name); + if (precondition_name != null && precondition == null) { + throw new Error.UNSUPPORTED_SECURITY("No suitable security precondiiton found"); + } Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; if (my_jid == null) { throw new Error.GENERAL("Couldn't determine own JID"); } TransportParameters transport_params = transport.create_transport_parameters(stream, my_jid, receiver_full_jid); - Session session = new Session.initiate_sent(random_uuid(), type, transport_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session); + SecurityParameters? security_params = precondition != null ? precondition.create_security_parameters(stream, my_jid, receiver_full_jid, precondation_options) : null; + Session session = new Session.initiate_sent(random_uuid(), type, transport_params, security_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session); StanzaNode content = new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content_name) .put_attribute("senders", senders.to_string()) .put_node(description) .put_node(transport_params.to_transport_stanza_node()); + if (security_params != null) { + content.put_node(security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); + } StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) .add_self_xmlns() .put_attribute("action", "session-initiate") @@ -233,8 +256,17 @@ public class Module : XmppStreamModule, Iq.Handler { } ContentParameters content_params = content_type.parse_content_parameters(content.description); + SecurityPrecondition? precondition = content.security != null ? get_security_precondition(content.security.ns_uri) : null; + SecurityParameters? security_params = null; + if (precondition != null) { + debug("Using precondition %s", precondition.security_ns_uri()); + security_params = precondition.parse_security_parameters(stream, my_jid, iq.from, content.security); + } else if (content.security != null) { + throw new IqError.NOT_IMPLEMENTED("unknown security precondition"); + } + TransportType type = content_type.content_type_transport_type(); - Session session = new Session.initiate_received(sid, type, transport_params, my_jid, iq.from, content.name, send_terminate_and_remove_session); + Session session = new Session.initiate_received(sid, type, transport_params, security_params, my_jid, iq.from, content.name, send_terminate_and_remove_session); stream.get_flag(Flag.IDENTITY).add_session(session); stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); @@ -328,7 +360,7 @@ public interface Transport : Object { public abstract bool is_transport_available(XmppStream stream, Jid full_jid); public abstract TransportType transport_type(); public abstract int transport_priority(); - public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid); + public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) throws Error; public abstract TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws IqError; } @@ -375,6 +407,17 @@ public interface ContentParameters : Object { public abstract void on_session_initiate(XmppStream stream, Session session); } +public interface SecurityPrecondition : Object { + public abstract string security_ns_uri(); + public abstract SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error; + public abstract SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError; +} + +public interface SecurityParameters : Object { + public abstract string security_ns_uri(); + public abstract StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid); + public abstract IOStream wrap_stream(IOStream stream); +} public class Session { // INITIATE_SENT -> CONNECTING -> [REPLACING_TRANSPORT -> CONNECTING ->]... ACTIVE -> ENDED @@ -398,6 +441,7 @@ public class Session { public Jid peer_full_jid { get; private set; } public Role content_creator { get; private set; } public string content_name { get; private set; } + public SecurityParameters? security { get; private set; } private Connection connection; public IOStream conn { get { return connection; } } @@ -410,7 +454,7 @@ public class Session { SessionTerminate session_terminate_handler; - public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { + public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { this.state = State.INITIATE_SENT; this.role = Role.INITIATOR; this.sid = sid; @@ -422,12 +466,13 @@ public class Session { this.tried_transport_methods = new HashSet(); this.tried_transport_methods.add(transport.transport_ns_uri()); this.transport = transport; + this.security = security; this.connection = new Connection(this); this.session_terminate_handler = (owned)session_terminate_handler; this.terminate_on_connection_close = true; } - public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { + public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { this.state = State.INITIATE_RECEIVED; this.role = Role.RESPONDER; this.sid = sid; @@ -437,6 +482,7 @@ public class Session { this.content_creator = Role.INITIATOR; this.content_name = content_name; this.transport = transport; + this.security = security; this.tried_transport_methods = new HashSet(); if (transport != null) { this.tried_transport_methods.add(transport.transport_ns_uri()); @@ -557,7 +603,12 @@ public class Session { state = State.ACTIVE; transport = null; tried_transport_methods.clear(); - connection.set_inner(conn); + if (security != null) { + connection.set_inner(security.wrap_stream(conn)); + } else { + connection.set_inner(conn); + } + } else { if (role == Role.INITIATOR) { select_new_transport(stream); @@ -913,6 +964,7 @@ public class Connection : IOStream { return true; } public async bool close_read_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + debug("Closing Jingle input stream"); yield wait_and_check_for_errors(io_priority, cancellable); if (read_closed) { return true; diff --git a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala index 951ea7b7..7780d973 100644 --- a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala +++ b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala @@ -48,18 +48,24 @@ public class Module : Jingle.ContentType, XmppStreamModule { return stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid); } - public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size) throws IOError { + public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws IOError { + StanzaNode file_node; StanzaNode description = new StanzaNode.build("description", NS_URI) .add_self_xmlns() - .put_node(new StanzaNode.build("file", NS_URI) - .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename))) - .put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string())))); + .put_node(file_node = new StanzaNode.build("file", NS_URI) + .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename)))); // TODO(hrxi): Add the mandatory hash field + if (size > 0) { + file_node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string()))); + } else { + warning("Sending file %s without size, likely going to cause problems down the road...", basename); + } + Jingle.Session session; try { session = stream.get_module(Jingle.Module.IDENTITY) - .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description); // TODO(hrxi): Why "a-file-offer"? + .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description, precondition_name, precondition_options); // TODO(hrxi): Why "a-file-offer"? } catch (Jingle.Error e) { throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)"); } @@ -172,13 +178,14 @@ public class FileTransfer : Object { public Jid peer { get { return session.peer_full_jid; } } public string? file_name { get { return parameters.name; } } public int64 size { get { return parameters.size; } } + public Jingle.SecurityParameters? security { get { return session.security; } } public InputStream? stream { get; private set; } public FileTransfer(Jingle.Session session, Parameters parameters) { this.session = session; this.parameters = parameters; - this.stream = new FileTransferInputStream(session.conn.input_stream, parameters.size); + this.stream = new FileTransferInputStream(session.conn.input_stream, size); } public void accept(XmppStream stream) throws IOError { diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index a5eb1250..991ea141 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -287,6 +287,7 @@ class Parameters : Jingle.TransportParameters, Object { } remote_sent_selected_candidate = true; remote_selected_candidate = candidate; + debug("Remote selected candidate %s", candidate.cid); try_completing_negotiation(); } private void handle_activated(string cid) throws Jingle.IqError { @@ -353,6 +354,7 @@ class Parameters : Jingle.TransportParameters, Object { } } public async void wait_for_remote_activation(Candidate candidate, SocketConnection conn) { + debug("Waiting for remote activation of %s", candidate.cid); waiting_for_activation_cid = candidate.cid; waiting_for_activation_callback = wait_for_remote_activation.callback; yield; @@ -368,6 +370,7 @@ class Parameters : Jingle.TransportParameters, Object { } } public async void connect_to_local_candidate(Candidate candidate) { + debug("Connecting to candidate %s", candidate.cid); try { SocketConnection conn = yield connect_to_socks5(candidate, local_dstaddr); @@ -420,6 +423,7 @@ class Parameters : Jingle.TransportParameters, Object { SocketClient socket_client = new SocketClient() { timeout=3 }; string address = @"[$(candidate.host)]:$(candidate.port)"; + debug("Connecting to SOCKS5 server at %s", address); size_t written; size_t read; @@ -500,6 +504,7 @@ class Parameters : Jingle.TransportParameters, Object { local_determined_selected_candidate = true; local_selected_candidate = candidate; local_selected_candidate_conn = conn; + debug("Selected candidate %s", candidate.cid); session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("sid", sid) @@ -522,6 +527,8 @@ class Parameters : Jingle.TransportParameters, Object { .put_attribute("sid", sid) .put_node(new StanzaNode.build("candidate-error", NS_URI)) ); + // Try remote candidates + try_completing_negotiation(); } public void create_transport_connection(XmppStream stream, Jingle.Session session) { this.session = session; diff --git a/xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala b/xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala new file mode 100644 index 00000000..e2b1326b --- /dev/null +++ b/xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala @@ -0,0 +1,142 @@ +using Gee; +using Xmpp.Xep.Jingle; + +namespace Xmpp.Xep.Jet { +public const string NS_URI = "urn:xmpp:jingle:jet:0"; + +public class Module : XmppStreamModule, SecurityPrecondition { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0391_jet"); + private HashMap envelop_encodings = new HashMap(); + private HashMap ciphers = new HashMap(); + + public override void attach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(Jingle.Module.IDENTITY).register_security_precondition(this); + } + + public override void detach(XmppStream stream) { + } + + public bool is_available(XmppStream stream, Jid full_jid) { + bool? has_feature = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI); + return has_feature != null && (!)has_feature; + } + + public void register_envelop_encoding(EnvelopEncoding encoding) { + envelop_encodings[encoding.get_type_uri()] = encoding; + } + + public void register_cipher(Cipher cipher) { + ciphers[cipher.get_cipher_uri()] = cipher; + } + + public string security_ns_uri() { + return NS_URI; + } + + public Jingle.SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error requires (options is Options) { + Options jet_options = (Options) options; + string cipher = jet_options.cipher_uri; + string type = jet_options.type_uri; + if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) { + throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown"); + } + EnvelopEncoding encoding = envelop_encodings[type]; + return new SecurityParameters(ciphers[cipher], encoding, ciphers[cipher].generate_random_secret(), jet_options); + } + + public Jingle.SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError { + string? cipher = security.get_attribute("cipher"); + string? type = security.get_attribute("type"); + if (cipher == null || type == null) { + throw new IqError.BAD_REQUEST("No cipher or type specified for JET"); + } + if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) { + throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown"); + } + EnvelopEncoding encoding = envelop_encodings[type]; + TransportSecret secret = encoding.decode_envolop(stream, local_full_jid, peer_full_jid, security); + return new SecurityParameters(ciphers[cipher], encoding, secret); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +public class Options : Object { + public string type_uri { get; private set; } + public string cipher_uri { get; private set; } + + public Options(string type_uri, string cipher_uri) { + this.type_uri = type_uri; + this.cipher_uri = cipher_uri; + } +} + +public class SecurityParameters : Jingle.SecurityParameters, Object { + public Cipher cipher { get; private set; } + public EnvelopEncoding encoding { get; private set; } + public TransportSecret secret { get; private set; } + public Options? options { get; private set; } + + public SecurityParameters(Cipher cipher, EnvelopEncoding encoding, TransportSecret secret, Options? options = null) { + this.cipher = cipher; + this.encoding = encoding; + this.secret = secret; + this.options = options; + } + + public string security_ns_uri() { + return NS_URI; + } + public IOStream wrap_stream(IOStream stream) { + debug("Wrapping stream into encrypted stream for %s/%s", encoding.get_type_uri(), cipher.get_cipher_uri()); + return new EncryptedStream(cipher, secret, stream); + } + public StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) { + StanzaNode security = new StanzaNode.build("security", NS_URI) + .add_self_xmlns() + .put_attribute("cipher", cipher.get_cipher_uri()) + .put_attribute("type", encoding.get_type_uri()); + encoding.encode_envelop(stream, local_full_jid, peer_full_jid, this, security); + return security; + } +} + +public interface Cipher : Object { + public abstract string get_cipher_uri(); + public abstract TransportSecret generate_random_secret(); + public abstract InputStream wrap_input_stream(InputStream input, TransportSecret secret); + public abstract OutputStream wrap_output_stream(OutputStream output, TransportSecret secret); +} + +private class EncryptedStream : IOStream { + private IOStream stream; + private InputStream input; + private OutputStream output; + public override InputStream input_stream { get { return input; } } + public override OutputStream output_stream { get { return output; } } + + public EncryptedStream(Cipher cipher, TransportSecret secret, IOStream stream) { + this.stream = stream; + input = cipher.wrap_input_stream(stream.input_stream, secret); + output = cipher.wrap_output_stream(stream.output_stream, secret); + } +} + +public class TransportSecret { + public uint8[] transport_key { get; private set; } + public uint8[] initialization_vector { get; private set; } + public TransportSecret(uint8[] transport_key, uint8[] initialization_vector) { + this.transport_key = transport_key; + this.initialization_vector = initialization_vector; + } +} + +public interface EnvelopEncoding : Object { + public abstract string get_type_uri(); + public abstract TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError; + public abstract void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, SecurityParameters security_params, StanzaNode security); +} + +} \ No newline at end of file