From 98a1f3ad6392482e4c7f2e83e560e3c59fecf112 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 29 Mar 2020 14:07:14 +0200 Subject: [PATCH] Initial omemo:1 support --- plugins/omemo-vala/libomemo-c | 2 +- plugins/omemo-vala/src/context.vala | 23 + plugins/omemo-vala/src/store.vala | 4 +- plugins/omemo-vala/vapi/omemo-native.vapi | 4 + plugins/omemo-vala/vapi/omemo-public.vapi | 5 +- plugins/omemo/src/jingle/jet_omemo.vala | 2 +- plugins/omemo/src/logic/database.vala | 28 +- plugins/omemo/src/logic/encrypt_state.vala | 2 +- plugins/omemo/src/logic/manager.vala | 74 ++- plugins/omemo/src/logic/trust_manager.vala | 582 +++++++++++++----- plugins/omemo/src/protocol/stream_module.vala | 8 +- .../omemo/src/protocol/v1_stream_module.vala | 14 +- plugins/omemo/src/protocol/version.vala | 22 +- 13 files changed, 578 insertions(+), 192 deletions(-) diff --git a/plugins/omemo-vala/libomemo-c b/plugins/omemo-vala/libomemo-c index 0c5c9561..06184660 160000 --- a/plugins/omemo-vala/libomemo-c +++ b/plugins/omemo-vala/libomemo-c @@ -1 +1 @@ -Subproject commit 0c5c9561b7e70d8c643460ef2c069255d15a9619 +Subproject commit 06184660790daa42433e616fa3dee730717d1c1b diff --git a/plugins/omemo-vala/src/context.vala b/plugins/omemo-vala/src/context.vala index bece27e9..46492ae4 100644 --- a/plugins/omemo-vala/src/context.vala +++ b/plugins/omemo-vala/src/context.vala @@ -1,3 +1,4 @@ +using Omemo; namespace Omemo { public class Context { @@ -81,6 +82,12 @@ public class Context { return res; } + public SignalMessage deserialize_signal_message_omemo(uint8[] data) throws Error { + SignalMessage res; + throw_by_code(signal_message_deserialize_omemo(out res, data, native_context)); + return res; + } + public SignalMessage copy_signal_message(CiphertextMessage original) throws Error { SignalMessage res; throw_by_code(signal_message_copy(out res, (SignalMessage) original, native_context)); @@ -93,11 +100,27 @@ public class Context { return res; } + public PreKeySignalMessage deserialize_pre_key_signal_message_omemo(uint8[] data, int32 sid) throws Error { + PreKeySignalMessage res; + throw_by_code(pre_key_signal_message_deserialize_omemo(out res, data, sid, native_context)); + return res; + } + public PreKeySignalMessage copy_pre_key_signal_message(CiphertextMessage original) throws Error { PreKeySignalMessage res; throw_by_code(pre_key_signal_message_copy(out res, (PreKeySignalMessage) original, native_context)); return res; } + + public uint8[] derive_payload_secret(uint8[] ikm, size_t output_len) throws Error { + NativeHkdfContext context; + throw_by_code(NativeHkdfContext.create(out context, 4, native_context)); + uint8[] empty = new uint8[32]; + Memory.set(empty, 0, empty.length); + uint8[] output = null; + context.derive_secrets(out output, ikm, empty, "OMEMO Payload".data, output_len); + return output; + } } } diff --git a/plugins/omemo-vala/src/store.vala b/plugins/omemo-vala/src/store.vala index b1861d93..632d9810 100644 --- a/plugins/omemo-vala/src/store.vala +++ b/plugins/omemo-vala/src/store.vala @@ -377,9 +377,9 @@ public class Store : Object { throw_by_code(Protocol.Session.delete_session(native_context, address)); } - public SessionRecord load_session(Address other) throws Error { + public SessionRecord load_session(Address other, int preferred_version = 2) throws Error { SessionRecord record; - throw_by_code(Protocol.Session.load_session(native_context, out record, other)); + throw_by_code(Protocol.Session.load_session(native_context, out record, other, preferred_version)); return record; } diff --git a/plugins/omemo-vala/vapi/omemo-native.vapi b/plugins/omemo-vala/vapi/omemo-native.vapi index 3010a9c3..05fbbc75 100644 --- a/plugins/omemo-vala/vapi/omemo-native.vapi +++ b/plugins/omemo-vala/vapi/omemo-native.vapi @@ -239,12 +239,16 @@ namespace Omemo { public static int session_cipher_create(out SessionCipher cipher, NativeStoreContext store, Address remote_address, NativeContext global_context); [CCode (cname = "pre_key_signal_message_deserialize", cheader_filename = "omemo/protocol.h")] public static int pre_key_signal_message_deserialize(out PreKeySignalMessage message, uint8[] data, NativeContext global_context); + [CCode (cname = "pre_key_signal_message_deserialize_omemo", cheader_filename = "omemo/protocol.h")] + public static int pre_key_signal_message_deserialize_omemo(out PreKeySignalMessage message, uint8[] data, uint32 remote_registration_id, NativeContext global_context); [CCode (cname = "pre_key_signal_message_copy", cheader_filename = "omemo/protocol.h")] public static int pre_key_signal_message_copy(out PreKeySignalMessage message, PreKeySignalMessage other_message, NativeContext global_context); [CCode (cname = "signal_message_create", cheader_filename = "omemo/protocol.h")] public static int signal_message_create(out SignalMessage message, uint8 message_version, uint8[] mac_key, ECPublicKey sender_ratchet_key, uint32 counter, uint32 previous_counter, uint8[] ciphertext, ECPublicKey sender_identity_key, ECPublicKey receiver_identity_key, NativeContext global_context); [CCode (cname = "signal_message_deserialize", cheader_filename = "omemo/protocol.h")] public static int signal_message_deserialize(out SignalMessage message, uint8[] data, NativeContext global_context); + [CCode (cname = "signal_message_deserialize_omemo", cheader_filename = "omemo/protocol.h")] + public static int signal_message_deserialize_omemo(out SignalMessage message, uint8[] data, NativeContext global_context); [CCode (cname = "signal_message_copy", cheader_filename = "omemo/protocol.h")] public static int signal_message_copy(out SignalMessage message, SignalMessage other_message, NativeContext global_context); [CCode (cname = "curve_generate_key_pair", cheader_filename = "omemo/curve.h")] diff --git a/plugins/omemo-vala/vapi/omemo-public.vapi b/plugins/omemo-vala/vapi/omemo-public.vapi index a2ce23b5..63d92170 100644 --- a/plugins/omemo-vala/vapi/omemo-public.vapi +++ b/plugins/omemo-vala/vapi/omemo-public.vapi @@ -98,6 +98,7 @@ namespace Omemo { public void process_pre_key_bundle(PreKeyBundle pre_key_bundle) throws GLib.Error { throw_by_code(process_pre_key_bundle_(pre_key_bundle)); } + public int version { get; set; } } [Compact] @@ -299,7 +300,9 @@ namespace Omemo { return res.data; } public int get_remote_registration_id(out uint32 remote_id); - public int get_session_version(uint32 version); + public int get_session_version(out uint32 version); + + public uint32 version { get; set; } [CCode (has_target = false)] public delegate int DecryptionCallback(SessionCipher cipher, Buffer plaintext, void* decrypt_context); diff --git a/plugins/omemo/src/jingle/jet_omemo.vala b/plugins/omemo/src/jingle/jet_omemo.vala index e396271b..6cfc3103 100644 --- a/plugins/omemo/src/jingle/jet_omemo.vala +++ b/plugins/omemo/src/jingle/jet_omemo.vala @@ -104,7 +104,7 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding { .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); + plugin.trust_manager.encrypt_key(header_node, null, security_params.secret.transport_key, null, local_full_jid.bare_jid, new ArrayList.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account); security.put_node(encrypted_node); } diff --git a/plugins/omemo/src/logic/database.vala b/plugins/omemo/src/logic/database.vala index 429b5f0b..a5de8852 100644 --- a/plugins/omemo/src/logic/database.vala +++ b/plugins/omemo/src/logic/database.vala @@ -6,7 +6,7 @@ using Dino.Entities; namespace Dino.Plugins.Omemo { public class Database : Qlite.Database { - private const int VERSION = 5; + private const int VERSION = 6; public class IdentityMetaTable : Table { //Default to provide backwards compatability @@ -20,10 +20,12 @@ public class Database : Qlite.Database { public Column last_active = new Column.Long("last_active"); public Column last_message_untrusted = new Column.Long("last_message_untrusted") { min_version = 5 }; public Column last_message_undecryptable = new Column.Long("last_message_undecryptable") { min_version = 5 }; + public Column version = new Column.Integer("version") { default = "0", min_version = 6 }; + public Column label = new Column.Text("label") { min_version = 6 }; internal IdentityMetaTable(Database db) { base(db, "identity_meta"); - init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active, last_message_untrusted, last_message_undecryptable}); + init({identity_id, address_name, device_id, identity_key_public_base64, trusted_identity, trust_level, now_active, last_active, last_message_untrusted, last_message_undecryptable, version, label}); index("identity_meta_idx", {identity_id, address_name, device_id}, true); index("identity_meta_list_idx", {identity_id, address_name}); } @@ -36,8 +38,8 @@ public class Database : Qlite.Database { return select().with(this.identity_id, "=", identity_id).with(this.device_id, "=", device_id); } - public void insert_device_list(int32 identity_id, string address_name, ArrayList devices) { - update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform(); + public void insert_legacy_device_list(int32 identity_id, string address_name, ArrayList devices) { + update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).with(this.version, "=", ProtocolVersion.LEGACY.to_int()).set(now_active, false).perform(); foreach (int32 device_id in devices) { upsert() .value(this.identity_id, identity_id, true) @@ -49,6 +51,21 @@ public class Database : Qlite.Database { } } + public void insert_v1_device_list(int32 identity_id, string address_name, ArrayList devices) { + update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).with(this.version, "=", ProtocolVersion.V1.to_int()).set(now_active, false).perform(); + foreach (V1.DeviceListItem device_id in devices) { + upsert() + .value(this.identity_id, identity_id, true) + .value(this.address_name, address_name, true) + .value(this.device_id, device_id.device_id, true) + .value(this.now_active, true) + .value(this.last_active, (long) new DateTime.now_utc().to_unix()) + .value(this.version, ProtocolVersion.V1.to_int()) + .value(this.label, device_id.label) + .perform(); + } + } + public int64 insert_device_bundle(int32 identity_id, string address_name, int device_id, Bundle bundle, TrustLevel trust) { if (bundle == null || bundle.identity_key == null) return -1; // Do not replace identity_key if it was known before, it should never change! @@ -66,7 +83,7 @@ public class Database : Qlite.Database { .value(this.trust_level, trust).perform(); } - public int64 insert_device_session(int32 identity_id, string address_name, int device_id, string identity_key, TrustLevel trust) { + public int64 insert_device_session(int32 identity_id, string address_name, int device_id, string identity_key, ProtocolVersion version, TrustLevel trust) { RowOption row = with_address(identity_id, address_name).with(this.device_id, "=", device_id).single().row(); if (row.is_present() && row[identity_key_public_base64] != null && row[identity_key_public_base64] != identity_key) { critical("Tried to change the identity key for a known device id. Likely an attack."); @@ -77,6 +94,7 @@ public class Database : Qlite.Database { .value(this.address_name, address_name, true) .value(this.device_id, device_id, true) .value(this.identity_key_public_base64, identity_key) + .value(this.version, version.to_int()) .value(this.trust_level, trust).perform(); } diff --git a/plugins/omemo/src/logic/encrypt_state.vala b/plugins/omemo/src/logic/encrypt_state.vala index fd72faf4..919a9bf1 100644 --- a/plugins/omemo/src/logic/encrypt_state.vala +++ b/plugins/omemo/src/logic/encrypt_state.vala @@ -17,7 +17,7 @@ public class EncryptState { public bool own_list { get; internal set; } public string to_string() { - return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; + return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists), own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; } } diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala index 6e67aa6d..200a8bbe 100644 --- a/plugins/omemo/src/logic/manager.vala +++ b/plugins/omemo/src/logic/manager.vala @@ -148,18 +148,26 @@ public class Manager : StreamInteractionModule, Object { } else { debug("delaying message %s", state.to_string()); - // TODO: V1 support if (state.waiting_own_sessions > 0) { - legacy_module.fetch_bundles((!)stream, conversation.account.bare_jid, trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid)); + var devices = trust_manager.get_trusted_devices(conversation.account, conversation.account.bare_jid); + var list = devices.filter((d) => d.version == ProtocolVersion.LEGACY).fold>((d, list) => { list.add(d.device_id); return list; }, new ArrayList()); + legacy_module.fetch_bundles((!)stream, conversation.account.bare_jid, list); + list = devices.filter((d) => d.version == ProtocolVersion.LEGACY).fold>((d, list) => { list.add(d.device_id); return list; }, new ArrayList()); + v1_module.fetch_bundles((!)stream, conversation.account.bare_jid, list); } if (state.waiting_other_sessions > 0 && message.counterpart != null) { foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { - legacy_module.fetch_bundles((!)stream, jid, trust_manager.get_trusted_devices(conversation.account, jid)); + var devices = trust_manager.get_trusted_devices(conversation.account, jid); + var list = devices.filter((d) => d.version == ProtocolVersion.LEGACY).fold>((d, list) => { list.add(d.device_id); return list; }, new ArrayList()); + legacy_module.fetch_bundles((!)stream, jid, list); + list = devices.filter((d) => d.version == ProtocolVersion.V1).fold>((d, list) => { list.add(d.device_id); return list; }, new ArrayList()); + v1_module.fetch_bundles((!)stream, jid, list); } } if (state.waiting_other_devicelists > 0 && message.counterpart != null) { foreach(Jid jid in get_occupants(((!)message.counterpart).bare_jid, conversation.account)) { legacy_module.request_user_devicelist.begin((!)stream, jid); + v1_module.request_user_devicelist.begin((!)stream, jid); } } } @@ -179,22 +187,22 @@ public class Manager : StreamInteractionModule, Object { 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.device_list_loaded.connect((jid, devices) => on_legacy_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.device_list_loaded.connect((jid, devices) => on_v1_device_list_loaded(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); } - private void on_device_list_loaded(Account account, Jid jid, ArrayList device_list) { - debug("received device list for %s from %s", account.bare_jid.to_string(), jid.to_string()); + private void on_legacy_device_list_loaded(Account account, Jid jid, ArrayList device_list) { + debug("[Legacy] received device list for %s from %s", account.bare_jid.to_string(), jid.to_string()); XmppStream? stream = stream_interactor.get_stream(account); if (stream == null) { @@ -209,7 +217,7 @@ public class Manager : StreamInteractionModule, Object { if (identity_id < 0) return; //Update meta database - db.identity_meta.insert_device_list(identity_id, jid.bare_jid.to_string(), device_list); + db.identity_meta.insert_legacy_device_list(identity_id, jid.bare_jid.to_string(), device_list); //Fetch the bundle for each new device int inc = 0; @@ -225,6 +233,45 @@ public class Manager : StreamInteractionModule, Object { debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string()); } + on_after_devicelist_laoded(account, jid, identity_id); + } + + private void on_v1_device_list_loaded(Account account, Jid jid, ArrayList device_list) { + debug("[V1] received device list for %s from %s", account.bare_jid.to_string(), jid.to_string()); + + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) { + return; + } + V1.StreamModule? module = ((!)stream).get_module(V1.StreamModule.IDENTITY); + if (module == null) { + return; + } + + int identity_id = db.identity.get_id(account.id); + if (identity_id < 0) return; + + //Update meta database + db.identity_meta.insert_v1_device_list(identity_id, jid.bare_jid.to_string(), device_list); + + //Fetch the bundle for each new device + int inc = 0; + foreach (Row row in db.identity_meta.get_unknown_devices(identity_id, jid.bare_jid.to_string())) { + try { + module.fetch_bundle(stream, new Jid(row[db.identity_meta.address_name]), row[db.identity_meta.device_id], false); + inc++; + } catch (InvalidJidError e) { + warning("Ignoring device with invalid Jid: %s", e.message); + } + } + if (inc > 0) { + debug("new bundles %i/%i for %s", inc, device_list.size, jid.to_string()); + } + + on_after_devicelist_laoded(account, jid, identity_id); + } + + private void on_after_devicelist_laoded(Account account, Jid jid, int identity_id) { //Create an entry for the jid in the account table if one does not exist already if (db.trust.select().with(db.trust.identity_id, "=", identity_id).with(db.trust.address_name, "=", jid.bare_jid.to_string()).count() == 0) { db.trust.insert().value(db.trust.identity_id, identity_id).value(db.trust.address_name, jid.bare_jid.to_string()).value(db.trust.blind_trust, true).perform(); @@ -254,7 +301,6 @@ public class Manager : StreamInteractionModule, Object { if (conv == null) continue; stream_interactor.get_module(MessageProcessor.IDENTITY).send_xmpp_message(msg, (!)conv, true); } - } private void on_bundle_fetched(Account account, Jid jid, int32 device_id, Bundle bundle) { @@ -288,7 +334,7 @@ public class Manager : StreamInteractionModule, Object { if (should_start_session(account, jid)) { XmppStream? stream = stream_interactor.get_stream(account); if (stream != null) { - Legacy.StreamModule? module = ((!)stream).get_module(Legacy.StreamModule.IDENTITY); + BaseStreamModule? module = get_module_from_stream(stream, bundle.version); if (module != null) { module.start_session(stream, jid, device_id, bundle); } @@ -297,6 +343,14 @@ public class Manager : StreamInteractionModule, Object { continue_message_sending(account, jid); } + private BaseStreamModule? get_module_from_stream(XmppStream stream, ProtocolVersion version) { + switch (version) { + case ProtocolVersion.LEGACY: return stream.get_module(Legacy.StreamModule.IDENTITY); + case ProtocolVersion.V1: return stream.get_module(V1.StreamModule.IDENTITY); + } + return null; + } + private bool should_start_session(Account account, Jid jid) { lock (message_states) { foreach (Entities.Message msg in message_states.keys) { diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala index 5e3915da..8e7f4d7d 100644 --- a/plugins/omemo/src/logic/trust_manager.vala +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -6,6 +6,11 @@ using Qlite; namespace Dino.Plugins.Omemo { +public class TrustedDevice { + public int32 device_id; + public ProtocolVersion version; +} + public class TrustManager { public signal void bad_message_state_updated(Account account, Jid jid, int device_id); @@ -69,7 +74,7 @@ public class TrustManager { } } - private StanzaNode create_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error { + private StanzaNode create_legacy_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error { 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); @@ -80,9 +85,46 @@ 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 { + private void append_v1_encrypted_key_node(StanzaNode v1_header_node, uint8[] key, Address address, Store store) throws GLib.Error { + SessionCipher cipher = store.create_session_cipher(address); + cipher.version = 4; + uint32 version; + cipher.get_session_version(out version); + if (version != 4) { + warning(@"OMEMO:1 not configured: Session version: $(version) != 4 for $(address.name):$(address.device_id)"); + throw new Error(-1, ErrorCode.UNKNOWN, "Session is outdated"); + } + 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", V1.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("kex", "true"); + bool matched = false; + foreach (Xmpp.StanzaNode keys in v1_header_node.get_subnodes("keys")) { + if (keys.get_attribute("jid") == address.name) { + keys.put_node(key_node); + matched = true; + break; + } + } + if (!matched) { + v1_header_node.put_node(new StanzaNode.build("keys", V1.NS_URI).put_attribute("jid", address.name).put_node(key_node)); + } + } + + private BaseStreamModule? pick_module(Legacy.StreamModule? legacy_module, V1.StreamModule? v1_module, ProtocolVersion version) { + switch (version) { + case ProtocolVersion.LEGACY: return legacy_module; + case ProtocolVersion.V1: return v1_module; + } + return null; + } + + internal EncryptState encrypt_key(StanzaNode legacy_header_node, StanzaNode v1_header_node, uint8[] legacy_keytag, uint8[] v1_keymac, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) throws Error { EncryptState status = new EncryptState(); - Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY); + Legacy.StreamModule? legacy_module = stream.get_module(Legacy.StreamModule.IDENTITY); + V1.StreamModule? v1_module = stream.get_module(V1.StreamModule.IDENTITY); //Check we have the bundles and device lists needed to send the message if (!is_known_address(account, self_jid)) return status; @@ -103,16 +145,25 @@ public class TrustManager { //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)) { + foreach(TrustedDevice device in get_trusted_devices(account, recipient)) { + if (pick_module(legacy_module, v1_module, device.version).is_ignored_device(recipient, device.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); + address.device_id = (int) device.device_id; + switch (device.version) { + case ProtocolVersion.LEGACY: + if (legacy_header_node == null) continue; + StanzaNode key_node = create_legacy_encrypted_key_node(legacy_keytag, address, legacy_module.store); + legacy_header_node.put_node(key_node); + break; + case ProtocolVersion.V1: + if (v1_header_node == null) continue; + append_v1_encrypted_key_node(v1_header_node, v1_keymac, address, v1_module.store); + break; + } status.other_success++; } catch (Error e) { if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; @@ -123,16 +174,25 @@ public class TrustManager { // 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)) { + foreach(TrustedDevice device in get_trusted_devices(account, self_jid)) { + if (pick_module(legacy_module, v1_module, device.version).is_ignored_device(self_jid, device.device_id)) { status.own_lost++; continue; } - if (device_id != module.store.local_registration_id) { - address.device_id = (int) device_id; + if (device.device_id != v1_module.store.local_registration_id) { + address.device_id = (int) device.device_id; try { - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); + switch (device.version) { + case ProtocolVersion.LEGACY: + if (legacy_header_node == null) continue; + StanzaNode key_node = create_legacy_encrypted_key_node(legacy_keytag, address, legacy_module.store); + legacy_header_node.put_node(key_node); + break; + case ProtocolVersion.V1: + if (v1_header_node == null) continue; + append_v1_encrypted_key_node(v1_header_node, v1_keymac, address, v1_module.store); + break; + } status.own_success++; } catch (Error e) { if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; @@ -144,40 +204,81 @@ public class TrustManager { return status; } + private static uint8[] hmac(ChecksumType type, uint8[] key, uint8[] data) { + Hmac hmac = new Hmac(type, key); + hmac.update(data); + uint8[] res = new uint8[type.get_length()]; + size_t resl = res.length; + hmac.get_digest(res, ref resl); + return res; + } + 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; if (message.to == null) return status; - Legacy.StreamModule module = stream.get_module(Legacy.StreamModule.IDENTITY); + Legacy.StreamModule legacy_module = stream.get_module(Legacy.StreamModule.IDENTITY); + Legacy.StreamModule v1_module = stream.get_module(Legacy.StreamModule.IDENTITY); try { - //Create a key and use it to encrypt the message - uint8[] key = new uint8[16]; - Plugin.get_context().randomize(key); - uint8[] iv = new uint8[16]; - Plugin.get_context().randomize(iv); + //Create legacy key and use it to encrypt the message + uint8[] legacy_key = new uint8[16]; + Plugin.get_context().randomize(legacy_key); + uint8[] legacy_iv = new uint8[12]; + Plugin.get_context().randomize(legacy_iv); + uint8[] legacy_aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, legacy_key, legacy_iv, message.body.data); + uint8[] legacy_ciphertext = legacy_aes_encrypt_result[0:legacy_aes_encrypt_result.length-16]; + uint8[] legacy_tag = legacy_aes_encrypt_result[legacy_aes_encrypt_result.length-16:legacy_aes_encrypt_result.length]; + uint8[] legacy_keytag = new uint8[legacy_key.length + legacy_tag.length]; + Memory.copy(legacy_keytag, legacy_key, legacy_key.length); + Memory.copy((uint8*)legacy_keytag + legacy_key.length, legacy_tag, legacy_tag.length); - uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data); - uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16]; - uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length]; - uint8[] keytag = new uint8[key.length + tag.length]; - Memory.copy(keytag, key, key.length); - Memory.copy((uint8*)keytag + key.length, tag, tag.length); + // Same for v1 + uint8[] v1_key = new uint8[32]; + Plugin.get_context().randomize(v1_key); + uint8[] v1_hkdf = Plugin.get_context().derive_payload_secret(v1_key, 80); + uint8[] v1_enc_key = v1_hkdf[0:32]; + uint8[] v1_auth_key = v1_hkdf[32:64]; + uint8[] v1_iv = v1_hkdf[64:80]; + // TODO: build proper sce message + string v1_sce_content = @"$(message.body.replace("&", "&").replace("\"", """).replace("'", "'").replace("<", "<").replace(">", ">"))nope"; + uint8[] v1_ciphertext = aes_encrypt(Cipher.AES_CBC_PKCS5, v1_enc_key, v1_iv, v1_sce_content.data); + uint8[] v1_hmac = hmac(ChecksumType.SHA256, v1_auth_key, v1_sce_content.data); + uint8[] v1_keymac = new uint8[v1_key.length + v1_hmac.length]; + Memory.copy(v1_keymac, v1_key, v1_key.length); + Memory.copy((uint8*)v1_keymac + v1_key.length, v1_hmac, v1_hmac.length); - StanzaNode header_node; - 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", Legacy.NS_URI) - .put_node(new StanzaNode.text(Base64.encode(iv))))) - .put_node(new StanzaNode.build("payload", Legacy.NS_URI) - .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); + StanzaNode legacy_header_node = null, legacy_encrypted_node = null; + if (legacy_module != null) { + legacy_encrypted_node = new StanzaNode.build("encrypted", Legacy.NS_URI).add_self_xmlns() + .put_node(legacy_header_node = new StanzaNode.build("header", Legacy.NS_URI) + .put_attribute("sid", legacy_module.store.local_registration_id.to_string()) + .put_node(new StanzaNode.build("iv", Legacy.NS_URI) + .put_node(new StanzaNode.text(Base64.encode(legacy_iv))))) + .put_node(new StanzaNode.build("payload", Legacy.NS_URI) + .put_node(new StanzaNode.text(Base64.encode(legacy_ciphertext)))); + } - status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account); + StanzaNode v1_header_node = null, v1_encrypted_node = null; + if (v1_module != null) { + v1_encrypted_node = new StanzaNode.build("encrypted", V1.NS_URI).add_self_xmlns() + .put_node(v1_header_node = new StanzaNode.build("header", V1.NS_URI) + .put_attribute("sid", v1_module.store.local_registration_id.to_string())) + .put_node(new StanzaNode.build("payload", V1.NS_URI) + .put_node(new StanzaNode.text(Base64.encode(v1_ciphertext)))); + } - message.stanza.put_node(encrypted_node); - Xep.ExplicitEncryption.add_encryption_tag_to_message(message, Legacy.NS_URI, "OMEMO"); + status = encrypt_key(legacy_header_node, v1_header_node, legacy_keytag, v1_keymac, self_jid, recipients, stream, account); + + if (legacy_header_node.get_subnodes("key").size > 0) { + message.stanza.put_node(legacy_encrypted_node); + Xep.ExplicitEncryption.add_encryption_tag_to_message(message, Legacy.NS_URI, "OMEMO"); + } + if (v1_header_node.get_subnodes("keys").size > 0) { + message.stanza.put_node(v1_encrypted_node); + Xep.ExplicitEncryption.add_encryption_tag_to_message(message, V1.NS_URI, "OMEMO"); + } message.body = "[This message is OMEMO encrypted]"; status.encrypted = true; } catch (Error e) { @@ -194,13 +295,13 @@ public class TrustManager { return db.identity_meta.with_address(identity_id, jid.to_string()).with(db.identity_meta.last_active, ">", 0).count() > 0; } - public Gee.List get_trusted_devices(Account account, Jid jid) { - Gee.List devices = new ArrayList(); + public Gee.List get_trusted_devices(Account account, Jid jid) { + Gee.List devices = new ArrayList(); int identity_id = db.identity.get_id(account.id); if (identity_id < 0) return devices; foreach (Row device in db.identity_meta.get_trusted_devices(identity_id, jid.bare_jid.to_string())) { if(device[db.identity_meta.trust_level] != TrustLevel.UNKNOWN || device[db.identity_meta.identity_key_public_base64] == null) - devices.add(device[db.identity_meta.device_id]); + devices.add(new TrustedDevice() { device_id = device[db.identity_meta.device_id], version = ProtocolVersion.from_int(device[db.identity_meta.version]) }); } return devices; } @@ -277,12 +378,14 @@ public class TrustManager { } public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - Legacy.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY); - Store store = module.store; + Store store = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY).store; - 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; + StanzaNode? legacy_encrypted = stanza.stanza.get_subnode("encrypted", Legacy.NS_URI); + StanzaNode? v1_encrypted = stanza.stanza.get_subnode("encrypted", V1.NS_URI); + if ((legacy_encrypted == null && v1_encrypted == null) || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; + if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == V1.NS_URI) { + message.body = "[This message is OMEMO:1 encrypted]"; // TODO temporary + } if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == Legacy.NS_URI) { message.body = "[This message is OMEMO encrypted]"; // TODO temporary } @@ -290,132 +393,60 @@ public class TrustManager { int identity_id = db.identity.get_id(conversation.account.id); MessageFlag flag = new MessageFlag(); stanza.add_flag(flag); - StanzaNode? _header = encrypted.get_subnode("header"); - if (_header == null) return false; - StanzaNode header = (!)_header; - int sid = header.get_attribute_int("sid"); - if (sid <= 0) return false; - var our_nodes = new ArrayList(); - foreach (StanzaNode key_node in header.get_subnodes("key")) { - debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id); - if (key_node.get_attribute_int("rid") == store.local_registration_id) { - our_nodes.add(key_node); + int sid = -1; + StanzaNode? v1_header = null; + if (v1_encrypted != null) { v1_header = v1_encrypted.get_subnode("header"); } + if (v1_header != null) { sid = v1_header.get_attribute_int("sid"); } + StanzaNode? our_v1_node = null; + if (v1_header != null && sid > 0) { + foreach (StanzaNode keys_node in v1_header.get_subnodes("keys")) { + if (keys_node.get_attribute("jid") == conversation.account.bare_jid.to_string()) { + foreach (StanzaNode key_node in keys_node.get_subnodes("key")) { + debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id); + if (key_node.get_attribute_int("rid") == store.local_registration_id) { + our_v1_node = key_node; + } + } + } + } + } + if (our_v1_node != null) { + string? payload = v1_encrypted.get_deep_string_content("payload"); + string? key_node_content = our_v1_node.get_string_content(); + if (key_node_content != null) { + if (yield decrypt_v1_key_node(message, stanza, conversation, our_v1_node, identity_id, sid, payload)) { + return false; + } } } - foreach (StanzaNode key_node in our_nodes) { - 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 (iv_node == null || key_node_content == null) continue; - uint8[] key; + StanzaNode? legacy_header = null; + if (legacy_encrypted != null) { legacy_header = legacy_encrypted.get_subnode("header"); } + sid = -1; + if (legacy_header != null) { sid = legacy_header.get_attribute_int("sid"); } + + var our_legacy_nodes = new ArrayList(); + if (legacy_header != null && sid > 0) { + foreach (StanzaNode key_node in legacy_header.get_subnodes("key")) { + debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id); + if (key_node.get_attribute_int("rid") == store.local_registration_id) { + our_legacy_nodes.add(key_node); + } + } + } + + foreach (StanzaNode key_node in our_legacy_nodes) { + string? payload = legacy_encrypted.get_deep_string_content("payload"); + string? iv_node = legacy_header.get_deep_string_content("iv"); + if (iv_node == null || key_node.get_string_content() == null) continue; uint8[] iv = Base64.decode((!)iv_node); - Gee.List possible_jids = new ArrayList(); - if (conversation.type_ == Conversation.Type.CHAT) { - possible_jids.add(stanza.from.bare_jid); - } else { - Jid? real_jid = message.real_jid; - if (real_jid != null) { - possible_jids.add(real_jid.bare_jid); - } else if (key_node.get_attribute_bool("prekey")) { - // pre key messages do store the identity key, so we can use that to find the real jid - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - string identity_key = Base64.encode(msg.identity_key.serialize()); - foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { - try { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } catch (InvalidJidError e) { - warning("Ignoring invalid jid from database: %s", e.message); - } - } - if (possible_jids.size != 1) { - continue; - } - } else { - // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id - foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) { - try { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } catch (InvalidJidError e) { - warning("Ignoring invalid jid from database: %s", e.message); - } - } - } - } - - if (possible_jids.size == 0) { - debug("Received message from unknown entity with device id %d", sid); - } - - foreach (Jid possible_jid in possible_jids) { - try { - Address address = new Address(possible_jid.to_string(), sid); - if (key_node.get_attribute_bool("prekey")) { - Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid); - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - string identity_key = Base64.encode(msg.identity_key.serialize()); - if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { - if (device[db.identity_meta.identity_key_public_base64] != identity_key) { - critical("Tried to use a different identity key for a known device id."); - continue; - } - } else { - debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid); - bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true); - if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { - critical("Failed learning a device."); - continue; - } - XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (device == null && stream != null) { - module.request_user_devicelist.begin(stream, possible_jid); - } - } - debug("Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_pre_key_signal_message(msg); - // TODO: Finish session - } else { - debug("Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid); - SignalMessage msg = 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 - - 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; - } - flag.decrypted = true; - } catch (Error e) { - debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message); - continue; - } - - // If we figured out which real jid a message comes from due to decryption working, save it - if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { - message.real_jid = possible_jid; - } + if (decrypt_legacy_key_node(message, stanza, conversation, key_node, identity_id, sid, payload, iv)) { return false; } } - if (our_nodes.size == 0) { + if (our_legacy_nodes.size == 0 && our_v1_node == null) { db.identity_meta.update_last_message_undecryptable(identity_id, sid, message.time); trust_manager.bad_message_state_updated(conversation.account, message.from, sid); } @@ -424,6 +455,233 @@ public class TrustManager { return false; } + private async bool decrypt_v1_key_node(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation, StanzaNode key_node, int identity_id, int sid, string? payload) { + V1.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, V1.StreamModule.IDENTITY); + Store store = module.store; + string? key_node_content = key_node.get_string_content(); + Gee.List possible_jids = new ArrayList(); + if (conversation.type_ == Conversation.Type.CHAT) { + possible_jids.add(stanza.from.bare_jid); + } else { + Jid? real_jid = message.real_jid; + if (real_jid != null) { + possible_jids.add(real_jid.bare_jid); + } else if (key_node.get_attribute_bool("kex")) { + // pre key messages do store the identity key, so we can use that to find the real jid + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message_omemo(Base64.decode((!)key_node_content), sid); + string identity_key = Base64.encode(msg.identity_key.serialize()); + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("[V1] Ignoring invalid jid from database: %s", e.message); + } + } + if (possible_jids.size != 1) { + return false; + } + } else { + // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("[V1] Ignoring invalid jid from database: %s", e.message); + } + } + } + } + uint8[] key; + foreach (Jid possible_jid in possible_jids) { + try { + Address address = new Address(possible_jid.to_string(), sid); + if (key_node.get_attribute_bool("kex")) { + Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid); + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message_omemo(Base64.decode((!)key_node_content), sid); + string identity_key = Base64.encode(msg.identity_key.serialize()); + if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { + if (device[db.identity_meta.identity_key_public_base64] != identity_key) { + critical("[V1] Tried to use a different identity key for a known device id."); + continue; + } + } else { + debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid); + bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true); + if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, ProtocolVersion.V1, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { + critical("[V1] Failed learning a device."); + continue; + } + XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (device == null && stream != null) { + module.request_user_devicelist.begin(stream, possible_jid); + } + } + debug("[V1] Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid); + SessionCipher cipher = store.create_session_cipher(address); + cipher.version = 4; + key = cipher.decrypt_pre_key_signal_message(msg); + // TODO: Finish session + } else { + debug("[V1] Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid); + SignalMessage msg = Plugin.get_context().deserialize_signal_message_omemo(Base64.decode((!)key_node_content)); + SessionCipher cipher = store.create_session_cipher(address); + cipher.version = 4; + key = cipher.decrypt_signal_message(msg); + } + //address.device_id = 0; // TODO: Hack to have address obj live longer + + if (key.length != 64) { + critical("[V1] Key length is invalid."); + continue; + } + if (payload != null) { + uint8[] ciphertext = Base64.decode(payload); + uint8[] ikm = key[0:32]; + uint8[] mac = key[32:64]; + uint8[] hkdf = Plugin.get_context().derive_payload_secret(ikm, 80); + uint8[] enc_key = hkdf[0:32]; + uint8[] auth_key = hkdf[32:64]; + uint8[] iv = hkdf[64:80]; + uint8[] decrypted = aes_decrypt(Cipher.AES_CBC_PKCS5, enc_key, iv, ciphertext); + uint8[] mac_cmp = hmac(ChecksumType.SHA256, auth_key, decrypted); + if (Memory.cmp(mac_cmp, mac, mac.length) != 0) { + critical("[V1] HMAC mismatches."); + continue; + } + + // TODO: SCE + StanzaNode content = yield new StanzaReader.for_buffer(decrypted).read_stanza_node(); + + message.body = content.get_subnode("payload").get_subnode("body", "jabber:client").get_string_content(); + message_device_id_map[message] = address.device_id; + message.encryption = Encryption.OMEMO; + } + MessageFlag.get_flag(stanza).decrypted = true; + } catch (Error e) { + debug("[V1] Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message); + continue; + } + + // If we figured out which real jid a message comes from due to decryption working, save it + if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { + message.real_jid = possible_jid; + } + return true; + } + return false; + } + + private bool decrypt_legacy_key_node(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation, StanzaNode key_node, int identity_id, int sid, string? payload, uint8[] iv) { + Legacy.StreamModule module = stream_interactor.module_manager.get_module(conversation.account, Legacy.StreamModule.IDENTITY); + Store store = module.store; + Gee.List possible_jids = new ArrayList(); + string? key_node_content = key_node.get_string_content(); + if (conversation.type_ == Conversation.Type.CHAT) { + possible_jids.add(stanza.from.bare_jid); + } else { + Jid? real_jid = message.real_jid; + if (real_jid != null) { + possible_jids.add(real_jid.bare_jid); + } else if (key_node.get_attribute_bool("prekey")) { + // pre key messages do store the identity key, so we can use that to find the real jid + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); + string identity_key = Base64.encode(msg.identity_key.serialize()); + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("[Legacy] Ignoring invalid jid from database: %s", e.message); + } + } + if (possible_jids.size != 1) { + return false; + } + } else { + // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("[Legacy] Ignoring invalid jid from database: %s", e.message); + } + } + } + } + + if (possible_jids.size == 0) { + debug("Received message from unknown entity with device id %d", sid); + } + + uint8[] key; + + foreach (Jid possible_jid in possible_jids) { + try { + Address address = new Address(possible_jid.to_string(), sid); + if (key_node.get_attribute_bool("prekey")) { + Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid); + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); + string identity_key = Base64.encode(msg.identity_key.serialize()); + if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { + if (device[db.identity_meta.identity_key_public_base64] != identity_key) { + critical("[Legacy] Tried to use a different identity key for a known device id."); + continue; + } + } else { + debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid); + bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true); + if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, ProtocolVersion.LEGACY, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { + critical("[Legacy] Failed learning a device."); + continue; + } + XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (device == null && stream != null) { + module.request_user_devicelist.begin(stream, possible_jid); + } + } + debug("[Legacy] Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_pre_key_signal_message(msg); + // TODO: Finish session + } else { + debug("[Legacy] Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid); + SignalMessage msg = 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 + + 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; + } + MessageFlag.get_flag(stanza).decrypted = true; + } catch (Error e) { + debug("[Legacy] Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message); + continue; + } + + // If we figured out which real jid a message comes from due to decryption working, save it + if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { + message.real_jid = possible_jid; + } + return true; + } + return false; + } + private string arr_to_str(uint8[] arr) { // null-terminate the array uint8[] rarr = new uint8[arr.length+1]; diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala index 4103c4f6..197276dc 100644 --- a/plugins/omemo/src/protocol/stream_module.vala +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -1,4 +1,6 @@ -namespace Dino.Plugins.Omemo { -public interface BaseStreamModule { -} +using Xmpp; + +public interface Dino.Plugins.Omemo.BaseStreamModule : Object { + public abstract bool start_session(XmppStream stream, Jid jid, int32 device_id, Bundle bundle); + public abstract bool is_ignored_device(Jid jid, int32 device_id); } \ 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 index 3e19edbe..8177a75e 100644 --- a/plugins/omemo/src/protocol/v1_stream_module.vala +++ b/plugins/omemo/src/protocol/v1_stream_module.vala @@ -100,7 +100,12 @@ public class StreamModule : XmppStreamModule, BaseStreamModule { if (!is_ignored_device(jid, device_id)) { address.device_id = device_id; try { - if (!store.contains_session(address)) { + if (store.contains_session(address)) { + var session = store.load_session(address, 4); + if (session.state.session_version != 4) { + fetch_bundle(stream, jid, device_id); + } + } else { fetch_bundle(stream, jid, device_id); } } catch (Error e) { @@ -186,10 +191,15 @@ public class StreamModule : XmppStreamModule, BaseStreamModule { Address address = new Address(jid.bare_jid.to_string(), device_id); try { if (store.contains_session(address)) { - return false; + var session = store.load_session(address, 4); + if (session.state.session_version == 4) { + return false; + } + store.delete_session(address); } 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.version = 4; 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); diff --git a/plugins/omemo/src/protocol/version.vala b/plugins/omemo/src/protocol/version.vala index 7e66c7df..4a7f3673 100644 --- a/plugins/omemo/src/protocol/version.vala +++ b/plugins/omemo/src/protocol/version.vala @@ -1,7 +1,21 @@ -namespace Dino.Plugins.Omemo { -public enum ProtocolVersion { +public enum Dino.Plugins.Omemo.ProtocolVersion { UNKNOWN, LEGACY, - V1 -} + V1; + + public static ProtocolVersion from_int(int i) { + switch (i) { + case 0: return LEGACY; + case 1: return V1; + } + return UNKNOWN; + } + + public int to_int() { + switch (this) { + case LEGACY: return 0; + case V1: return 1; + } + return -1; + } } \ No newline at end of file