From 6257e9705cd51543437507dbe805b8913845dc40 Mon Sep 17 00:00:00 2001
From: Marvin W <git@larma.de>
Date: Fri, 20 Dec 2019 02:06:36 +0100
Subject: OMEMO: Improve handling of newly added devices

---
 plugins/omemo/src/logic/database.vala            | 34 ++++++++---
 plugins/omemo/src/logic/manager.vala             | 78 ++++++++++++++----------
 plugins/omemo/src/logic/trust_manager.vala       | 56 ++++++++++++++---
 plugins/omemo/src/protocol/stream_module.vala    | 60 +++++++++++-------
 plugins/omemo/src/ui/contact_details_dialog.vala | 54 +++++++++++++++-
 5 files changed, 210 insertions(+), 72 deletions(-)

(limited to 'plugins/omemo/src')

diff --git a/plugins/omemo/src/logic/database.vala b/plugins/omemo/src/logic/database.vala
index 1f9e1304..6243569f 100644
--- a/plugins/omemo/src/logic/database.vala
+++ b/plugins/omemo/src/logic/database.vala
@@ -30,6 +30,10 @@ public class Database : Qlite.Database {
             return select().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name);
         }
 
+        public QueryBuilder get_with_device_id(int identity_id, int device_id) {
+            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<int32> devices) {
             update().with(this.identity_id, "=", identity_id).with(this.address_name, "=", address_name).set(now_active, false).perform();
             foreach (int32 device_id in devices) {
@@ -49,7 +53,22 @@ public class Database : Qlite.Database {
             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.");
+                critical("Tried to change the identity key for a known device id. Likely an attack.");
+                return -1;
+            }
+            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, identity_key)
+                    .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) {
+            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.");
+                return -1;
             }
             return upsert()
                     .value(this.identity_id, identity_id, true)
@@ -86,10 +105,6 @@ public class Database : Qlite.Database {
             return this.with_address(identity_id, address_name)
                 .with(this.device_id, "=", device_id).single().row().inner;
         }
-
-        public QueryBuilder get_with_device_id(int device_id) {
-            return select().with(this.device_id, "=", device_id);
-        }
     }
 
 
@@ -104,10 +119,11 @@ public class Database : Qlite.Database {
             index("trust_idx", {identity_id, address_name}, true);
         }
 
-        public bool get_blind_trust(int32 identity_id, string address_name) {
-            return this.select().with(this.identity_id, "=", identity_id)
-                    .with(this.address_name, "=", address_name)
-                    .with(this.blind_trust, "=", true).count() > 0;
+        public bool get_blind_trust(int32 identity_id, string address_name, bool def = false) {
+            RowOption row = this.select().with(this.identity_id, "=", identity_id)
+                    .with(this.address_name, "=", address_name).single().row();
+            if (row.is_present()) return row[blind_trust];
+            return def;
         }
     }
 
diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala
index 53e02e37..2bbd918c 100644
--- a/plugins/omemo/src/logic/manager.vala
+++ b/plugins/omemo/src/logic/manager.vala
@@ -66,7 +66,6 @@ public class Manager : StreamInteractionModule, Object {
         this.trust_manager = trust_manager;
 
         stream_interactor.stream_negotiated.connect(on_stream_negotiated);
-        stream_interactor.account_added.connect(on_account_added);
         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);
     }
@@ -171,14 +170,15 @@ public class Manager : StreamInteractionModule, Object {
         stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist.begin((!)stream, jid);
     }
 
-    private void on_account_added(Account account) {
-        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store_created.connect((store) => on_store_created.begin(account, store));
-        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices));
-        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle));
-    }
-
     private void on_stream_negotiated(Account account, XmppStream stream) {
-        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).request_user_devicelist.begin(stream, account.bare_jid);
+        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));
+        }
+        initialize_store.begin(account);
     }
 
     private void on_device_list_loaded(Account account, Jid jid, ArrayList<int32> device_list) {
@@ -202,7 +202,7 @@ public class Manager : StreamInteractionModule, Object {
         //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())) {
-            module.fetch_bundle(stream, Jid.parse(row[db.identity_meta.address_name]), row[db.identity_meta.device_id]);
+            module.fetch_bundle(stream, Jid.parse(row[db.identity_meta.address_name]), row[db.identity_meta.device_id], false);
             inc++;
         }
         if (inc > 0) {
@@ -245,7 +245,7 @@ public class Manager : StreamInteractionModule, Object {
         int identity_id = db.identity.get_id(account.id);
         if (identity_id < 0) return;
 
-        bool blind_trust = db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string());
+        bool blind_trust = db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true);
 
         //If we don't blindly trust new devices and we haven't seen this key before then don't trust it
         bool untrust = !(blind_trust || db.identity_meta.with_address(identity_id, jid.bare_jid.to_string())
@@ -269,30 +269,44 @@ public class Manager : StreamInteractionModule, Object {
         //Update the database with the appropriate trust information
         db.identity_meta.insert_device_bundle(identity_id, jid.bare_jid.to_string(), device_id, bundle, trusted);
 
-        XmppStream? stream = stream_interactor.get_stream(account);
-        if(stream == null) return;
-        StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
-        if(module == null) return;
+        if (should_start_session(account, jid)) {
+            XmppStream? stream = stream_interactor.get_stream(account);
+            if (stream != null) {
+                StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
+                if (module != null) {
+                    module.start_session(stream, jid, device_id, bundle);
+                }
+            }
+        }
+        continue_message_sending(account, jid);
+    }
 
-        //Get all messages waiting on the bundle and determine if they can now be sent
-        HashSet<Entities.Message> send_now = new HashSet<Entities.Message>();
+    private bool should_start_session(Account account, Jid jid) {
         lock (message_states) {
             foreach (Entities.Message msg in message_states.keys) {
+                if (!msg.account.equals(account)) continue;
+                Gee.List<Jid> occupants = get_occupants(msg.counterpart.bare_jid, account);
+                if (account.bare_jid.equals(jid) || (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
 
-                bool session_created = true;
+    private void continue_message_sending(Account account, Jid jid) {
+        //Get all messages waiting and determine if they can now be sent
+        HashSet<Entities.Message> send_now = new HashSet<Entities.Message>();
+        lock (message_states) {
+            foreach (Entities.Message msg in message_states.keys) {
                 if (!msg.account.equals(account)) continue;
                 Gee.List<Jid> occupants = get_occupants(msg.counterpart.bare_jid, account);
 
                 MessageState state = message_states[msg];
 
-                if (trusted == TrustLevel.TRUSTED || trusted == TrustLevel.VERIFIED) {
-                    if(account.bare_jid.equals(jid) || (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)))) {
-                        session_created = module.start_session(stream, jid, device_id, bundle);
-                    }
-                }
-                if (account.bare_jid.equals(jid) && session_created) {
+                if (account.bare_jid.equals(jid)) {
                     state.waiting_own_sessions--;
-                } else if (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid)) && session_created) {
+                } else if (msg.counterpart != null && (msg.counterpart.equals_bare(jid) || occupants.contains(jid))) {
                     state.waiting_other_sessions--;
                 }
                 if (state.should_retry_now()){
@@ -309,12 +323,15 @@ public class Manager : StreamInteractionModule, Object {
         }
     }
 
-    private async void on_store_created(Account account, Store store) {
+    private async void initialize_store(Account account) {
         // If the account is not yet persisted, wait for that and then continue - without identity.account_id the entry isn't worth much.
         if (account.id == -1) {
-            account.notify["id"].connect(() => on_store_created.callback());
+            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;
         Qlite.Row? row = db.identity.row_with(db.identity.account_id, account.id).inner;
         int identity_id = -1;
         bool publish_identity = false;
@@ -354,12 +371,9 @@ public class Manager : StreamInteractionModule, Object {
         }
 
         // Generated new device ID, ensure this gets added to the devicelist
-        if (publish_identity) {
-            XmppStream? stream = stream_interactor.get_stream(account);
-            if (stream == null) return;
-            StreamModule? module = ((!)stream).get_module(StreamModule.IDENTITY);
-            if(module == null) return;
-            module.request_user_devicelist.begin(stream, account.bare_jid);
+        XmppStream? stream = stream_interactor.get_stream(account);
+        if (stream != null) {
+            module.request_user_devicelist.begin((!)stream, account.bare_jid);
         }
     }
 
diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala
index 658e55ef..1b8a9436 100644
--- a/plugins/omemo/src/logic/trust_manager.vala
+++ b/plugins/omemo/src/logic/trust_manager.vala
@@ -65,6 +65,7 @@ public class TrustManager {
     private StanzaNode create_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);
         StanzaNode key_node = new StanzaNode.build("key", NS_URI)
             .put_attribute("rid", address.device_id.to_string())
             .put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
@@ -181,7 +182,7 @@ public class TrustManager {
     public bool is_known_address(Account account, Jid jid) {
         int identity_id = db.identity.get_id(account.id);
         if (identity_id < 0) return false;
-        return db.identity_meta.with_address(identity_id, jid.to_string()).count() > 0;
+        return db.identity_meta.with_address(identity_id, jid.to_string()).with(db.identity_meta.last_active, ">", 0).count() > 0;
     }
 
     public Gee.List<int32> get_trusted_devices(Account account, Jid jid) {
@@ -261,7 +262,8 @@ public class TrustManager {
         }
 
         public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
-            Store store = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY).store;
+            StreamModule module = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY);
+            Store store = module.store;
 
             StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI);
             if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
@@ -270,6 +272,7 @@ public class TrustManager {
                 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();
             stanza.add_flag(flag);
             StanzaNode? _header = encrypted.get_subnode("header");
@@ -278,6 +281,7 @@ public class TrustManager {
             int sid = header.get_attribute_int("sid");
             if (sid <= 0) return false;
             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) {
 
                     string? payload = encrypted.get_deep_string_content("payload");
@@ -289,27 +293,63 @@ public class TrustManager {
                     uint8[] iv = Base64.decode((!)iv_node);
                     Gee.List<Jid> possible_jids = new ArrayList<Jid>();
                     if (conversation.type_ == Conversation.Type.CHAT) {
-                        possible_jids.add(stanza.from);
+                        possible_jids.add(stanza.from.bare_jid);
                     } else {
                         Jid? real_jid = message.real_jid;
                         if (real_jid != null) {
-                            possible_jids.add(real_jid);
+                            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)) {
+                                possible_jids.add(new Jid(row[db.identity_meta.address_name]));
+                            }
+                            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(sid)) {
+                            foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) {
                                 possible_jids.add(new Jid(row[db.identity_meta.address_name]));
                             }
                         }
                     }
 
+                    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.bare_jid.to_string(), header.get_attribute_int("sid"));
+                            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);
@@ -332,6 +372,7 @@ public class TrustManager {
                             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;
                         }
 
@@ -339,10 +380,11 @@ public class TrustManager {
                         if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
                             message.real_jid = possible_jid;
                         }
-                        break;
+                        return false;
                     }
                 }
             }
+            debug("Received OMEMO encryped message that could not be decrypted.");
             return false;
         }
 
diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala
index 21476bd8..83822ea2 100644
--- a/plugins/omemo/src/protocol/stream_module.vala
+++ b/plugins/omemo/src/protocol/stream_module.vala
@@ -14,23 +14,25 @@ private const int NUM_KEYS_TO_PUBLISH = 100;
 
 public class StreamModule : XmppStreamModule {
     public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "omemo_module");
+    private static TimeSpan IGNORE_TIME = TimeSpan.MINUTE;
 
     public Store store { public get; private set; }
     private ConcurrentSet<string> active_bundle_requests = new ConcurrentSet<string>();
     private HashMap<Jid, Future<ArrayList<int32>>> active_devicelist_requests = new HashMap<Jid, Future<ArrayList<int32>>>(Jid.hash_func, Jid.equals_func);
-    private Map<Jid, ArrayList<int32>> ignored_devices = new HashMap<Jid, ArrayList<int32>>(Jid.hash_bare_func, Jid.equals_bare_func);
+    private Map<string, DateTime> device_ignore_time = new HashMap<string, DateTime>();
 
-    public signal void store_created(Store store);
     public signal void device_list_loaded(Jid jid, ArrayList<int32> 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 override void attach(XmppStream stream) {
-        if (!Plugin.ensure_context()) return;
+    public StreamModule() {
+        if (Plugin.ensure_context()) {
+            this.store = Plugin.get_context().create_store();
+        }
+    }
 
-        this.store = Plugin.get_context().create_store();
-        store_created(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) {}
@@ -105,43 +107,56 @@ public class StreamModule : XmppStreamModule {
         address.device_id = 0; // TODO: Hack to have address obj live longer
     }
 
-    public void fetch_bundle(XmppStream stream, Jid jid, int device_id) {
+    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 from %s: %i", 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);
+                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 (ignored_devices) {
-            if (!ignored_devices.has_key(jid)) {
-                ignored_devices[jid] = new ArrayList<int32>();
-            }
-            ignored_devices[jid].add(device_id);
+        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 (ignored_devices) {
-            return ignored_devices.has_key(jid) && ignored_devices[jid].contains(device_id);
+        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) {
+    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
-            debug("Ignoring device %s (%i): No bundle", jid.bare_jid.to_string(), device_id);
-            stream.get_module(IDENTITY).ignore_device(jid, device_id);
+            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");
@@ -169,17 +184,18 @@ public class StreamModule : XmppStreamModule {
                     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 (%i): %s", jid.bare_jid.to_string(), device_id, e.message);
+                    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 (%i): Bad bundle: %s", jid.bare_jid.to_string(), device_id, bundle.node.to_string());
+            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;
diff --git a/plugins/omemo/src/ui/contact_details_dialog.vala b/plugins/omemo/src/ui/contact_details_dialog.vala
index a26c426f..90bd0d3e 100644
--- a/plugins/omemo/src/ui/contact_details_dialog.vala
+++ b/plugins/omemo/src/ui/contact_details_dialog.vala
@@ -16,6 +16,9 @@ public class ContactDetailsDialog : Gtk.Dialog {
     private Jid jid;
     private bool own = false;
     private int own_id = 0;
+    private int identity_id = 0;
+    private Signal.Store store;
+    private Set<uint32> displayed_ids = new HashSet<uint32>();
 
     [GtkChild] private Label automatically_accept_new_label;
     [GtkChild] private Label automatically_accept_new_descr;
@@ -63,10 +66,14 @@ public class ContactDetailsDialog : Gtk.Dialog {
         inactive_keys_listbox.row_activated.connect(on_key_entry_clicked);
         auto_accept_switch.state_set.connect(on_auto_accept_toggled);
 
-        int identity_id = plugin.db.identity.get_id(account.id);
+        identity_id = plugin.db.identity.get_id(account.id);
         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;
+        }
 
-        auto_accept_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string()));
+        auto_accept_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true));
 
         // Dialog opened from the account settings menu
         // Show the fingerprint for this device separately with buttons for a qrcode and to copy
@@ -118,6 +125,31 @@ public class ContactDetailsDialog : Gtk.Dialog {
             }
             add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]);
         }
+
+        // Check for unknown devices
+        fetch_unknown_bundles();
+    }
+
+    private void fetch_unknown_bundles() {
+        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);
+                }
+            }
+        });
+        foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) {
+            module.fetch_bundle(stream, Jid.parse(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false);
+        }
     }
 
     private void header_function(ListBoxRow row, ListBoxRow? before) {
@@ -129,6 +161,22 @@ public class ContactDetailsDialog : Gtk.Dialog {
     private void add_fingerprint(Row device, TrustLevel trust) {
         string key_base64 = device[plugin.db.identity_meta.identity_key_public_base64];
         bool key_active = device[plugin.db.identity_meta.now_active];
+        if (store != null) {
+            try {
+                Signal.Address address = new Signal.Address(jid.to_string(), device[plugin.db.identity_meta.device_id]);
+                Signal.SessionRecord? session = null;
+                if (store.contains_session(address)) {
+                    session = store.load_session(address);
+                    string session_key_base64 = Base64.encode(session.state.remote_identity_key.serialize());
+                    if (key_base64 != session_key_base64) {
+                        critical("Session and database identity key mismatch!");
+                        key_base64 = session_key_base64;
+                    }
+                }
+            } catch (Error e) {
+                print("Error while reading session store: %s", e.message);
+            }
+        }
         FingerprintRow fingerprint_row = new FingerprintRow(device, key_base64, trust, key_active) { visible = true, activatable = true, hexpand = true };
 
         if (device[plugin.db.identity_meta.now_active]) {
@@ -138,6 +186,7 @@ public class ContactDetailsDialog : Gtk.Dialog {
             inactive_keys_expander.visible=true;
             inactive_keys_listbox.add(fingerprint_row);
         }
+        displayed_ids.add(device[plugin.db.identity_meta.device_id]);
     }
 
     private void on_key_entry_clicked(ListBoxRow widget) {
@@ -228,6 +277,7 @@ public class ContactDetailsDialog : Gtk.Dialog {
 
         lbr.add(box);
         new_keys_listbox.add(lbr);
+        displayed_ids.add(device[plugin.db.identity_meta.device_id]);
     }
 }
 
-- 
cgit v1.2.3-70-g09d2