diff --git a/plugins/omemo-vala/vapi/omemo-public.vapi b/plugins/omemo-vala/vapi/omemo-public.vapi index 03d5fe0d..a2ce23b5 100644 --- a/plugins/omemo-vala/vapi/omemo-public.vapi +++ b/plugins/omemo-vala/vapi/omemo-public.vapi @@ -213,6 +213,8 @@ namespace Omemo { } return buffer.data; } + public Buffer ed { get; } + public Buffer mont { get; } public int compare(ECPublicKey other); public int memcmp(ECPublicKey other); } diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index 33402018..d8f153bc 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -50,6 +50,9 @@ SOURCES src/protocol/bundle.vala src/protocol/message_flag.vala src/protocol/stream_module.vala + src/protocol/legacy_stream_module.vala + src/protocol/v1_stream_module.vala + src/protocol/version.vala src/ui/account_settings_entry.vala src/ui/account_settings_widget.vala diff --git a/plugins/omemo/src/jingle/jet_omemo.vala b/plugins/omemo/src/jingle/jet_omemo.vala index f845378a..e396271b 100644 --- a/plugins/omemo/src/jingle/jet_omemo.vala +++ b/plugins/omemo/src/jingle/jet_omemo.vala @@ -37,14 +37,14 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding { } public string get_type_uri() { - return Omemo.NS_URI; + return Omemo.Legacy.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); + Store store = stream.get_module(Omemo.Legacy.StreamModule.IDENTITY).store; + StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.Legacy.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); + StanzaNode? header = encrypted.get_subnode("header", Omemo.Legacy.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"); @@ -84,7 +84,7 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding { 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; + Store store = stream.get_module(Omemo.Legacy.StreamModule.IDENTITY).store; Account? account = null; foreach (Account compare in accounts) { if (compare.bare_jid.equals_bare(local_full_jid)) { @@ -98,10 +98,10 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding { } 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) + StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.Legacy.NS_URI).add_self_xmlns() + .put_node(header_node = new StanzaNode.build("header", Omemo.Legacy.NS_URI) .put_attribute("sid", store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", Omemo.NS_URI) + .put_node(new StanzaNode.build("iv", Omemo.Legacy.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); diff --git a/plugins/omemo/src/jingle/jingle_helper.vala b/plugins/omemo/src/jingle/jingle_helper.vala index 6814fd00..12d13c43 100644 --- a/plugins/omemo/src/jingle/jingle_helper.vala +++ b/plugins/omemo/src/jingle/jingle_helper.vala @@ -39,12 +39,12 @@ public class EncryptionHelper : JingleFileEncryptionHelper, Object { } public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) { - return new Xep.Jet.Options(Omemo.NS_URI, AES_128_GCM_URI); + return new Xep.Jet.Options(Omemo.Legacy.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) { + if (security != null && security.encoding.get_type_uri() == Omemo.Legacy.NS_URI) { file_transfer.encryption = Encryption.OMEMO; } return file_meta; diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala index a66e7b84..6e67aa6d 100644 --- a/plugins/omemo/src/logic/manager.vala +++ b/plugins/omemo/src/logic/manager.vala @@ -73,7 +73,8 @@ public class Manager : StreamInteractionModule, Object { XmppStream? stream = stream_interactor.get_stream(account); if (stream == null) return; - stream.get_module(StreamModule.IDENTITY).clear_device_list(stream); + stream.get_module(Legacy.StreamModule.IDENTITY).clear_device_list(stream); + stream.get_module(V1.StreamModule.IDENTITY).clear_device_list(stream); } private Gee.List get_occupants(Jid jid, Account account){ @@ -100,12 +101,12 @@ public class Manager : StreamInteractionModule, Object { message.marked = Entities.Message.Marked.UNSENT; return; } - StreamModule? module_ = ((!)stream).get_module(StreamModule.IDENTITY); - if (module_ == null) { + Legacy.StreamModule? legacy_module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY); + V1.StreamModule? v1_module = ((!)stream).get_module(V1.StreamModule.IDENTITY); + if (legacy_module == null && v1_module == null) { message.marked = Entities.Message.Marked.UNSENT; return; } - StreamModule module = (!)module_; //Get a list of everyone for whom the message should be encrypted Gee.List recipients; @@ -147,17 +148,18 @@ public class Manager : StreamInteractionModule, Object { } else { debug("delaying message %s", state.to_string()); + // TODO: V1 support if (state.waiting_own_sessions > 0) { - module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid)); + legacy_module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid)); } if (state.waiting_other_sessions > 0 && message.counterpart != null) { foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { - module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid)); + legacy_module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid)); } } if (state.waiting_other_devicelists > 0 && message.counterpart != null) { foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { - module.request_user_devicelist.begin((!)stream, jid); + legacy_module.request_user_devicelist.begin((!)stream, jid); } } } @@ -169,16 +171,24 @@ public class Manager : StreamInteractionModule, Object { XmppStream? stream = stream_interactor.get_stream(account); if(stream == null) return; - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid); + stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid); + stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid); } private void on_stream_negotiated(Account account, XmppStream stream) { - StreamModule module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY); - if (module != null) { - module.request_user_devicelist.begin(stream, account.bare_jid); - module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices)); - module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); - module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid)); + Legacy.StreamModule legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY); + if (legacy_module != null) { + legacy_module.request_user_devicelist.begin(stream, account.bare_jid); + legacy_module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices)); + legacy_module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); + legacy_module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid)); + } + V1.StreamModule v1_module = stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY); + if (v1_module != null) { + v1_module.request_user_devicelist.begin(stream, account.bare_jid); + //v1_module.device_list_loaded.connect((jid, devices) => on_device_list_loaded_v1(account, jid, devices)); + v1_module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle)); + v1_module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid)); } initialize_store.begin(account); } @@ -190,7 +200,7 @@ public class Manager : StreamInteractionModule, Object { if (stream == null) { return; } - StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); + Legacy.StreamModule? module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY); if (module == null) { return; } @@ -278,7 +288,7 @@ public class Manager : StreamInteractionModule, Object { if (should_start_session(account, jid)) { XmppStream? stream = stream_interactor.get_stream(account); if (stream != null) { - StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY); + Legacy.StreamModule? module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY); if (module != null) { module.start_session(stream, jid, device_id, bundle); } @@ -335,9 +345,20 @@ public class Manager : StreamInteractionModule, Object { account.notify["id"].connect(() => initialize_store.callback()); yield; } - StreamModule? module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY); - if (module == null) return; - Store store = module.store; + Store store = null; + Legacy.StreamModule? legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY); + V1.StreamModule? v1_module = stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY); + if (legacy_module != null) { + store = legacy_module.store; + } + if (v1_module != null) { + if (store != null) { + v1_module.store = store; + } else { + store = v1_module.store; + } + } + if (store == null) return; Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner; int identity_id = -1; bool publish_identity = false; @@ -379,7 +400,12 @@ public class Manager : StreamInteractionModule, Object { // Generated new device ID, ensure this gets added to the devicelist XmppStream? stream = stream_interactor.get_stream(account); if (stream != null) { - module.request_user_devicelist.begin((!)stream, account.bare_jid); + if (legacy_module != null) { + legacy_module.request_user_devicelist.begin((!)stream, account.bare_jid); + } + if (v1_module != null) { + v1_module.request_user_devicelist.begin((!)stream, account.bare_jid); + } } } @@ -401,8 +427,15 @@ public class Manager : StreamInteractionModule, Object { if (trust_manager.is_known_address(account, jid)) return true; XmppStream? stream = stream_interactor.get_stream(account); if (stream != null) { - var device_list = yield stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist(stream, jid); - return device_list.size > 0; + var legacy_module = stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY); + if (legacy_module != null) { + if ((yield legacy_module.request_user_devicelist(stream, jid)).size > 0) return true; + } + var v1_module = stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY); + if (legacy_module != null) { + if ((yield v1_module.request_user_devicelist(stream, jid)).size > 0) return true; + } + return false; } return true; // TODO wait for stream? } diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala index 1e110bf9..5e3915da 100644 --- a/plugins/omemo/src/logic/trust_manager.vala +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -73,7 +73,7 @@ public class TrustManager { SessionCipher cipher = store.create_session_cipher(address); CiphertextMessage device_key = cipher.encrypt(key); debug("Created encrypted key for %s/%d", address.name, address.device_id); - StanzaNode key_node = new StanzaNode.build("key", NS_URI) + StanzaNode key_node = new StanzaNode.build("key", Legacy.NS_URI) .put_attribute("rid", address.device_id.to_string()) .put_node(new StanzaNode.text(Base64.encode(device_key.serialized))); if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true"); @@ -82,7 +82,7 @@ public class TrustManager { 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); + Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY); //Check we have the bundles and device lists needed to send the message if (!is_known_address(account, self_jid)) return status; @@ -149,7 +149,7 @@ public class TrustManager { if (!Plugin.ensure_context()) return status; if (message.to == null) return status; - StreamModule module = stream.get_module(StreamModule.IDENTITY); + Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY); try { //Create a key and use it to encrypt the message @@ -166,18 +166,18 @@ public class TrustManager { Memory.copy((uint8*)keytag + key.length, tag, tag.length); StanzaNode header_node; - StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns() - .put_node(header_node = new StanzaNode.build("header", NS_URI) + StanzaNode encrypted_node = new StanzaNode.build("encrypted", Legacy.NS_URI).add_self_xmlns() + .put_node(header_node = new StanzaNode.build("header", Legacy.NS_URI) .put_attribute("sid", module.store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", NS_URI) + .put_node(new StanzaNode.build("iv", Legacy.NS_URI) .put_node(new StanzaNode.text(Base64.encode(iv))))) - .put_node(new StanzaNode.build("payload", NS_URI) + .put_node(new StanzaNode.build("payload", Legacy.NS_URI) .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); 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"); + Xep.ExplicitEncryption.add_encryption_tag_to_message(message, Legacy.NS_URI, "OMEMO"); message.body = "[This message is OMEMO encrypted]"; status.encrypted = true; } catch (Error e) { @@ -277,12 +277,15 @@ public class TrustManager { } public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - StreamModule module = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY); + Legacy.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY); Store store = module.store; - StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI); + StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", Legacy.NS_URI); if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; StanzaNode encrypted = (!)_encrypted; + if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == Legacy.NS_URI) { + message.body = "[This message is OMEMO encrypted]"; // TODO temporary + } if (!Plugin.ensure_context()) return false; int identity_id = db.identity.get_id(conversation.account.id); MessageFlag flag = new MessageFlag(); @@ -305,9 +308,8 @@ public class TrustManager { string? payload = encrypted.get_deep_string_content("payload"); string? iv_node = header.get_deep_string_content("iv"); string? key_node_content = key_node.get_string_content(); - if (payload == null || iv_node == null || key_node_content == null) continue; - uint8[] key; - uint8[] ciphertext = Base64.decode((!)payload); + if (iv_node == null || key_node_content == null) continue; + uint8[] key; uint8[] iv = Base64.decode((!)iv_node); Gee.List possible_jids = new ArrayList(); if (conversation.type_ == Conversation.Type.CHAT) { @@ -382,20 +384,23 @@ public class TrustManager { } //address.device_id = 0; // TODO: Hack to have address obj live longer - if (key.length >= 32) { - int authtaglength = key.length - 16; - uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; - uint8[] new_key = new uint8[16]; - Memory.copy(new_ciphertext, ciphertext, ciphertext.length); - Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); - Memory.copy(new_key, key, 16); - ciphertext = new_ciphertext; - key = new_key; - } + if (payload != null) { + uint8[] ciphertext = Base64.decode(payload ?? ""); + if (key.length >= 32) { + int authtaglength = key.length - 16; + uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; + uint8[] new_key = new uint8[16]; + Memory.copy(new_ciphertext, ciphertext, ciphertext.length); + Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); + Memory.copy(new_key, key, 16); + ciphertext = new_ciphertext; + key = new_key; + } - message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); - message_device_id_map[message] = address.device_id; - message.encryption = Encryption.OMEMO; + message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); + message_device_id_map[message] = address.device_id; + message.encryption = Encryption.OMEMO; + } flag.decrypted = true; } catch (Error e) { debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message); diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala index 4b287238..13a33eee 100644 --- a/plugins/omemo/src/plugin.vala +++ b/plugins/omemo/src/plugin.vala @@ -50,7 +50,8 @@ public class Plugin : RootInterface, Object { this.app.plugin_registry.register_notification_populator(device_notification_populator); this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this)); this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { - list.add(new StreamModule()); + list.add(new Legacy.StreamModule()); + list.add(new V1.StreamModule()); list.add(new JetOmemo.Module(this)); this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account); }); diff --git a/plugins/omemo/src/protocol/bundle.vala b/plugins/omemo/src/protocol/bundle.vala index 48f7ed0e..055292ce 100644 --- a/plugins/omemo/src/protocol/bundle.vala +++ b/plugins/omemo/src/protocol/bundle.vala @@ -12,16 +12,40 @@ public class Bundle { assert(Plugin.ensure_context()); } + public ProtocolVersion version { get { + switch(node.ns_uri) { + case Legacy.NS_URI: return ProtocolVersion.LEGACY; + case V1.NS_URI: return ProtocolVersion.V1; + } + return ProtocolVersion.UNKNOWN; + }} + public int32 signed_pre_key_id { owned get { if (node == null) return -1; - string? id = ((!)node).get_deep_attribute("signedPreKeyPublic", "signedPreKeyId"); + string? id = null; + switch(version) { + case ProtocolVersion.LEGACY: + id = ((!)node).get_deep_attribute("signedPreKeyPublic", "signedPreKeyId"); + break; + case ProtocolVersion.V1: + id = ((!)node).get_deep_attribute("spk", "id"); + break; + } if (id == null) return -1; return int.parse((!)id); }} public ECPublicKey? signed_pre_key { owned get { if (node == null) return null; - string? key = ((!)node).get_deep_string_content("signedPreKeyPublic"); + string? key = null; + switch(version) { + case ProtocolVersion.LEGACY: + key = ((!)node).get_deep_string_content("signedPreKeyPublic"); + break; + case ProtocolVersion.V1: + key = ((!)node).get_deep_string_content("spk"); + break; + } if (key == null) return null; try { return Plugin.get_context().decode_public_key(Base64.decode((!)key)); @@ -32,14 +56,30 @@ public class Bundle { public uint8[]? signed_pre_key_signature { owned get { if (node == null) return null; - string? sig = ((!)node).get_deep_string_content("signedPreKeySignature"); + string? sig = null; + switch(version) { + case ProtocolVersion.LEGACY: + sig = ((!)node).get_deep_string_content("signedPreKeySignature"); + break; + case ProtocolVersion.V1: + sig = ((!)node).get_deep_string_content("spks"); + break; + } if (sig == null) return null; return Base64.decode((!)sig); }} public ECPublicKey? identity_key { owned get { if (node == null) return null; - string? key = ((!)node).get_deep_string_content("identityKey"); + string? key = null; + switch(version) { + case ProtocolVersion.LEGACY: + key = ((!)node).get_deep_string_content("identityKey"); + break; + case ProtocolVersion.V1: + key = ((!)node).get_deep_string_content("ik"); + break; + } if (key == null) return null; try { return Plugin.get_context().decode_public_key(Base64.decode((!)key)); @@ -51,10 +91,21 @@ public class Bundle { public ArrayList pre_keys { owned get { ArrayList list = new ArrayList(); if (node == null || ((!)node).get_subnode("prekeys") == null) return list; - ((!)node).get_deep_subnodes("prekeys", "preKeyPublic") - .filter((node) => ((!)node).get_attribute("preKeyId") != null) - .map(PreKey.create) - .foreach((key) => list.add(key)); + + switch(version) { + case ProtocolVersion.LEGACY: + ((!)node).get_deep_subnodes("prekeys", "preKeyPublic") + .filter((node) => ((!)node).get_attribute("preKeyId") != null) + .map(PreKey.create) + .foreach((key) => list.add(key)); + break; + case ProtocolVersion.V1: + ((!)node).get_deep_subnodes("prekeys", "pk") + .filter((node) => ((!)node).get_attribute("id") != null) + .map(PreKey.create) + .foreach((key) => list.add(key)); + break; + } return list; }} @@ -65,12 +116,26 @@ public class Bundle { return new PreKey(node); } + public ProtocolVersion version { get { + switch(node.ns_uri) { + case Legacy.NS_URI: return ProtocolVersion.LEGACY; + case V1.NS_URI: return ProtocolVersion.V1; + } + return ProtocolVersion.UNKNOWN; + }} + public PreKey(StanzaNode node) { this.node = node; } public int32 key_id { owned get { - return int.parse(node.get_attribute("preKeyId") ?? "-1"); + switch(version) { + case ProtocolVersion.LEGACY: + return int.parse(node.get_attribute("preKeyId") ?? "-1"); + case ProtocolVersion.V1: + return int.parse(node.get_attribute("id") ?? "-1"); + } + return -1; }} public ECPublicKey? key { owned get { diff --git a/plugins/omemo/src/protocol/legacy_stream_module.vala b/plugins/omemo/src/protocol/legacy_stream_module.vala new file mode 100644 index 00000000..8256a3b5 --- /dev/null +++ b/plugins/omemo/src/protocol/legacy_stream_module.vala @@ -0,0 +1,327 @@ +using Gee; +using Omemo; +using Xmpp; +using Xmpp.Xep; + +namespace Dino.Plugins.Omemo.Legacy { + +private const string NS_URI = "eu.siacs.conversations.axolotl"; +private const string NODE_DEVICELIST = NS_URI + ".devicelist"; +private const string NODE_BUNDLES = NS_URI + ".bundles"; +private const string NODE_VERIFICATION = NS_URI + ".verification"; + +private const int NUM_KEYS_TO_PUBLISH = 100; + +public class StreamModule : XmppStreamModule, BaseStreamModule { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "omemo_module"); + private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE; + + public Store store { public get; private set; } + private ConcurrentSet active_bundle_requests = new ConcurrentSet(); + private HashMap>> active_devicelist_requests = new HashMap>>(Jid.hash_func, Jid.equals_func); + private Map device_ignore_time = new HashMap(); + + public signal void device_list_loaded(Jid jid, ArrayList devices); + public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); + public signal void bundle_fetch_failed(Jid jid, int device_id); + + public StreamModule() { + if (Plugin.ensure_context()) { + this.store = Plugin.get_context().create_store(); + } + } + + public override void attach(XmppStream stream) { + stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null); + } + + public override void detach(XmppStream stream) {} + + public async ArrayList request_user_devicelist(XmppStream stream, Jid jid) { + var future = active_devicelist_requests[jid]; + if (future == null) { + var promise = new Promise?>(); + future = promise.future; + active_devicelist_requests[jid] = future; + + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => { + ArrayList device_list = parse_device_list(stream, jid, id, node); + promise.set_value(device_list); + active_devicelist_requests.unset(jid); + }); + } + + try { + ArrayList device_list = yield future.wait_async(); + return device_list; + } catch (FutureError error) { + warning("[Legacy] Future error when waiting for device list: %s", error.message); + return new ArrayList(); + } + } + + public ArrayList parse_device_list(XmppStream stream, Jid jid, string? id, StanzaNode? node_) { + ArrayList device_list = new ArrayList(); + + StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns(); + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) return device_list; + if (jid.equals_bare(my_jid) && store.local_registration_id != 0) { + bool am_on_devicelist = false; + foreach (StanzaNode device_node in node.get_subnodes("device")) { + int device_id = device_node.get_attribute_int("id"); + if (store.local_registration_id == device_id) { + am_on_devicelist = true; + } + } + if (!am_on_devicelist) { + debug("[Legacy] Not on device list, adding id"); + node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string())); + stream.get_module(Pubsub.Module.IDENTITY).publish.begin(stream, jid, NODE_DEVICELIST, id, node); + } + publish_bundles_if_needed(stream, jid); + } + + foreach (StanzaNode device_node in node.get_subnodes("device")) { + device_list.add(device_node.get_attribute_int("id")); + } + device_list_loaded(jid, device_list); + + return device_list; + } + + public void fetch_bundles(XmppStream stream, Jid jid, Gee.List devices) { + Address address = new Address(jid.bare_jid.to_string(), 0); + foreach(int32 device_id in devices) { + if (!is_ignored_device(jid, device_id)) { + address.device_id = device_id; + try { + if (!store.contains_session(address)) { + fetch_bundle(stream, jid, device_id); + } + } catch (Error e) { + // Ignore + } + } + } + address.device_id = 0; // TODO: Hack to have address obj live longer + } + + public void fetch_bundle(XmppStream stream, Jid jid, int device_id, bool ignore_if_non_present = true) { + if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) { + debug("[Legacy] Asking for bundle for %s/%d", jid.bare_jid.to_string(), device_id); + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => { + on_other_bundle_result(stream, jid, device_id, id, node, ignore_if_non_present); + }); + } + } + + public void ignore_device(Jid jid, int32 device_id) { + if (device_id <= 0) return; + lock (device_ignore_time) { + device_ignore_time[jid.bare_jid.to_string() + @":$device_id"] = new DateTime.now_utc(); + } + } + + public void unignore_device(Jid jid, int32 device_id) { + if (device_id <= 0) return; + lock (device_ignore_time) { + device_ignore_time.unset(jid.bare_jid.to_string() + @":$device_id"); + } + } + + public bool is_ignored_device(Jid jid, int32 device_id) { + if (device_id <= 0) return true; + lock (device_ignore_time) { + string id = jid.bare_jid.to_string() + @":$device_id"; + if (device_ignore_time.has_key(id)) { + return new DateTime.now_utc().difference(device_ignore_time[id]) < IGNORE_TIME; + } + } + return false; + } + + public void clear_device_list(XmppStream stream) { + stream.get_module(Pubsub.Module.IDENTITY).delete_node(stream, null, NODE_DEVICELIST); + } + + private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node, bool ignore_if_non_present) { + if (node == null) { + // Device not registered, shouldn't exist + if (ignore_if_non_present) { + debug("[Legacy] Ignoring device %s/%d: No bundle", jid.bare_jid.to_string(), device_id); + stream.get_module(IDENTITY).ignore_device(jid, device_id); + } + bundle_fetch_failed(jid, device_id); + } else { + Bundle bundle = new Bundle(node); + stream.get_module(IDENTITY).unignore_device(jid, device_id); + debug("[Legacy] Received bundle for %s/%d: %s", jid.bare_jid.to_string(), device_id, Base64.encode(bundle.identity_key.serialize())); + bundle_fetched(jid, device_id, bundle); + } + stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id"); + } + + public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) { + bool fail = false; + int32 signed_pre_key_id = bundle.signed_pre_key_id; + ECPublicKey? signed_pre_key = bundle.signed_pre_key; + uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature; + ECPublicKey? identity_key = bundle.identity_key; + + ArrayList pre_keys = bundle.pre_keys; + if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) { + fail = true; + } else { + int pre_key_idx = Random.int_range(0, pre_keys.size); + int32 pre_key_id = pre_keys[pre_key_idx].key_id; + ECPublicKey? pre_key = pre_keys[pre_key_idx].key; + if (pre_key_id < 0 || pre_key == null) { + fail = true; + } else { + Address address = new Address(jid.bare_jid.to_string(), device_id); + try { + if (store.contains_session(address)) { + return false; + } + debug("[Legacy] Starting new session for encryption with %s/%d", jid.bare_jid.to_string(), device_id); + SessionBuilder builder = store.create_session_builder(address); + builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key)); + } catch (Error e) { + debug("[Legacy] Can't create session with %s/%d: %s", jid.bare_jid.to_string(), device_id, e.message); + fail = true; + } + address.device_id = 0; // TODO: Hack to have address obj live longer + } + } + if (fail) { + debug("[Legacy] Ignoring device %s/%d: Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string()); + stream.get_module(IDENTITY).ignore_device(jid, device_id); + } + return true; + } + + public void publish_bundles_if_needed(XmppStream stream, Jid jid) { + if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) { + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result); + } + } + + private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) { + if (!Plugin.ensure_context()) return; + Map keys = new HashMap(); + ECPublicKey? identity_key = null; + int32 signed_pre_key_id = -1; + ECPublicKey? signed_pre_key = null; + SignedPreKeyRecord? signed_pre_key_record = null; + bool changed = false; + if (node == null) { + identity_key = store.identity_key_pair.public; + changed = true; + } else { + Bundle bundle = new Bundle(node); + foreach (Bundle.PreKey prekey in bundle.pre_keys) { + ECPublicKey? key = prekey.key; + if (key != null) { + keys[prekey.key_id] = (!)key; + } + } + identity_key = bundle.identity_key; + signed_pre_key_id = bundle.signed_pre_key_id; + signed_pre_key = bundle.signed_pre_key; + } + + try { + // Validate IdentityKey + if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) { + changed = true; + } + IdentityKeyPair identity_key_pair = store.identity_key_pair; + + // Validate signedPreKeyRecord + ID + if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) { + signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number + signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id); + store.store_signed_pre_key((!)signed_pre_key_record); + changed = true; + } else { + signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id); + } + + // Validate PreKeys + Set pre_key_records = new HashSet(); + foreach (var entry in keys.entries) { + if (store.contains_pre_key(entry.key)) { + PreKeyRecord record = store.load_pre_key(entry.key); + if (record.key_pair.public.compare(entry.value) == 0) { + pre_key_records.add(record); + } + } + } + int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size; + if (new_keys > 0) { + int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number + Set new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys); + pre_key_records.add_all(new_records); + foreach (PreKeyRecord record in new_records) { + store.store_pre_key(record); + } + changed = true; + } + + if (changed) { + publish_bundles.begin(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id); + } + } catch (Error e) { + warning(@"[Legacy] Unexpected error while publishing bundle: $(e.message)\n"); + } + stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)"); + } + + public async void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set pre_key_records, int32 device_id) throws Error { + ECKeyPair tmp; + StanzaNode bundle = new StanzaNode.build("bundle", NS_URI) + .add_self_xmlns() + .put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI) + .put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string()) + .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize())))) + .put_node(new StanzaNode.build("signedPreKeySignature", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature)))) + .put_node(new StanzaNode.build("identityKey", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize())))); + StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI); + foreach (PreKeyRecord pre_key_record in pre_key_records) { + prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI) + .put_attribute("preKeyId", pre_key_record.id.to_string()) + .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize())))); + } + bundle.put_node(prekeys); + + yield stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", "1", bundle); + yield try_make_bundle_public(stream, device_id); + + } + + private async void try_make_bundle_public(XmppStream stream, int32 device_id) { + DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, @"$NODE_BUNDLES:$device_id"); + if (data_form == null) return; + + foreach (DataForms.DataForm.Field field in data_form.fields) { + if (field.var == "pubsub#access_model" && field.get_value_string() != Pubsub.ACCESS_MODEL_OPEN) { + field.set_value_string(Pubsub.ACCESS_MODEL_OPEN); + yield stream.get_module(Pubsub.Module.IDENTITY).submit_node_config(stream, data_form, @"$NODE_BUNDLES:$device_id"); + break; + } + } + } + + public override string get_ns() { + return NS_URI; + } + + public override string get_id() { + return IDENTITY.id; + } +} + +} diff --git a/plugins/omemo/src/protocol/message_flag.vala b/plugins/omemo/src/protocol/message_flag.vala index ba9ea16e..5c2f7f3f 100644 --- a/plugins/omemo/src/protocol/message_flag.vala +++ b/plugins/omemo/src/protocol/message_flag.vala @@ -8,11 +8,11 @@ public class MessageFlag : Xmpp.MessageFlag { public bool decrypted = false; public static MessageFlag? get_flag(MessageStanza message) { - return (MessageFlag) message.get_flag(NS_URI, id); + return (MessageFlag) message.get_flag(V1.NS_URI, id); } public override string get_ns() { - return NS_URI; + return V1.NS_URI; } public override string get_id() { diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala index 3b796c9e..4103c4f6 100644 --- a/plugins/omemo/src/protocol/stream_module.vala +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -1,327 +1,4 @@ -using Gee; -using Omemo; -using Xmpp; -using Xmpp.Xep; - namespace Dino.Plugins.Omemo { - -private const string NS_URI = "eu.siacs.conversations.axolotl"; -private const string NODE_DEVICELIST = NS_URI + ".devicelist"; -private const string NODE_BUNDLES = NS_URI + ".bundles"; -private const string NODE_VERIFICATION = NS_URI + ".verification"; - -private const int NUM_KEYS_TO_PUBLISH = 100; - -public class StreamModule : XmppStreamModule { - public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "omemo_module"); - private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE; - - public Store store { public get; private set; } - private ConcurrentSet active_bundle_requests = new ConcurrentSet(); - private HashMap>> active_devicelist_requests = new HashMap>>(Jid.hash_func, Jid.equals_func); - private Map device_ignore_time = new HashMap(); - - public signal void device_list_loaded(Jid jid, ArrayList devices); - public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); - public signal void bundle_fetch_failed(Jid jid, int device_id); - - public StreamModule() { - if (Plugin.ensure_context()) { - this.store = Plugin.get_context().create_store(); - } - } - - public override void attach(XmppStream stream) { - stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null); - } - - public override void detach(XmppStream stream) {} - - public async ArrayList request_user_devicelist(XmppStream stream, Jid jid) { - var future = active_devicelist_requests[jid]; - if (future == null) { - var promise = new Promise?>(); - future = promise.future; - active_devicelist_requests[jid] = future; - - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => { - ArrayList device_list = parse_device_list(stream, jid, id, node); - promise.set_value(device_list); - active_devicelist_requests.unset(jid); - }); - } - - try { - ArrayList device_list = yield future.wait_async(); - return device_list; - } catch (FutureError error) { - warning("Future error when waiting for device list: %s", error.message); - return new ArrayList(); - } - } - - public ArrayList parse_device_list(XmppStream stream, Jid jid, string? id, StanzaNode? node_) { - ArrayList device_list = new ArrayList(); - - StanzaNode node = node_ ?? new StanzaNode.build("list", NS_URI).add_self_xmlns(); - Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; - if (my_jid == null) return device_list; - if (jid.equals_bare(my_jid) && store.local_registration_id != 0) { - bool am_on_devicelist = false; - foreach (StanzaNode device_node in node.get_subnodes("device")) { - int device_id = device_node.get_attribute_int("id"); - if (store.local_registration_id == device_id) { - am_on_devicelist = true; - } - } - if (!am_on_devicelist) { - debug("Not on device list, adding id"); - node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string())); - stream.get_module(Pubsub.Module.IDENTITY).publish.begin(stream, jid, NODE_DEVICELIST, id, node); - } - publish_bundles_if_needed(stream, jid); - } - - foreach (StanzaNode device_node in node.get_subnodes("device")) { - device_list.add(device_node.get_attribute_int("id")); - } - device_list_loaded(jid, device_list); - - return device_list; - } - - public void fetch_bundles(XmppStream stream, Jid jid, Gee.List devices) { - Address address = new Address(jid.bare_jid.to_string(), 0); - foreach(int32 device_id in devices) { - if (!is_ignored_device(jid, device_id)) { - address.device_id = device_id; - try { - if (!store.contains_session(address)) { - fetch_bundle(stream, jid, device_id); - } - } catch (Error e) { - // Ignore - } - } - } - address.device_id = 0; // TODO: Hack to have address obj live longer - } - - public void fetch_bundle(XmppStream stream, Jid jid, int device_id, bool ignore_if_non_present = true) { - if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) { - debug("Asking for bundle for %s/%d", jid.bare_jid.to_string(), device_id); - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, @"$NODE_BUNDLES:$device_id", (stream, jid, id, node) => { - on_other_bundle_result(stream, jid, device_id, id, node, ignore_if_non_present); - }); - } - } - - public void ignore_device(Jid jid, int32 device_id) { - if (device_id <= 0) return; - lock (device_ignore_time) { - device_ignore_time[jid.bare_jid.to_string() + @":$device_id"] = new DateTime.now_utc(); - } - } - - public void unignore_device(Jid jid, int32 device_id) { - if (device_id <= 0) return; - lock (device_ignore_time) { - device_ignore_time.unset(jid.bare_jid.to_string() + @":$device_id"); - } - } - - public bool is_ignored_device(Jid jid, int32 device_id) { - if (device_id <= 0) return true; - lock (device_ignore_time) { - string id = jid.bare_jid.to_string() + @":$device_id"; - if (device_ignore_time.has_key(id)) { - return new DateTime.now_utc().difference(device_ignore_time[id]) < IGNORE_TIME; - } - } - return false; - } - - public void clear_device_list(XmppStream stream) { - stream.get_module(Pubsub.Module.IDENTITY).delete_node(stream, null, NODE_DEVICELIST); - } - - private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node, bool ignore_if_non_present) { - if (node == null) { - // Device not registered, shouldn't exist - if (ignore_if_non_present) { - debug("Ignoring device %s/%d: No bundle", jid.bare_jid.to_string(), device_id); - stream.get_module(IDENTITY).ignore_device(jid, device_id); - } - bundle_fetch_failed(jid, device_id); - } else { - Bundle bundle = new Bundle(node); - stream.get_module(IDENTITY).unignore_device(jid, device_id); - debug("Received bundle for %s/%d: %s", jid.bare_jid.to_string(), device_id, Base64.encode(bundle.identity_key.serialize())); - bundle_fetched(jid, device_id, bundle); - } - stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$device_id"); - } - - public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) { - bool fail = false; - int32 signed_pre_key_id = bundle.signed_pre_key_id; - ECPublicKey? signed_pre_key = bundle.signed_pre_key; - uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature; - ECPublicKey? identity_key = bundle.identity_key; - - ArrayList pre_keys = bundle.pre_keys; - if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) { - fail = true; - } else { - int pre_key_idx = Random.int_range(0, pre_keys.size); - int32 pre_key_id = pre_keys[pre_key_idx].key_id; - ECPublicKey? pre_key = pre_keys[pre_key_idx].key; - if (pre_key_id < 0 || pre_key == null) { - fail = true; - } else { - Address address = new Address(jid.bare_jid.to_string(), device_id); - try { - if (store.contains_session(address)) { - return false; - } - debug("Starting new session for encryption with %s/%d", jid.bare_jid.to_string(), device_id); - SessionBuilder builder = store.create_session_builder(address); - builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key)); - } catch (Error e) { - debug("Can't create session with %s/%d: %s", jid.bare_jid.to_string(), device_id, e.message); - fail = true; - } - address.device_id = 0; // TODO: Hack to have address obj live longer - } - } - if (fail) { - debug("Ignoring device %s/%d: Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string()); - stream.get_module(IDENTITY).ignore_device(jid, device_id); - } - return true; - } - - public void publish_bundles_if_needed(XmppStream stream, Jid jid) { - if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) { - stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result); - } - } - - private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) { - if (!Plugin.ensure_context()) return; - Map keys = new HashMap(); - ECPublicKey? identity_key = null; - int32 signed_pre_key_id = -1; - ECPublicKey? signed_pre_key = null; - SignedPreKeyRecord? signed_pre_key_record = null; - bool changed = false; - if (node == null) { - identity_key = store.identity_key_pair.public; - changed = true; - } else { - Bundle bundle = new Bundle(node); - foreach (Bundle.PreKey prekey in bundle.pre_keys) { - ECPublicKey? key = prekey.key; - if (key != null) { - keys[prekey.key_id] = (!)key; - } - } - identity_key = bundle.identity_key; - signed_pre_key_id = bundle.signed_pre_key_id; - signed_pre_key = bundle.signed_pre_key; - } - - try { - // Validate IdentityKey - if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) { - changed = true; - } - IdentityKeyPair identity_key_pair = store.identity_key_pair; - - // Validate signedPreKeyRecord + ID - if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) { - signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number - signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id); - store.store_signed_pre_key((!)signed_pre_key_record); - changed = true; - } else { - signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id); - } - - // Validate PreKeys - Set pre_key_records = new HashSet(); - foreach (var entry in keys.entries) { - if (store.contains_pre_key(entry.key)) { - PreKeyRecord record = store.load_pre_key(entry.key); - if (record.key_pair.public.compare(entry.value) == 0) { - pre_key_records.add(record); - } - } - } - int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size; - if (new_keys > 0) { - int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number - Set new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys); - pre_key_records.add_all(new_records); - foreach (PreKeyRecord record in new_records) { - store.store_pre_key(record); - } - changed = true; - } - - if (changed) { - publish_bundles.begin(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id); - } - } catch (Error e) { - warning(@"Unexpected error while publishing bundle: $(e.message)\n"); - } - stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)"); - } - - public async void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set pre_key_records, int32 device_id) throws Error { - ECKeyPair tmp; - StanzaNode bundle = new StanzaNode.build("bundle", NS_URI) - .add_self_xmlns() - .put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI) - .put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string()) - .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize())))) - .put_node(new StanzaNode.build("signedPreKeySignature", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature)))) - .put_node(new StanzaNode.build("identityKey", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize())))); - StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI); - foreach (PreKeyRecord pre_key_record in pre_key_records) { - prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI) - .put_attribute("preKeyId", pre_key_record.id.to_string()) - .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize())))); - } - bundle.put_node(prekeys); - - yield stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", "1", bundle); - yield try_make_bundle_public(stream, device_id); - - } - - private async void try_make_bundle_public(XmppStream stream, int32 device_id) { - DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, @"$NODE_BUNDLES:$device_id"); - if (data_form == null) return; - - foreach (DataForms.DataForm.Field field in data_form.fields) { - if (field.var == "pubsub#access_model" && field.get_value_string() != Pubsub.ACCESS_MODEL_OPEN) { - field.set_value_string(Pubsub.ACCESS_MODEL_OPEN); - yield stream.get_module(Pubsub.Module.IDENTITY).submit_node_config(stream, data_form, @"$NODE_BUNDLES:$device_id"); - break; - } - } - } - - public override string get_ns() { - return NS_URI; - } - - public override string get_id() { - return IDENTITY.id; - } -} - +public interface BaseStreamModule { } +} \ No newline at end of file diff --git a/plugins/omemo/src/protocol/v1_stream_module.vala b/plugins/omemo/src/protocol/v1_stream_module.vala new file mode 100644 index 00000000..3e19edbe --- /dev/null +++ b/plugins/omemo/src/protocol/v1_stream_module.vala @@ -0,0 +1,330 @@ +using Gee; +using Omemo; +using Xmpp; +using Xmpp.Xep; + +namespace Dino.Plugins.Omemo.V1 { + +private const string NS_URI = "urn:xmpp:omemo:1"; +private const string NODE_DEVICELIST = NS_URI + ":devices"; +private const string NODE_BUNDLES = NS_URI + ":bundles"; + +private const int NUM_KEYS_TO_PUBLISH = 100; + +public class DeviceListItem { + public int32 device_id; + public string label; +} + +public class StreamModule : XmppStreamModule, BaseStreamModule { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "omemo_module"); + private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE; + + public Store store { public get; internal set; } + private ConcurrentSet active_bundle_requests = new ConcurrentSet(); + private HashMap>> active_devicelist_requests = new HashMap>>(Jid.hash_func, Jid.equals_func); + private Map device_ignore_time = new HashMap(); + + public signal void device_list_loaded(Jid jid, ArrayList devices); + public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); + public signal void bundle_fetch_failed(Jid jid, int device_id); + + public StreamModule() { + if (Plugin.ensure_context()) { + this.store = Plugin.get_context().create_store(); + } + } + + public override void attach(XmppStream stream) { + stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node), null); + } + + public override void detach(XmppStream stream) {} + + public async ArrayList request_user_devicelist(XmppStream stream, Jid jid) { + var future = active_devicelist_requests[jid]; + if (future == null) { + var promise = new Promise?>(); + future = promise.future; + active_devicelist_requests[jid] = future; + + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_DEVICELIST, (stream, jid, id, node) => { + ArrayList device_list = parse_device_list(stream, jid, id, node); + promise.set_value(device_list); + active_devicelist_requests.unset(jid); + }); + } + + try { + ArrayList device_list = yield future.wait_async(); + return device_list; + } catch (FutureError error) { + warning("[V1] Future error when waiting for device list: %s", error.message); + return new ArrayList(); + } + } + + public ArrayList parse_device_list(XmppStream stream, Jid jid, string? id, StanzaNode? node_) { + ArrayList device_list = new ArrayList(); + + StanzaNode node = node_ ?? new StanzaNode.build("devices", NS_URI).add_self_xmlns(); + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) return device_list; + if (jid.equals_bare(my_jid) && store.local_registration_id != 0) { + bool am_on_devicelist = false; + foreach (StanzaNode device_node in node.get_subnodes("device")) { + int device_id = device_node.get_attribute_int("id"); + if (store.local_registration_id == device_id) { + am_on_devicelist = true; + } + } + if (!am_on_devicelist) { + debug("[V1] Not on device list, adding id"); + node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string())); + stream.get_module(Pubsub.Module.IDENTITY).publish.begin(stream, jid, NODE_DEVICELIST, id, node); + } + publish_bundles_if_needed(stream, jid); + } + + foreach (StanzaNode device_node in node.get_subnodes("device")) { + device_list.add(new DeviceListItem() { device_id = device_node.get_attribute_int("id"), label = device_node.get_attribute("label") }); + } + device_list_loaded(jid, device_list); + + return device_list; + } + + public void fetch_bundles(XmppStream stream, Jid jid, Gee.List devices) { + Address address = new Address(jid.bare_jid.to_string(), 0); + foreach(int32 device_id in devices) { + if (!is_ignored_device(jid, device_id)) { + address.device_id = device_id; + try { + if (!store.contains_session(address)) { + fetch_bundle(stream, jid, device_id); + } + } catch (Error e) { + // Ignore + } + } + } + address.device_id = 0; // TODO: Hack to have address obj live longer + } + + public void fetch_bundle(XmppStream stream, Jid jid, int device_id, bool ignore_if_non_present = true) { + if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$device_id")) { + debug("[V1] Asking for bundle for %s/%d", jid.bare_jid.to_string(), device_id); + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid.bare_jid, NODE_BUNDLES, (stream, jid, id, node) => { + on_other_bundle_result(stream, jid, device_id, id, node, ignore_if_non_present); + }, device_id.to_string()); + } + } + + public void ignore_device(Jid jid, int32 device_id) { + if (device_id <= 0) return; + lock (device_ignore_time) { + device_ignore_time[jid.bare_jid.to_string() + @":$device_id"] = new DateTime.now_utc(); + } + } + + public void unignore_device(Jid jid, int32 device_id) { + if (device_id <= 0) return; + lock (device_ignore_time) { + device_ignore_time.unset(@"$(jid.bare_jid):$device_id"); + } + } + + public bool is_ignored_device(Jid jid, int32 device_id) { + if (device_id <= 0) return true; + lock (device_ignore_time) { + string id = @"$(jid.bare_jid):$device_id"; + if (device_ignore_time.has_key(id)) { + return new DateTime.now_utc().difference(device_ignore_time[id]) < IGNORE_TIME; + } + } + return false; + } + + public void clear_device_list(XmppStream stream) { + stream.get_module(Pubsub.Module.IDENTITY).delete_node(stream, null, NODE_DEVICELIST); + } + + private void on_other_bundle_result(XmppStream stream, Jid jid, int device_id, string? id, StanzaNode? node, bool ignore_if_non_present) { + if (node == null) { + // Device not registered, shouldn't exist + if (ignore_if_non_present) { + debug("[V1] Ignoring device %s/%d: No bundle", jid.bare_jid.to_string(), device_id); + stream.get_module(IDENTITY).ignore_device(jid, device_id); + } + bundle_fetch_failed(jid, device_id); + } else { + Bundle bundle = new Bundle(node); + stream.get_module(IDENTITY).unignore_device(jid, device_id); + debug("[V1] Received bundle for %s/%d: %s", jid.bare_jid.to_string(), device_id, Base64.encode(bundle.identity_key.serialize())); + bundle_fetched(jid, device_id, bundle); + } + stream.get_module(IDENTITY).active_bundle_requests.remove(@"$(jid.bare_jid):$device_id"); + } + + public bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle) { + bool fail = false; + int32 signed_pre_key_id = bundle.signed_pre_key_id; + ECPublicKey? signed_pre_key = bundle.signed_pre_key; + uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature; + ECPublicKey? identity_key = bundle.identity_key; + + ArrayList pre_keys = bundle.pre_keys; + if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_keys.size == 0) { + fail = true; + } else { + int pre_key_idx = Random.int_range(0, pre_keys.size); + int32 pre_key_id = pre_keys[pre_key_idx].key_id; + ECPublicKey? pre_key = pre_keys[pre_key_idx].key; + if (pre_key_id < 0 || pre_key == null) { + fail = true; + } else { + Address address = new Address(jid.bare_jid.to_string(), device_id); + try { + if (store.contains_session(address)) { + return false; + } + debug("[V1] Starting new session for encryption with %s/%d", jid.bare_jid.to_string(), device_id); + SessionBuilder builder = store.create_session_builder(address); + builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key)); + } catch (Error e) { + debug("[V1] Can't create session with %s/%d: %s", jid.bare_jid.to_string(), device_id, e.message); + fail = true; + } + address.device_id = 0; // TODO: Hack to have address obj live longer + } + } + if (fail) { + debug("[V1] Ignoring device %s/%d: Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string()); + stream.get_module(IDENTITY).ignore_device(jid, device_id); + } + return true; + } + + public void publish_bundles_if_needed(XmppStream stream, Jid jid) { + if (active_bundle_requests.add(jid.bare_jid.to_string() + @":$(store.local_registration_id)")) { + stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, NODE_BUNDLES, on_self_bundle_result, store.local_registration_id.to_string()); + } + } + + private void on_self_bundle_result(XmppStream stream, Jid jid, string? id, StanzaNode? node) { + if (!Plugin.ensure_context()) return; + Map keys = new HashMap(); + ECPublicKey? identity_key = null; + int32 signed_pre_key_id = -1; + ECPublicKey? signed_pre_key = null; + SignedPreKeyRecord? signed_pre_key_record = null; + bool changed = false; + if (node == null) { + identity_key = store.identity_key_pair.public; + changed = true; + } else { + Bundle bundle = new Bundle(node); + foreach (Bundle.PreKey prekey in bundle.pre_keys) { + ECPublicKey? key = prekey.key; + if (key != null) { + keys[prekey.key_id] = (!)key; + } + } + identity_key = bundle.identity_key; + signed_pre_key_id = bundle.signed_pre_key_id; + signed_pre_key = bundle.signed_pre_key; + } + + try { + // Validate IdentityKey + if (identity_key == null || store.identity_key_pair.public.compare((!)identity_key) != 0) { + changed = true; + } + IdentityKeyPair identity_key_pair = store.identity_key_pair; + + // Validate signedPreKeyRecord + ID + if (signed_pre_key == null || signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare((!)signed_pre_key) != 0) { + signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number + signed_pre_key_record = Plugin.get_context().generate_signed_pre_key(identity_key_pair, signed_pre_key_id); + store.store_signed_pre_key((!)signed_pre_key_record); + changed = true; + } else { + signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id); + } + + // Validate PreKeys + Set pre_key_records = new HashSet(); + foreach (var entry in keys.entries) { + if (store.contains_pre_key(entry.key)) { + PreKeyRecord record = store.load_pre_key(entry.key); + if (record.key_pair.public.compare(entry.value) == 0) { + pre_key_records.add(record); + } + } + } + int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size; + if (new_keys > 0) { + int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number + Set new_records = Plugin.get_context().generate_pre_keys((uint)next_id, (uint)new_keys); + pre_key_records.add_all(new_records); + foreach (PreKeyRecord record in new_records) { + store.store_pre_key(record); + } + changed = true; + } + + if (changed) { + publish_bundles.begin(stream, (!)signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id); + } + } catch (Error e) { + warning(@"[V1] Unexpected error while publishing bundle: $(e.message)\n"); + } + stream.get_module(IDENTITY).active_bundle_requests.remove(jid.bare_jid.to_string() + @":$(store.local_registration_id)"); + } + + public async void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set pre_key_records, int32 device_id) throws Error { + ECKeyPair tmp; + StanzaNode bundle = new StanzaNode.build("bundle", NS_URI) + .add_self_xmlns() + .put_node(new StanzaNode.build("spk", NS_URI) + .put_attribute("id", signed_pre_key_record.id.to_string()) + .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize())))) + .put_node(new StanzaNode.build("spks", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature)))) + .put_node(new StanzaNode.build("ik", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.ed.data)))); + StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI); + foreach (PreKeyRecord pre_key_record in pre_key_records) { + prekeys.put_node(new StanzaNode.build("pk", NS_URI) + .put_attribute("id", pre_key_record.id.to_string()) + .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize())))); + } + bundle.put_node(prekeys); + + yield stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, NODE_BUNDLES, device_id.to_string(), bundle); + yield try_make_bundle_public(stream); + } + + private async void try_make_bundle_public(XmppStream stream) { + DataForms.DataForm? data_form = yield stream.get_module(Pubsub.Module.IDENTITY).request_node_config(stream, null, NODE_BUNDLES); + if (data_form == null) return; + + foreach (DataForms.DataForm.Field field in data_form.fields) { + if (field.var == "pubsub#access_model" && field.get_value_string() != Pubsub.ACCESS_MODEL_OPEN) { + field.set_value_string(Pubsub.ACCESS_MODEL_OPEN); + yield stream.get_module(Pubsub.Module.IDENTITY).submit_node_config(stream, data_form, NODE_BUNDLES); + break; + } + } + } + + public override string get_ns() { + return NS_URI; + } + + public override string get_id() { + return IDENTITY.id; + } +} + +} diff --git a/plugins/omemo/src/protocol/version.vala b/plugins/omemo/src/protocol/version.vala new file mode 100644 index 00000000..7e66c7df --- /dev/null +++ b/plugins/omemo/src/protocol/version.vala @@ -0,0 +1,7 @@ +namespace Dino.Plugins.Omemo { +public enum ProtocolVersion { + UNKNOWN, + LEGACY, + V1 +} +} \ No newline at end of file diff --git a/plugins/omemo/src/ui/contact_details_dialog.vala b/plugins/omemo/src/ui/contact_details_dialog.vala index 92eed72f..f45e7c5c 100644 --- a/plugins/omemo/src/ui/contact_details_dialog.vala +++ b/plugins/omemo/src/ui/contact_details_dialog.vala @@ -71,7 +71,15 @@ public class ContactDetailsDialog : Gtk.Dialog { if (identity_id < 0) return; Dino.Application? app = Application.get_default() as Dino.Application; if (app != null) { - store = app.stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store; + var legacy_module = app.stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY); + var v1_module = app.stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY); + if (legacy_module != null) { + store = legacy_module.store; + } else if (v1_module != null) { + store = v1_module.store; + } else { + return; + } } auto_accept_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true)); @@ -135,22 +143,42 @@ public class ContactDetailsDialog : Gtk.Dialog { Dino.Application app = Application.get_default() as Dino.Application; XmppStream? stream = app.stream_interactor.get_stream(account); if (stream == null) return; - StreamModule? module = stream.get_module(StreamModule.IDENTITY); - if (module == null) return; - module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => { - if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) { - Row? device = plugin.db.identity_meta.get_device(identity_id, jid.to_string(), device_id); - if (device == null) return; - if (auto_accept_switch.active) { - add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]); - } else { - add_new_fingerprint(device); + Legacy.StreamModule? legacy_module = stream.get_module(Legacy.StreamModule.IDENTITY); + V1.StreamModule? v1_module = stream.get_module(V1.StreamModule.IDENTITY); + if (legacy_module != null) { + legacy_module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => { + if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) { + Row? device = plugin.db.identity_meta.get_device(identity_id, jid.to_string(), device_id); + if (device == null) return; + if (auto_accept_switch.active) { + add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]); + } else { + add_new_fingerprint(device); + } } - } - }); + }); + } + if (v1_module != null) { + v1_module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => { + if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) { + Row? device = plugin.db.identity_meta.get_device(identity_id, jid.to_string(), device_id); + if (device == null) return; + if (auto_accept_switch.active) { + add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]); + } else { + add_new_fingerprint(device); + } + } + }); + } foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) { try { - module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false); + if (legacy_module != null) { + legacy_module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false); + } + if (v1_module != null) { + v1_module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false); + } } catch (InvalidJidError e) { warning("Ignoring device with invalid Jid: %s", e.message); } diff --git a/plugins/omemo/src/ui/device_notification_populator.vala b/plugins/omemo/src/ui/device_notification_populator.vala index 2f276f2b..1c9cefab 100644 --- a/plugins/omemo/src/ui/device_notification_populator.vala +++ b/plugins/omemo/src/ui/device_notification_populator.vala @@ -49,7 +49,7 @@ public class DeviceNotificationPopulator : NotificationPopulator, Object { } private void on_account_added(Account account) { - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { + stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { if (current_conversation != null && jid.equals(current_conversation.counterpart) && plugin.has_new_devices(current_conversation.account, current_conversation.counterpart)) { display_notification(); } diff --git a/plugins/omemo/src/ui/own_notifications.vala b/plugins/omemo/src/ui/own_notifications.vala index 7ff10e30..90c044ab 100644 --- a/plugins/omemo/src/ui/own_notifications.vala +++ b/plugins/omemo/src/ui/own_notifications.vala @@ -14,7 +14,12 @@ public class OwnNotifications { this.stream_interactor = (!)stream_interactor; this.plugin = plugin; this.account = account; - stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { + stream_interactor.module_manager.get_module(account, Legacy.StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { + if (jid.equals(account.bare_jid) && plugin.has_new_devices(account, account.bare_jid)) { + display_notification(); + } + }); + stream_interactor.module_manager.get_module(account, V1.StreamModule.IDENTITY).bundle_fetched.connect_after((jid, device_id, bundle) => { if (jid.equals(account.bare_jid) && plugin.has_new_devices(account, account.bare_jid)) { display_notification(); } diff --git a/xmpp-vala/src/module/xep/0060_pubsub.vala b/xmpp-vala/src/module/xep/0060_pubsub.vala index 279d87c3..fcb0755f 100644 --- a/xmpp-vala/src/module/xep/0060_pubsub.vala +++ b/xmpp-vala/src/module/xep/0060_pubsub.vala @@ -48,8 +48,12 @@ namespace Xmpp.Xep.Pubsub { } public delegate void OnResult(XmppStream stream, Jid jid, string? id, StanzaNode? node); - public void request(XmppStream stream, Jid jid, string node, owned OnResult listener) { // TODO multiple nodes gehen auch - Iq.Stanza request_iq = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(new StanzaNode.build("items", NS_URI).put_attribute("node", node))); + public void request(XmppStream stream, Jid jid, string node, owned OnResult listener, string? item_id = null) { // TODO multiple nodes gehen auch + var items = new StanzaNode.build("items", NS_URI).put_attribute("node", node); + if (item_id != null) { + items.put_node(new StanzaNode.build("item", NS_URI).put_attribute("id", item_id)); + } + Iq.Stanza request_iq = new Iq.Stanza.get(new StanzaNode.build("pubsub", NS_URI).add_self_xmlns().put_node(items)); request_iq.to = jid; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, request_iq, (stream, iq) => { StanzaNode event_node = iq.stanza.get_subnode("pubsub", NS_URI);