diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index 9eba26ba..9f39ce59 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -144,6 +144,7 @@ public class ContentItemStore : StreamInteractionModule, Object { QueryBuilder select = db.content_item.select(); select.with(db.content_item.foreign_id, "=", message.id); select.with(db.content_item.content_type, "=", 1); + select.with(db.content_item.hide, "=", false); foreach (Row row in select) { MessageItem item = new MessageItem(message, conversation, row[db.content_item.id]); if (!discard(item)) { @@ -167,6 +168,10 @@ public class ContentItemStore : StreamInteractionModule, Object { } } + public bool get_item_hide(ContentItem content_item) { + return db.content_item.row_with(db.content_item.id, content_item.id)[db.content_item.hide, false]; + } + public void set_item_hide(ContentItem content_item, bool hide) { db.content_item.update() .with(db.content_item.id, "=", content_item.id) diff --git a/plugins/omemo/src/contact_details_dialog.vala b/plugins/omemo/src/contact_details_dialog.vala index 037cd6e9..c61d75c2 100644 --- a/plugins/omemo/src/contact_details_dialog.vala +++ b/plugins/omemo/src/contact_details_dialog.vala @@ -123,7 +123,7 @@ public class ContactDetailsDialog : Gtk.Dialog { } if (!now_active) { - img.icon_name= "appointment-missed-symbolic"; + img.icon_name = "appointment-missed-symbolic"; status_lbl.set_markup("%s".printf(_("Unused"))); lbr.activatable = false; } diff --git a/plugins/omemo/src/database.vala b/plugins/omemo/src/database.vala index 247c00f6..1bcd7cae 100644 --- a/plugins/omemo/src/database.vala +++ b/plugins/omemo/src/database.vala @@ -6,7 +6,7 @@ using Dino.Entities; namespace Dino.Plugins.Omemo { public class Database : Qlite.Database { - private const int VERSION = 2; + private const int VERSION = 4; public class IdentityMetaTable : Table { public enum TrustLevel { @@ -57,11 +57,17 @@ public class Database : Qlite.Database { 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! + string identity_key = Base64.encode(bundle.identity_key.serialize()); + 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) { + error("Tried to change the identity key for a known device id. Likely an attack."); + } return upsert() .value(this.identity_id, identity_id, true) .value(this.address_name, address_name, true) .value(this.device_id, device_id, true) - .value(this.identity_key_public_base64, Base64.encode(bundle.identity_key.serialize())) + .value(this.identity_key_public_base64, identity_key) .value(this.trust_level, trust).perform(); } @@ -173,12 +179,38 @@ public class Database : Qlite.Database { } } + public class ContentItemMetaTable : Table { + public Column content_item_id = new Column.Integer("message_id") { primary_key = true }; + public Column identity_id = new Column.Integer("identity_id") { not_null = true }; + public Column address_name = new Column.Text("address_name") { not_null = true }; + public Column device_id = new Column.Integer("device_id") { not_null = true }; + public Column trusted_when_received = new Column.BoolInt("trusted_when_received") { not_null = true, default = "1" }; + + internal ContentItemMetaTable(Database db) { + base(db, "content_item_meta"); + init({content_item_id, identity_id, address_name, device_id, trusted_when_received}); + index("content_item_meta_device_idx", {identity_id, device_id, address_name}); + } + + public RowOption with_content_item(ContentItem item) { + return row_with(content_item_id, item.id); + } + + public QueryBuilder with_device(int identity_id, string address_name, int device_id) { + return select() + .with(this.identity_id, "=", identity_id) + .with(this.address_name, "=", address_name) + .with(this.device_id, "=", device_id); + } + } + public IdentityMetaTable identity_meta { get; private set; } public TrustTable trust { get; private set; } public IdentityTable identity { get; private set; } public SignedPreKeyTable signed_pre_key { get; private set; } public PreKeyTable pre_key { get; private set; } public SessionTable session { get; private set; } + public ContentItemMetaTable content_item_meta { get; private set; } public Database(string fileName) { base(fileName, VERSION); @@ -188,7 +220,8 @@ public class Database : Qlite.Database { signed_pre_key = new SignedPreKeyTable(this); pre_key = new PreKeyTable(this); session = new SessionTable(this); - init({identity_meta, trust, identity, signed_pre_key, pre_key, session}); + content_item_meta = new ContentItemMetaTable(this); + init({identity_meta, trust, identity, signed_pre_key, pre_key, session, content_item_meta}); try { exec("PRAGMA synchronous=0"); } catch (Error e) { } diff --git a/plugins/omemo/src/manager.vala b/plugins/omemo/src/manager.vala index 95b15d60..ee82b9d5 100644 --- a/plugins/omemo/src/manager.vala +++ b/plugins/omemo/src/manager.vala @@ -14,7 +14,6 @@ public class Manager : StreamInteractionModule, Object { private Database db; private TrustManager trust_manager; private Map message_states = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); - private ReceivedMessageListener received_message_listener = new ReceivedMessageListener(); private class MessageState { public Entities.Message msg { get; private set; } @@ -68,26 +67,10 @@ public class Manager : StreamInteractionModule, Object { stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.account_added.connect(on_account_added); - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener); stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); stream_interactor.get_module(RosterManager.IDENTITY).mutual_subscription.connect(on_mutual_subscription); } - private class ReceivedMessageListener : MessageListener { - - public string[] after_actions_const = new string[]{ }; - public override string action_group { get { return "DECRYPT"; } } - public override string[] after_actions { get { return after_actions_const; } } - - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - MessageFlag? flag = MessageFlag.get_flag(stanza); - if (flag != null && ((!)flag).decrypted) { - message.encryption = Encryption.OMEMO; - } - return false; - } - } - private Gee.List get_occupants(Jid jid, Account account){ Gee.List occupants = new ArrayList(Jid.equals_bare_func); if(!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid, account)){ diff --git a/plugins/omemo/src/trust_manager.vala b/plugins/omemo/src/trust_manager.vala index 8f6e9017..352a043f 100644 --- a/plugins/omemo/src/trust_manager.vala +++ b/plugins/omemo/src/trust_manager.vala @@ -10,14 +10,19 @@ public class TrustManager { private StreamInteractor stream_interactor; private Database db; - private ReceivedMessageListener received_message_listener; + private DecryptMessageListener decrypt_message_listener; + private TagMessageListener tag_message_listener; + + private HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); public TrustManager(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; - received_message_listener = new ReceivedMessageListener(stream_interactor, db); - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(received_message_listener); + decrypt_message_listener = new DecryptMessageListener(stream_interactor, db, message_device_id_map); + tag_message_listener = new TagMessageListener(stream_interactor, db, message_device_id_map); + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener); + stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener); } public void set_blind_trust(Account account, Jid jid, bool blind_trust) { @@ -36,6 +41,23 @@ public class TrustManager { .with(db.identity_meta.address_name, "=", jid.bare_jid.to_string()) .with(db.identity_meta.device_id, "=", device_id) .set(db.identity_meta.trust_level, trust_level).perform(); + string selection = null; + string[] selection_args = {}; + var app_db = Application.get_default().db; + foreach (Row row in db.content_item_meta.with_device(identity_id, jid.bare_jid.to_string(), device_id).with(db.content_item_meta.trusted_when_received, "=", false)) { + if (selection == null) { + selection = @"$(app_db.content_item.id) = ?"; + } else { + selection += @" OR $(app_db.content_item.id) = ?"; + } + selection_args += row[db.content_item_meta.content_item_id].to_string(); + } + if (selection != null) { + app_db.content_item.update() + .set(app_db.content_item.hide, trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED || trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) + .where(selection, selection_args) + .perform(); + } } private StanzaNode create_encrypted_key(uint8[] key, Address address, Store store) throws GLib.Error { @@ -154,17 +176,69 @@ public class TrustManager { return devices; } - private class ReceivedMessageListener : MessageListener { + private class TagMessageListener : MessageListener { + public string[] after_actions_const = new string[]{ "STORE" }; + public override string action_group { get { return "DECRYPT_TAG"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private StreamInteractor stream_interactor; + private Database db; + private HashMap message_device_id_map; + + public TagMessageListener(StreamInteractor stream_interactor, Database db, HashMap message_device_id_map) { + this.stream_interactor = stream_interactor; + this.db = db; + this.message_device_id_map = message_device_id_map; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + int device_id = 0; + if (message_device_id_map.has_key(message)) { + device_id = message_device_id_map[message]; + message_device_id_map.unset(message); + } + + // TODO: Handling of files + + ContentItem? content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item(conversation, 1, message.id); + + if (content_item != null && device_id != 0) { + Jid jid = content_item.jid; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(jid, conversation.account); + } + + int identity_id = db.identity.get_id(conversation.account.id); + Database.IdentityMetaTable.TrustLevel trust_level = (Database.IdentityMetaTable.TrustLevel) db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), device_id)[db.identity_meta.trust_level]; + if (trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED || trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) { + stream_interactor.get_module(ContentItemStore.IDENTITY).set_item_hide(content_item, true); + } + + db.content_item_meta.insert() + .value(db.content_item_meta.content_item_id, content_item.id) + .value(db.content_item_meta.identity_id, identity_id) + .value(db.content_item_meta.address_name, jid.bare_jid.to_string()) + .value(db.content_item_meta.device_id, device_id) + .value(db.content_item_meta.trusted_when_received, trust_level != Database.IdentityMetaTable.TrustLevel.UNTRUSTED) + .perform(); + } + return false; + } + } + + private class DecryptMessageListener : MessageListener { public string[] after_actions_const = new string[]{ }; public override string action_group { get { return "DECRYPT"; } } public override string[] after_actions { get { return after_actions_const; } } private StreamInteractor stream_interactor; private Database db; + private HashMap message_device_id_map; - public ReceivedMessageListener(StreamInteractor stream_interactor, Database db) { + public DecryptMessageListener(StreamInteractor stream_interactor, Database db, HashMap message_device_id_map) { this.stream_interactor = stream_interactor; this.db = db; + this.message_device_id_map = message_device_id_map; } public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { @@ -205,7 +279,7 @@ public class TrustManager { 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 + //address.device_id = 0; // TODO: Hack to have address obj live longer if (key.length >= 32) { int authtaglength = key.length - 16; @@ -219,20 +293,9 @@ public class TrustManager { } 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; - - int identity_id = db.identity.get_id(conversation.account.id); - if (identity_id < 0) return false; - - Database.IdentityMetaTable.TrustLevel trust_level = (Database.IdentityMetaTable.TrustLevel) db.identity_meta.get_device(identity_id, jid.bare_jid.to_string(), header.get_attribute_int("sid"))[db.identity_meta.trust_level]; - if (trust_level == Database.IdentityMetaTable.TrustLevel.UNTRUSTED) { - message.body = _("OMEMO message from a rejected device"); - message.marked = Message.Marked.WONTSEND; - } - if (trust_level == Database.IdentityMetaTable.TrustLevel.UNKNOWN) { - message.body = _("OMEMO message from an unknown device: ")+message.body; - message.marked = Message.Marked.WONTSEND; - } } catch (Error e) { if (Plugin.DEBUG) print(@"OMEMO: Signal error while decrypting message: $(e.message)\n"); }