diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index 482b49fa..8ae4acc3 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -36,6 +36,8 @@ SOURCES src/file_transfer/file_decryptor.vala src/file_transfer/file_encryptor.vala + src/jingle/jingle_helper.vala + src/jingle/jet_omemo.vala src/logic/database.vala src/logic/encrypt_state.vala @@ -59,6 +61,7 @@ SOURCES src/ui/manage_key_dialog.vala src/ui/util.vala CUSTOM_VAPIS + ${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi ${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi @@ -74,7 +77,7 @@ OPTIONS add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\" -DG_LOG_DOMAIN="OMEMO") add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET}) add_dependencies(omemo ${GETTEXT_PACKAGE}-translations) -target_link_libraries(omemo libdino signal-protocol-vala ${OMEMO_PACKAGES}) +target_link_libraries(omemo libdino signal-protocol-vala crypto-vala ${OMEMO_PACKAGES}) set_target_properties(omemo PROPERTIES PREFIX "") set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/) diff --git a/plugins/omemo/src/jingle/jet_omemo.vala b/plugins/omemo/src/jingle/jet_omemo.vala new file mode 100644 index 00000000..2ddf5d33 --- /dev/null +++ b/plugins/omemo/src/jingle/jet_omemo.vala @@ -0,0 +1,145 @@ +using Crypto; +using Dino; +using Dino.Entities; +using Gee; +using Signal; +using Xmpp; +using Xmpp.Xep; + +namespace Dino.Plugins.JetOmemo { +private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0"; +private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; +public class Module : XmppStreamModule, Jet.EnvelopEncoding { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0396_jet_omemo"); + private Omemo.Plugin plugin; + + public Module(Omemo.Plugin plugin) { + this.plugin = plugin; + } + + public override void attach(XmppStream stream) { + if (stream.get_module(Jet.Module.IDENTITY) != null) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(Jet.Module.IDENTITY).register_envelop_encoding(this); + stream.get_module(Jet.Module.IDENTITY).register_cipher(new AesGcmCipher(16, AES_128_GCM_URI)); + } + } + + 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); + if (has_feature == null || !(!)has_feature) { + return false; + } + return stream.get_module(Xep.Jet.Module.IDENTITY).is_available(stream, full_jid); + } + + public string get_type_uri() { + return Omemo.NS_URI; + } + + public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError { + Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; + StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI); + if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element"); + StanzaNode? header = encrypted.get_subnode("header", Omemo.NS_URI); + if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element"); + string? iv_node = header.get_deep_string_content("iv"); + if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element"); + uint8[] iv = Base64.decode((!)iv_node); + foreach (StanzaNode key_node in header.get_subnodes("key")) { + if (key_node.get_attribute_int("rid") == store.local_registration_id) { + string? key_node_content = key_node.get_string_content(); + + uint8[] key; + Address address = new Address(peer_full_jid.bare_jid.to_string(), header.get_attribute_int("sid")); + if (key_node.get_attribute_bool("prekey")) { + PreKeySignalMessage msg = Omemo.Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_pre_key_signal_message(msg); + } else { + SignalMessage msg = Omemo.Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_signal_message(msg); + } + address.device_id = 0; // TODO: Hack to have address obj live longer + + uint8[] authtag = null; + if (key.length >= 32) { + int authtaglength = key.length - 16; + authtag = new uint8[authtaglength]; + uint8[] new_key = new uint8[16]; + Memory.copy(authtag, (uint8*)key + 16, 16); + Memory.copy(new_key, key, 16); + key = new_key; + } + // TODO: authtag? + return new Jet.TransportSecret(key, iv); + } + } + throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device"); + } + + public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) { + ArrayList accounts = plugin.app.stream_interactor.get_accounts(); + Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; + Account? account = null; + foreach (Account compare in accounts) { + if (compare.bare_jid.equals_bare(local_full_jid)) { + account = compare; + break; + } + } + if (account == null) { + // TODO + critical("Sending from offline account %s", local_full_jid.to_string()); + } + + StanzaNode header_node; + StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns() + .put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI) + .put_attribute("sid", store.local_registration_id.to_string()) + .put_node(new StanzaNode.build("iv", Omemo.NS_URI) + .put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector))))); + + plugin.trust_manager.encrypt_key(header_node, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account); + security.put_node(encrypted_node); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +public class AesGcmCipher : Jet.Cipher, Object { + private int key_size; + private string uri; + public AesGcmCipher(int key_size, string uri) { + this.key_size = key_size; + this.uri = uri; + } + public string get_cipher_uri() { + return uri; + } + public Jet.TransportSecret generate_random_secret() { + uint8[] iv = new uint8[16]; + Omemo.Plugin.get_context().randomize(iv); + uint8[] key = new uint8[key_size]; + Omemo.Plugin.get_context().randomize(key); + return new Jet.TransportSecret(key, iv); + } + public InputStream wrap_input_stream(InputStream input, Jet.TransportSecret secret) requires (secret.transport_key.length == key_size) { + SymmetricCipher cipher = new SymmetricCipher("AES-GCM"); + cipher.set_key(secret.transport_key); + cipher.set_iv(secret.initialization_vector); + return new ConverterInputStream(input, new SymmetricCipherDecrypter((owned) cipher, 16)); + } + public OutputStream wrap_output_stream(OutputStream output, Jet.TransportSecret secret) requires (secret.transport_key.length == key_size) { + Crypto.SymmetricCipher cipher = new SymmetricCipher("AES-GCM"); + cipher.set_key(secret.transport_key); + cipher.set_iv(secret.initialization_vector); + return new ConverterOutputStream(output, new SymmetricCipherEncrypter((owned) cipher, 16)); + } +} +} diff --git a/plugins/omemo/src/jingle/jingle_helper.vala b/plugins/omemo/src/jingle/jingle_helper.vala new file mode 100644 index 00000000..6814fd00 --- /dev/null +++ b/plugins/omemo/src/jingle/jingle_helper.vala @@ -0,0 +1,53 @@ +using Dino.Entities; +using Xmpp; + +namespace Dino.Plugins.JetOmemo { +public class EncryptionHelper : JingleFileEncryptionHelper, Object { + private StreamInteractor stream_interactor; + + public EncryptionHelper(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public bool can_transfer(Conversation conversation) { + return true; + } + + public bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid) { + XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream == null) return false; + + Gee.List? resources = stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart); + if (resources == null) return false; + + if (full_jid == null) { + foreach (Jid test_jid in resources) { + if (stream.get_module(Module.IDENTITY).is_available(stream, test_jid)) { + return true; + } + } + } else { + if (stream.get_module(Module.IDENTITY).is_available(stream, full_jid)) { + return true; + } + } + return false; + } + + public string? get_precondition_name(Conversation conversation, FileTransfer file_transfer) { + return Xep.Jet.NS_URI; + } + + public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) { + return new Xep.Jet.Options(Omemo.NS_URI, AES_128_GCM_URI); + } + + public FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) { + Xep.Jet.SecurityParameters? security = jingle_transfer.security as Xep.Jet.SecurityParameters; + if (security != null && security.encoding.get_type_uri() == Omemo.NS_URI) { + file_transfer.encryption = Encryption.OMEMO; + } + return file_meta; + } +} +} \ No newline at end of file diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala index ded7e995..658e55ef 100644 --- a/plugins/omemo/src/logic/trust_manager.vala +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -72,6 +72,70 @@ public class TrustManager { return key_node; } + internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) throws Error { + EncryptState status = new EncryptState(); + StreamModule module = stream.get_module(StreamModule.IDENTITY); + + //Check we have the bundles and device lists needed to send the message + if (!is_known_address(account, self_jid)) return status; + status.own_list = true; + status.own_devices = get_trusted_devices(account, self_jid).size; + status.other_waiting_lists = 0; + status.other_devices = 0; + foreach (Jid recipient in recipients) { + if (!is_known_address(account, recipient)) { + status.other_waiting_lists++; + } + if (status.other_waiting_lists > 0) return status; + status.other_devices += get_trusted_devices(account, recipient).size; + } + if (status.own_devices == 0 || status.other_devices == 0) return status; + + + //Encrypt the key for each recipient's device individually + Address address = new Address("", 0); + foreach (Jid recipient in recipients) { + foreach(int32 device_id in get_trusted_devices(account, recipient)) { + if (module.is_ignored_device(recipient, device_id)) { + status.other_lost++; + continue; + } + try { + address.name = recipient.bare_jid.to_string(); + address.device_id = (int) device_id; + StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); + header_node.put_node(key_node); + status.other_success++; + } catch (Error e) { + if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; + else status.other_failure++; + } + } + } + + // Encrypt the key for each own device + address.name = self_jid.bare_jid.to_string(); + foreach(int32 device_id in get_trusted_devices(account, self_jid)) { + if (module.is_ignored_device(self_jid, device_id)) { + status.own_lost++; + continue; + } + if (device_id != module.store.local_registration_id) { + address.device_id = (int) device_id; + try { + StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); + header_node.put_node(key_node); + status.own_success++; + } catch (Error e) { + if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; + else status.own_failure++; + } + } + } + + return status; + } + public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) { EncryptState status = new EncryptState(); if (!Plugin.ensure_context()) return status; @@ -80,21 +144,6 @@ public class TrustManager { StreamModule module = stream.get_module(StreamModule.IDENTITY); try { - //Check we have the bundles and device lists needed to send the message - if (!is_known_address(account, self_jid)) return status; - status.own_list = true; - status.own_devices = get_trusted_devices(account, self_jid).size; - status.other_waiting_lists = 0; - status.other_devices = 0; - foreach (Jid recipient in recipients) { - if (!is_known_address(account, recipient)) { - status.other_waiting_lists++; - } - if (status.other_waiting_lists > 0) return status; - status.other_devices += get_trusted_devices(account, recipient).size; - } - if (status.own_devices == 0 || status.other_devices == 0) return status; - //Create a key and use it to encrypt the message uint8[] key = new uint8[16]; Plugin.get_context().randomize(key); @@ -117,46 +166,7 @@ public class TrustManager { .put_node(new StanzaNode.build("payload", NS_URI) .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); - //Encrypt the key for each recipient's device individually - Address address = new Address(message.to.bare_jid.to_string(), 0); - foreach (Jid recipient in recipients) { - foreach(int32 device_id in get_trusted_devices(account, recipient)) { - if (module.is_ignored_device(recipient, device_id)) { - status.other_lost++; - continue; - } - try { - address.name = recipient.bare_jid.to_string(); - address.device_id = (int) device_id; - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); - status.other_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; - else status.other_failure++; - } - } - } - - // Encrypt the key for each own device - address.name = self_jid.bare_jid.to_string(); - foreach(int32 device_id in get_trusted_devices(account, self_jid)) { - if (module.is_ignored_device(self_jid, device_id)) { - status.own_lost++; - continue; - } - if (device_id != module.store.local_registration_id) { - address.device_id = (int) device_id; - try { - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); - status.own_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; - else status.own_failure++; - } - } - } + status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account); message.stanza.put_node(encrypted_node); Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala index 9f1252c0..913d3347 100644 --- a/plugins/omemo/src/plugin.vala +++ b/plugins/omemo/src/plugin.vala @@ -1,3 +1,5 @@ +using Dino.Entities; + extern const string GETTEXT_PACKAGE; extern const string LOCALE_INSTALL_DIR; @@ -47,11 +49,13 @@ public class Plugin : RootInterface, Object { this.app.plugin_registry.register_notification_populator(device_notification_populator); this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { list.add(new StreamModule()); + list.add(new JetOmemo.Module(this)); this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account); }); app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor()); app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor()); + JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.OMEMO, new JetOmemo.EncryptionHelper(app.stream_interactor)); Manager.start(this.app.stream_interactor, db, trust_manager); diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala index 824098ba..9a1bc75a 100644 --- a/plugins/omemo/src/protocol/stream_module.vala +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -30,6 +30,7 @@ public class StreamModule : XmppStreamModule { this.store = Plugin.get_context().create_store(); store_created(store); stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node)); + } public override void detach(XmppStream stream) {}