From 626c68091126f84819091840a011c50e26dcbd8d Mon Sep 17 00:00:00 2001
From: Konstantinos Sideris <sideris.konstantin@gmail.com>
Date: Sun, 10 Jun 2018 20:03:45 +0300
Subject: [PATCH] Add support for displaying decrypted messages

---
 CMakeLists.txt                  |   1 +
 deps/CMakeLists.txt             |   2 +-
 include/Cache.h                 | 131 ++++++++++++++++-
 include/ChatPage.h              |   8 +-
 include/Logging.hpp             |   3 +
 include/MainWindow.h            |   1 -
 include/MatrixClient.h          |   5 +-
 include/Olm.hpp                 |  65 +++++++++
 include/timeline/TimelineView.h |   3 +
 src/Cache.cc                    | 250 +++++++++++++++++++++++++++++++-
 src/ChatPage.cc                 | 199 +++++++++++++++++++------
 src/CommunitiesList.cc          |   3 +
 src/Logging.cpp                 |  15 +-
 src/MainWindow.cc               |  13 +-
 src/MatrixClient.cc             |   8 +-
 src/Olm.cpp                     | 139 ++++++++++++++++++
 src/RoomList.cc                 |   2 +-
 src/main.cc                     |   8 +-
 src/timeline/TimelineView.cc    | 112 ++++++++++----
 19 files changed, 869 insertions(+), 99 deletions(-)
 create mode 100644 include/Olm.hpp
 create mode 100644 src/Olm.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index eedf9a697..ef20be39b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -184,6 +184,7 @@ set(SRC_FILES
     src/MainWindow.cc
     src/MatrixClient.cc
     src/QuickSwitcher.cc
+    src/Olm.cpp
     src/RegisterPage.cc
     src/RoomInfoListItem.cc
     src/RoomList.cc
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
index d6bab7e5a..5f9b48caf 100644
--- a/deps/CMakeLists.txt
+++ b/deps/CMakeLists.txt
@@ -40,7 +40,7 @@ set(MATRIX_STRUCTS_URL https://github.com/mujx/matrix-structs)
 set(MATRIX_STRUCTS_TAG eeb7373729a1618e2b3838407863342b88b8a0de)
 
 set(MTXCLIENT_URL https://github.com/mujx/mtxclient)
-set(MTXCLIENT_TAG 57f56d1fe73989dbe041a7ac0a28bf2e3286bf98)
+set(MTXCLIENT_TAG 26aad7088b9532808ded9919d55f58711c0138e3)
 
 set(OLM_URL https://git.matrix.org/git/olm.git)
 set(OLM_TAG 4065c8e11a33ba41133a086ed3de4da94dcb6bae)
diff --git a/include/Cache.h b/include/Cache.h
index afc7a148a..994a6da7b 100644
--- a/include/Cache.h
+++ b/include/Cache.h
@@ -17,13 +17,16 @@
 
 #pragma once
 
-#include <QDebug>
 #include <QDir>
 #include <QImage>
+
 #include <json.hpp>
 #include <lmdb++.h>
 #include <mtx/events/join_rules.hpp>
 #include <mtx/responses.hpp>
+#include <mtxclient/crypto/client.hpp>
+#include <mutex>
+
 using mtx::events::state::JoinRule;
 
 struct RoomMember
@@ -140,6 +143,83 @@ struct RoomSearchResult
 Q_DECLARE_METATYPE(RoomSearchResult)
 Q_DECLARE_METATYPE(RoomInfo)
 
+// Extra information associated with an outbound megolm session.
+struct OutboundGroupSessionData
+{
+        std::string session_id;
+        std::string session_key;
+        uint64_t message_index = 0;
+};
+
+inline void
+to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg)
+{
+        obj["session_id"]    = msg.session_id;
+        obj["session_key"]   = msg.session_key;
+        obj["message_index"] = msg.message_index;
+}
+
+inline void
+from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg)
+{
+        msg.session_id    = obj.at("session_id");
+        msg.session_key   = obj.at("session_key");
+        msg.message_index = obj.at("message_index");
+}
+
+struct OutboundGroupSessionDataRef
+{
+        OlmOutboundGroupSession *session;
+        OutboundGroupSessionData data;
+};
+
+struct DevicePublicKeys
+{
+        std::string ed25519;
+        std::string curve25519;
+};
+
+inline void
+to_json(nlohmann::json &obj, const DevicePublicKeys &msg)
+{
+        obj["ed25519"]    = msg.ed25519;
+        obj["curve25519"] = msg.curve25519;
+}
+
+inline void
+from_json(const nlohmann::json &obj, DevicePublicKeys &msg)
+{
+        msg.ed25519    = obj.at("ed25519");
+        msg.curve25519 = obj.at("curve25519");
+}
+
+//! Represents a unique megolm session identifier.
+struct MegolmSessionIndex
+{
+        //! The room in which this session exists.
+        std::string room_id;
+        //! The session_id of the megolm session.
+        std::string session_id;
+        //! The curve25519 public key of the sender.
+        std::string sender_key;
+
+        //! Representation to be used in a hash map.
+        std::string to_hash() const { return room_id + session_id + sender_key; }
+};
+
+struct OlmSessionStorage
+{
+        std::map<std::string, mtx::crypto::OlmSessionPtr> outbound_sessions;
+        std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions;
+        std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions;
+        std::map<std::string, OutboundGroupSessionData> group_outbound_session_data;
+
+        // Guards for accessing critical data.
+        std::mutex outbound_mtx;
+        std::mutex group_outbound_mtx;
+        std::mutex group_inbound_mtx;
+};
+
 class Cache : public QObject
 {
         Q_OBJECT
@@ -260,6 +340,48 @@ public:
         //! Check if we have sent a desktop notification for the given event id.
         bool isNotificationSent(const std::string &event_id);
 
+        //! Mark a room that uses e2e encryption.
+        void setEncryptedRoom(const std::string &room_id);
+        //! Save the public keys for a device.
+        void saveDeviceKeys(const std::string &device_id);
+        void getDeviceKeys(const std::string &device_id);
+
+        //! Save the device list for a user.
+        void setDeviceList(const std::string &user_id, const std::vector<std::string> &devices);
+        std::vector<std::string> getDeviceList(const std::string &user_id);
+
+        //
+        // Outbound Megolm Sessions
+        //
+        void saveOutboundMegolmSession(const MegolmSessionIndex &index,
+                                       const OutboundGroupSessionData &data,
+                                       mtx::crypto::OutboundGroupSessionPtr session);
+        OutboundGroupSessionDataRef getOutboundMegolmSession(const MegolmSessionIndex &index);
+        bool outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept;
+
+        //
+        // Inbound Megolm Sessions
+        //
+        void saveInboundMegolmSession(const MegolmSessionIndex &index,
+                                      mtx::crypto::InboundGroupSessionPtr session);
+        OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index);
+        bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept;
+
+        //
+        // Outbound Olm Sessions
+        //
+        void saveOutboundOlmSession(const std::string &curve25519,
+                                    mtx::crypto::OlmSessionPtr session);
+        OlmSession *getOutboundOlmSession(const std::string &curve25519);
+        bool outboundOlmSessionsExists(const std::string &curve25519) noexcept;
+
+        void saveOlmAccount(const std::string &pickled);
+        std::string restoreOlmAccount();
+
+        void restoreSessions();
+
+        OlmSessionStorage session_storage;
+
 private:
         //! Save an invited room.
         void saveInvite(lmdb::txn &txn,
@@ -451,6 +573,13 @@ private:
         lmdb::dbi readReceiptsDb_;
         lmdb::dbi notificationsDb_;
 
+        lmdb::dbi devicesDb_;
+        lmdb::dbi deviceKeysDb_;
+
+        lmdb::dbi inboundMegolmSessionDb_;
+        lmdb::dbi outboundMegolmSessionDb_;
+        lmdb::dbi outboundOlmSessionDb_;
+
         QString localUserId_;
         QString cacheDirectory_;
 };
diff --git a/include/ChatPage.h b/include/ChatPage.h
index e99e94ba5..d85829938 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -29,8 +29,7 @@
 #include "Cache.h"
 #include "CommunitiesList.h"
 #include "Community.h"
-
-#include <mtx.hpp>
+#include "MatrixClient.h"
 
 class OverlayModal;
 class QuickSwitcher;
@@ -119,6 +118,7 @@ signals:
         void loggedOut();
 
         void trySyncCb();
+        void tryDelayedSyncCb();
         void tryInitialSyncCb();
         void leftRoom(const QString &room_id);
 
@@ -146,8 +146,12 @@ private slots:
 private:
         static ChatPage *instance_;
 
+        //! Handler callback for initial sync. It doesn't run on the main thread so all
+        //! communication with the GUI should be done through signals.
+        void initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err);
         void tryInitialSync();
         void trySync();
+        void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts);
 
         //! Check if the given room is currently open.
         bool isRoomActive(const QString &room_id)
diff --git a/include/Logging.hpp b/include/Logging.hpp
index c301d80d9..bdbd3e2c7 100644
--- a/include/Logging.hpp
+++ b/include/Logging.hpp
@@ -15,4 +15,7 @@ net();
 
 std::shared_ptr<spdlog::logger>
 db();
+
+std::shared_ptr<spdlog::logger>
+crypto();
 }
diff --git a/include/MainWindow.h b/include/MainWindow.h
index f0fa9a088..b068e8f62 100644
--- a/include/MainWindow.h
+++ b/include/MainWindow.h
@@ -59,7 +59,6 @@ class MainWindow : public QMainWindow
 
 public:
         explicit MainWindow(QWidget *parent = 0);
-        ~MainWindow();
 
         static MainWindow *instance() { return instance_; };
         void saveCurrentWindowSize();
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 832d6cad0..7ea5e0b72 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -11,12 +11,15 @@ Q_DECLARE_METATYPE(mtx::responses::Notifications)
 Q_DECLARE_METATYPE(mtx::responses::Rooms)
 Q_DECLARE_METATYPE(mtx::responses::Sync)
 Q_DECLARE_METATYPE(std::string)
-Q_DECLARE_METATYPE(std::vector<std::string>);
+Q_DECLARE_METATYPE(std::vector<std::string>)
 
 namespace http {
 namespace v2 {
 mtx::http::Client *
 client();
+
+bool
+is_logged_in();
 }
 
 //! Initialize the http module
diff --git a/include/Olm.hpp b/include/Olm.hpp
new file mode 100644
index 000000000..2f7b1d642
--- /dev/null
+++ b/include/Olm.hpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <memory>
+#include <mtxclient/crypto/client.hpp>
+
+constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
+
+namespace olm {
+
+struct OlmCipherContent
+{
+        std::string body;
+        uint8_t type;
+};
+
+inline void
+from_json(const nlohmann::json &obj, OlmCipherContent &msg)
+{
+        msg.body = obj.at("body");
+        msg.type = obj.at("type");
+}
+
+struct OlmMessage
+{
+        std::string sender_key;
+        std::string sender;
+
+        using RecipientKey = std::string;
+        std::map<RecipientKey, OlmCipherContent> ciphertext;
+};
+
+inline void
+from_json(const nlohmann::json &obj, OlmMessage &msg)
+{
+        if (obj.at("type") != "m.room.encrypted")
+                throw std::invalid_argument("invalid type for olm message");
+
+        if (obj.at("content").at("algorithm") != OLM_ALGO)
+                throw std::invalid_argument("invalid algorithm for olm message");
+
+        msg.sender     = obj.at("sender");
+        msg.sender_key = obj.at("content").at("sender_key");
+        msg.ciphertext =
+          obj.at("content").at("ciphertext").get<std::map<std::string, OlmCipherContent>>();
+}
+
+mtx::crypto::OlmClient *
+client();
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs);
+
+void
+handle_olm_message(const OlmMessage &msg);
+
+void
+handle_olm_normal_message(const std::string &sender,
+                          const std::string &sender_key,
+                          const OlmCipherContent &content);
+
+void
+handle_pre_key_olm_message(const std::string &sender,
+                           const std::string &sender_key,
+                           const OlmCipherContent &content);
+} // namespace olm
diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h
index 30af97fb3..88857222d 100644
--- a/include/timeline/TimelineView.h
+++ b/include/timeline/TimelineView.h
@@ -149,6 +149,9 @@ private:
 
         QWidget *relativeWidget(TimelineItem *item, int dt) const;
 
+        TimelineEvent parseEncryptedEvent(
+          const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
+
         //! Callback for all message sending.
         void sendRoomMessageHandler(const std::string &txn_id,
                                     const mtx::responses::EventId &res,
diff --git a/src/Cache.cc b/src/Cache.cc
index 2a5554250..150990b73 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -31,25 +31,42 @@
 
 //! Should be changed when a breaking change occurs in the cache format.
 //! This will reset client's data.
-static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.05.11");
+static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.06.10");
+static const std::string SECRET("secret");
 
 static const lmdb::val NEXT_BATCH_KEY("next_batch");
+static const lmdb::val OLM_ACCOUNT_KEY("olm_account");
 static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version");
 
 //! Cache databases and their format.
 //!
 //! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
 //! Format: room_id -> RoomInfo
-static constexpr const char *ROOMS_DB   = "rooms";
-static constexpr const char *INVITES_DB = "invites";
+constexpr auto ROOMS_DB("rooms");
+constexpr auto INVITES_DB("invites");
 //! Keeps already downloaded media for reuse.
 //! Format: matrix_url -> binary data.
-static constexpr const char *MEDIA_DB = "media";
+constexpr auto MEDIA_DB("media");
 //! Information that  must be kept between sync requests.
-static constexpr const char *SYNC_STATE_DB = "sync_state";
+constexpr auto SYNC_STATE_DB("sync_state");
 //! Read receipts per room/event.
-static constexpr const char *READ_RECEIPTS_DB = "read_receipts";
-static constexpr const char *NOTIFICATIONS_DB = "sent_notifications";
+constexpr auto READ_RECEIPTS_DB("read_receipts");
+constexpr auto NOTIFICATIONS_DB("sent_notifications");
+
+//! Encryption related databases.
+
+//! user_id -> list of devices
+constexpr auto DEVICES_DB("devices");
+//! device_id -> device keys
+constexpr auto DEVICE_KEYS_DB("device_keys");
+//! room_ids that have encryption enabled.
+// constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms");
+
+//! MegolmSessionIndex -> pickled OlmInboundGroupSession
+constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions");
+//! MegolmSessionIndex -> pickled OlmOutboundGroupSession
+constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
+constexpr auto OUTBOUND_OLM_SESSIONS_DB("outbound_olm_sessions");
 
 using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
 using Receipts       = std::map<std::string, std::map<std::string, uint64_t>>;
@@ -79,7 +96,7 @@ client()
 {
         return instance_.get();
 }
-}
+} // namespace cache
 
 Cache::Cache(const QString &userId, QObject *parent)
   : QObject{parent}
@@ -90,6 +107,11 @@ Cache::Cache(const QString &userId, QObject *parent)
   , mediaDb_{0}
   , readReceiptsDb_{0}
   , notificationsDb_{0}
+  , devicesDb_{0}
+  , deviceKeysDb_{0}
+  , inboundMegolmSessionDb_{0}
+  , outboundMegolmSessionDb_{0}
+  , outboundOlmSessionDb_{0}
   , localUserId_{userId}
 {}
 
@@ -149,9 +171,221 @@ Cache::setup()
         mediaDb_         = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE);
         readReceiptsDb_  = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
         notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
+
+        // Device management
+        devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
+        deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
+
+        // Session management
+        inboundMegolmSessionDb_  = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+        outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+        outboundOlmSessionDb_    = lmdb::dbi::open(txn, OUTBOUND_OLM_SESSIONS_DB, MDB_CREATE);
+
+        txn.commit();
+}
+
+//
+// Device Management
+//
+
+//
+// Session Management
+//
+
+void
+Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
+                                mtx::crypto::InboundGroupSessionPtr session)
+{
+        using namespace mtx::crypto;
+        const auto key     = index.to_hash();
+        const auto pickled = pickle<InboundSessionObject>(session.get(), SECRET);
+
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled));
+        txn.commit();
+
+        {
+                std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+                session_storage.group_inbound_sessions[key] = std::move(session);
+        }
+}
+
+OlmInboundGroupSession *
+Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
+{
+        std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+        return session_storage.group_inbound_sessions[index.to_hash()].get();
+}
+
+bool
+Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept
+{
+        std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx);
+        return session_storage.group_inbound_sessions.find(index.to_hash()) !=
+               session_storage.group_inbound_sessions.end();
+}
+
+void
+Cache::saveOutboundMegolmSession(const MegolmSessionIndex &index,
+                                 const OutboundGroupSessionData &data,
+                                 mtx::crypto::OutboundGroupSessionPtr session)
+{
+        using namespace mtx::crypto;
+        const auto key     = index.to_hash();
+        const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
+
+        json j;
+        j["data"]    = data;
+        j["session"] = pickled;
+
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(key), lmdb::val(j.dump()));
+        txn.commit();
+
+        {
+                std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+                session_storage.group_outbound_session_data[key] = data;
+                session_storage.group_outbound_sessions[key]     = std::move(session);
+        }
+}
+
+bool
+Cache::outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept
+{
+        const auto key = index.to_hash();
+
+        std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+        return (session_storage.group_outbound_sessions.find(key) !=
+                session_storage.group_outbound_sessions.end()) &&
+               (session_storage.group_outbound_session_data.find(key) !=
+                session_storage.group_outbound_session_data.end());
+}
+
+OutboundGroupSessionDataRef
+Cache::getOutboundMegolmSession(const MegolmSessionIndex &index)
+{
+        const auto key = index.to_hash();
+        std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
+        return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[key].get(),
+                                           session_storage.group_outbound_session_data[key]};
+}
+
+void
+Cache::saveOutboundOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session)
+{
+        using namespace mtx::crypto;
+        const auto pickled = pickle<SessionObject>(session.get(), SECRET);
+
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(curve25519), lmdb::val(pickled));
         txn.commit();
+
+        {
+                std::unique_lock<std::mutex> lock(session_storage.outbound_mtx);
+                session_storage.outbound_sessions[curve25519] = std::move(session);
+        }
+}
+
+bool
+Cache::outboundOlmSessionsExists(const std::string &curve25519) noexcept
+{
+        std::unique_lock<std::mutex> lock(session_storage.outbound_mtx);
+        return session_storage.outbound_sessions.find(curve25519) !=
+               session_storage.outbound_sessions.end();
 }
 
+OlmSession *
+Cache::getOutboundOlmSession(const std::string &curve25519)
+{
+        std::unique_lock<std::mutex> lock(session_storage.outbound_mtx);
+        return session_storage.outbound_sessions.at(curve25519).get();
+}
+
+void
+Cache::saveOlmAccount(const std::string &data)
+{
+        auto txn = lmdb::txn::begin(env_);
+        lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data));
+        txn.commit();
+}
+
+void
+Cache::restoreSessions()
+{
+        using namespace mtx::crypto;
+
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        std::string key, value;
+
+        //
+        // Inbound Megolm Sessions
+        //
+        {
+                auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
+                while (cursor.get(key, value, MDB_NEXT)) {
+                        auto session = unpickle<InboundSessionObject>(value, SECRET);
+                        session_storage.group_inbound_sessions[key] = std::move(session);
+                }
+                cursor.close();
+        }
+
+        //
+        // Outbound Megolm Sessions
+        //
+        {
+                auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_);
+                while (cursor.get(key, value, MDB_NEXT)) {
+                        json obj;
+
+                        try {
+                                obj = json::parse(value);
+
+                                session_storage.group_outbound_session_data[key] =
+                                  obj.at("data").get<OutboundGroupSessionData>();
+
+                                auto session =
+                                  unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
+                                session_storage.group_outbound_sessions[key] = std::move(session);
+                        } catch (const nlohmann::json::exception &e) {
+                                log::db()->warn("failed to parse outbound megolm session data: {}",
+                                                e.what());
+                        }
+                }
+                cursor.close();
+        }
+
+        //
+        // Outbound Olm Sessions
+        //
+        {
+                auto cursor = lmdb::cursor::open(txn, outboundOlmSessionDb_);
+                while (cursor.get(key, value, MDB_NEXT)) {
+                        auto session = unpickle<SessionObject>(value, SECRET);
+                        session_storage.outbound_sessions[key] = std::move(session);
+                }
+                cursor.close();
+        }
+
+        txn.commit();
+
+        log::db()->info("sessions restored");
+}
+
+std::string
+Cache::restoreOlmAccount()
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        lmdb::val pickled;
+        lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled);
+        txn.commit();
+
+        return std::string(pickled.data(), pickled.size());
+}
+
+//
+// Media Management
+//
+
 void
 Cache::saveImage(const std::string &url, const std::string &img_data)
 {
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 64ce69d62..a5a6a8c0f 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -25,6 +25,7 @@
 #include "Logging.hpp"
 #include "MainWindow.h"
 #include "MatrixClient.h"
+#include "Olm.hpp"
 #include "OverlayModal.h"
 #include "QuickSwitcher.h"
 #include "RoomList.h"
@@ -43,8 +44,12 @@
 #include "dialogs/ReadReceipts.h"
 #include "timeline/TimelineViewManager.h"
 
+// TODO: Needs to be updated with an actual secret.
+static const std::string STORAGE_SECRET_KEY("secret");
+
 ChatPage *ChatPage::instance_             = nullptr;
 constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
+constexpr size_t MAX_ONETIME_KEYS         = 50;
 
 ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
   : QWidget(parent)
@@ -612,6 +617,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
 
         connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync);
         connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync);
+        connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() {
+                QTimer::singleShot(5000, this, &ChatPage::trySync);
+        });
 
         connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage);
 
@@ -728,6 +736,11 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
           });
         // TODO http::client()->getOwnCommunities();
 
+        // The Olm client needs the user_id & device_id that will be included
+        // in the generated payloads & keys.
+        olm::client()->set_user_id(http::v2::client()->user_id().to_string());
+        olm::client()->set_device_id(http::v2::client()->device_id());
+
         cache::init(userid);
 
         try {
@@ -741,6 +754,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
 
                 if (cache::client()->isInitialized()) {
                         loadStateFromCache();
+                        // TODO: Bootstrap olm client with saved data.
                         return;
                 }
         } catch (const lmdb::error &e) {
@@ -749,6 +763,22 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
                 log::net()->info("falling back to initial sync");
         }
 
+        try {
+                // It's the first time syncing with this device
+                // There isn't a saved olm account to restore.
+                log::crypto()->info("creating new olm account");
+                olm::client()->create_new_account();
+                cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY));
+        } catch (const lmdb::error &e) {
+                log::crypto()->critical("failed to save olm account {}", e.what());
+                emit dropToLoginPageCb(QString::fromStdString(e.what()));
+                return;
+        } catch (const mtx::crypto::olm_exception &e) {
+                log::crypto()->critical("failed to create new olm account {}", e.what());
+                emit dropToLoginPageCb(QString::fromStdString(e.what()));
+                return;
+        }
+
         tryInitialSync();
 }
 
@@ -826,16 +856,29 @@ ChatPage::loadStateFromCache()
 
         QtConcurrent::run([this]() {
                 try {
+                        cache::client()->restoreSessions();
+                        olm::client()->load(cache::client()->restoreOlmAccount(),
+                                            STORAGE_SECRET_KEY);
+
                         cache::client()->populateMembers();
 
                         emit initializeEmptyViews(cache::client()->joinedRooms());
                         emit initializeRoomList(cache::client()->roomInfo());
+                } catch (const mtx::crypto::olm_exception &e) {
+                        log::crypto()->critical("failed to restore olm account: {}", e.what());
+                        emit dropToLoginPageCb(
+                          tr("Failed to restore OLM account. Please login again."));
+                        return;
                 } catch (const lmdb::error &e) {
-                        std::cout << "load cache error:" << e.what() << '\n';
-                        // TODO Clear cache and restart.
+                        log::db()->critical("failed to restore cache: {}", e.what());
+                        emit dropToLoginPageCb(
+                          tr("Failed to restore save data. Please login again."));
                         return;
                 }
 
+                log::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+                log::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
+
                 // Start receiving events.
                 emit trySyncCb();
 
@@ -1008,49 +1051,40 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res)
 void
 ChatPage::tryInitialSync()
 {
-        mtx::http::SyncOpts opts;
-        opts.timeout = 0;
+        log::crypto()->info("ed25519   : {}", olm::client()->identity_keys().ed25519);
+        log::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519);
 
-        log::net()->info("trying initial sync");
+        // Upload one time keys for the device.
+        log::crypto()->info("generating one time keys");
+        olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS);
 
-        http::v2::client()->sync(
-          opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) {
+        http::v2::client()->upload_keys(
+          olm::client()->create_upload_keys_request(),
+          [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) {
                   if (err) {
-                          const auto error      = QString::fromStdString(err->matrix_error.error);
-                          const auto msg        = tr("Please try to login again: %1").arg(error);
-                          const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
                           const int status_code = static_cast<int>(err->status_code);
-
-                          log::net()->error("sync error: {} {}", status_code, err_code);
-
-                          switch (status_code) {
-                          case 502:
-                          case 504:
-                          case 524: {
-                                  emit tryInitialSyncCb();
-                                  return;
-                          }
-                          default: {
-                                  emit dropToLoginPageCb(msg);
-                                  return;
-                          }
-                          }
-                  }
-
-                  log::net()->info("initial sync completed");
-
-                  try {
-                          cache::client()->saveState(res);
-                          emit initializeViews(std::move(res.rooms));
-                          emit initializeRoomList(cache::client()->roomInfo());
-                  } catch (const lmdb::error &e) {
-                          log::db()->error("{}", e.what());
+                          log::crypto()->critical("failed to upload one time keys: {} {}",
+                                                  err->matrix_error.error,
+                                                  status_code);
+                          // TODO We should have a timeout instead of keeping hammering the server.
                           emit tryInitialSyncCb();
                           return;
                   }
 
-                  emit trySyncCb();
-                  emit contentLoaded();
+                  olm::client()->mark_keys_as_published();
+                  for (const auto &entry : res.one_time_key_counts)
+                          log::net()->info(
+                            "uploaded {} {} one-time keys", entry.second, entry.first);
+
+                  log::net()->info("trying initial sync");
+
+                  mtx::http::SyncOpts opts;
+                  opts.timeout = 0;
+                  http::v2::client()->sync(opts,
+                                           std::bind(&ChatPage::initialSyncHandler,
+                                                     this,
+                                                     std::placeholders::_1,
+                                                     std::placeholders::_2));
           });
 }
 
@@ -1079,24 +1113,31 @@ ChatPage::trySync()
 
                           log::net()->error("sync error: {} {}", status_code, err_code);
 
+                          if (status_code <= 0 || status_code >= 600) {
+                                  if (!http::v2::is_logged_in())
+                                          return;
+
+                                  emit dropToLoginPageCb(msg);
+                                  return;
+                          }
+
                           switch (status_code) {
                           case 502:
                           case 504:
                           case 524: {
-                                  emit trySync();
+                                  emit trySyncCb();
                                   return;
                           }
                           case 401:
                           case 403: {
-                                  // We are logged out.
-                                  if (http::v2::client()->access_token().empty())
+                                  if (!http::v2::is_logged_in())
                                           return;
 
                                   emit dropToLoginPageCb(msg);
                                   return;
                           }
                           default: {
-                                  emit trySync();
+                                  emit tryDelayedSyncCb();
                                   return;
                           }
                           }
@@ -1104,9 +1145,14 @@ ChatPage::trySync()
 
                   log::net()->debug("sync completed: {}", res.next_batch);
 
+                  // Ensure that we have enough one-time keys available.
+                  ensureOneTimeKeyCount(res.device_one_time_keys_count);
+
                   // TODO: fine grained error handling
                   try {
                           cache::client()->saveState(res);
+                          olm::handle_to_device_messages(res.to_device);
+
                           emit syncUI(res.rooms);
 
                           auto updates = cache::client()->roomUpdates(res);
@@ -1194,3 +1240,74 @@ ChatPage::sendTypingNotifications()
                   }
           });
 }
+
+void
+ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err)
+{
+        if (err) {
+                const auto error      = QString::fromStdString(err->matrix_error.error);
+                const auto msg        = tr("Please try to login again: %1").arg(error);
+                const auto err_code   = mtx::errors::to_string(err->matrix_error.errcode);
+                const int status_code = static_cast<int>(err->status_code);
+
+                log::net()->error("sync error: {} {}", status_code, err_code);
+
+                switch (status_code) {
+                case 502:
+                case 504:
+                case 524: {
+                        emit tryInitialSyncCb();
+                        return;
+                }
+                default: {
+                        emit dropToLoginPageCb(msg);
+                        return;
+                }
+                }
+        }
+
+        log::net()->info("initial sync completed");
+
+        try {
+                cache::client()->saveState(res);
+
+                olm::handle_to_device_messages(res.to_device);
+
+                emit initializeViews(std::move(res.rooms));
+                emit initializeRoomList(cache::client()->roomInfo());
+        } catch (const lmdb::error &e) {
+                log::db()->error("{}", e.what());
+                emit tryInitialSyncCb();
+                return;
+        }
+
+        emit trySyncCb();
+        emit contentLoaded();
+}
+
+void
+ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
+{
+        for (const auto &entry : counts) {
+                if (entry.second < MAX_ONETIME_KEYS) {
+                        const int nkeys = MAX_ONETIME_KEYS - entry.second;
+
+                        log::crypto()->info("uploading {} {} keys", nkeys, entry.first);
+                        olm::client()->generate_one_time_keys(nkeys);
+
+                        http::v2::client()->upload_keys(
+                          olm::client()->create_upload_keys_request(),
+                          [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
+                                  if (err) {
+                                          log::crypto()->warn(
+                                            "failed to update one-time keys: {} {}",
+                                            err->matrix_error.error,
+                                            static_cast<int>(err->status_code));
+                                          return;
+                                  }
+
+                                  olm::client()->mark_keys_as_published();
+                          });
+                }
+        }
+}
diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc
index 8ccd5e9d5..49affcb7f 100644
--- a/src/CommunitiesList.cc
+++ b/src/CommunitiesList.cc
@@ -128,6 +128,9 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr
                 return;
         }
 
+        if (avatarUrl.isEmpty())
+                return;
+
         mtx::http::ThumbOpts opts;
         opts.mxc_url = avatarUrl.toStdString();
         http::v2::client()->get_thumbnail(
diff --git a/src/Logging.cpp b/src/Logging.cpp
index c6c1c502d..77e61e09e 100644
--- a/src/Logging.cpp
+++ b/src/Logging.cpp
@@ -4,9 +4,10 @@
 #include <spdlog/sinks/file_sinks.h>
 
 namespace {
-std::shared_ptr<spdlog::logger> db_logger   = nullptr;
-std::shared_ptr<spdlog::logger> net_logger  = nullptr;
-std::shared_ptr<spdlog::logger> main_logger = nullptr;
+std::shared_ptr<spdlog::logger> db_logger     = nullptr;
+std::shared_ptr<spdlog::logger> net_logger    = nullptr;
+std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
+std::shared_ptr<spdlog::logger> main_logger   = nullptr;
 
 constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
 constexpr auto MAX_LOG_FILES = 3;
@@ -28,6 +29,8 @@ init(const std::string &file_path)
         net_logger  = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks));
         main_logger = std::make_shared<spdlog::logger>("main", std::begin(sinks), std::end(sinks));
         db_logger   = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
+        crypto_logger =
+          std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
 }
 
 std::shared_ptr<spdlog::logger>
@@ -47,4 +50,10 @@ db()
 {
         return db_logger;
 }
+
+std::shared_ptr<spdlog::logger>
+crypto()
+{
+        return crypto_logger;
+}
 }
diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index 9ba8b28e4..cca51f03c 100644
--- a/src/MainWindow.cc
+++ b/src/MainWindow.cc
@@ -46,15 +46,6 @@
 
 MainWindow *MainWindow::instance_ = nullptr;
 
-MainWindow::~MainWindow()
-{
-        if (http::v2::client() != nullptr) {
-                http::v2::client()->shutdown();
-                // TODO: find out why waiting for the threads to join is slow.
-                http::v2::client()->close();
-        }
-}
-
 MainWindow::MainWindow(QWidget *parent)
   : QMainWindow(parent)
   , progressModal_{nullptr}
@@ -154,9 +145,11 @@ MainWindow::MainWindow(QWidget *parent)
                 QString token       = settings.value("auth/access_token").toString();
                 QString home_server = settings.value("auth/home_server").toString();
                 QString user_id     = settings.value("auth/user_id").toString();
+                QString device_id   = settings.value("auth/device_id").toString();
 
                 http::v2::client()->set_access_token(token.toStdString());
                 http::v2::client()->set_server(home_server.toStdString());
+                http::v2::client()->set_device_id(device_id.toStdString());
 
                 try {
                         using namespace mtx::identifiers;
@@ -228,6 +221,7 @@ void
 MainWindow::showChatPage()
 {
         auto userid     = QString::fromStdString(http::v2::client()->user_id().to_string());
+        auto device_id  = QString::fromStdString(http::v2::client()->device_id());
         auto homeserver = QString::fromStdString(http::v2::client()->server() + ":" +
                                                  std::to_string(http::v2::client()->port()));
         auto token      = QString::fromStdString(http::v2::client()->access_token());
@@ -236,6 +230,7 @@ MainWindow::showChatPage()
         settings.setValue("auth/access_token", token);
         settings.setValue("auth/home_server", homeserver);
         settings.setValue("auth/user_id", userid);
+        settings.setValue("auth/device_id", device_id);
 
         showOverlayProgressBar();
 
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 0eb4658ab..d4ab8e33f 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -3,7 +3,7 @@
 #include <memory>
 
 namespace {
-auto v2_client_ = std::make_shared<mtx::http::Client>("matrix.org");
+auto v2_client_ = std::make_shared<mtx::http::Client>();
 }
 
 namespace http {
@@ -15,6 +15,12 @@ client()
         return v2_client_.get();
 }
 
+bool
+is_logged_in()
+{
+        return !v2_client_->access_token().empty();
+}
+
 } // namespace v2
 
 void
diff --git a/src/Olm.cpp b/src/Olm.cpp
new file mode 100644
index 000000000..769b02348
--- /dev/null
+++ b/src/Olm.cpp
@@ -0,0 +1,139 @@
+#include "Olm.hpp"
+
+#include "Cache.h"
+#include "Logging.hpp"
+
+using namespace mtx::crypto;
+
+namespace {
+auto client_ = std::make_unique<mtx::crypto::OlmClient>();
+}
+
+namespace olm {
+
+mtx::crypto::OlmClient *
+client()
+{
+        return client_.get();
+}
+
+void
+handle_to_device_messages(const std::vector<nlohmann::json> &msgs)
+{
+        if (msgs.empty())
+                return;
+
+        log::crypto()->info("received {} to_device messages", msgs.size());
+
+        for (const auto &msg : msgs) {
+                try {
+                        OlmMessage olm_msg = msg;
+                        handle_olm_message(std::move(olm_msg));
+                } catch (const nlohmann::json::exception &e) {
+                        log::crypto()->warn(
+                          "parsing error for olm message: {} {}", e.what(), msg.dump(2));
+                } catch (const std::invalid_argument &e) {
+                        log::crypto()->warn(
+                          "validation error for olm message: {} {}", e.what(), msg.dump(2));
+                }
+        }
+}
+
+void
+handle_olm_message(const OlmMessage &msg)
+{
+        log::crypto()->info("sender    : {}", msg.sender);
+        log::crypto()->info("sender_key: {}", msg.sender_key);
+
+        const auto my_key = olm::client()->identity_keys().curve25519;
+
+        for (const auto &cipher : msg.ciphertext) {
+                // We skip messages not meant for the current device.
+                if (cipher.first != my_key)
+                        continue;
+
+                const auto type = cipher.second.type;
+                log::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
+
+                if (type == OLM_MESSAGE_TYPE_PRE_KEY)
+                        handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second);
+                else
+                        handle_olm_normal_message(msg.sender, msg.sender_key, cipher.second);
+        }
+}
+
+void
+handle_pre_key_olm_message(const std::string &sender,
+                           const std::string &sender_key,
+                           const OlmCipherContent &content)
+{
+        log::crypto()->info("opening olm session with {}", sender);
+
+        OlmSessionPtr inbound_session = nullptr;
+        try {
+                inbound_session = olm::client()->create_inbound_session(content.body);
+        } catch (const olm_exception &e) {
+                log::crypto()->critical(
+                  "failed to create inbound session with {}: {}", sender, e.what());
+                return;
+        }
+
+        if (!matches_inbound_session_from(inbound_session.get(), sender_key, content.body)) {
+                log::crypto()->warn("inbound olm session doesn't match sender's key ({})", sender);
+                return;
+        }
+
+        mtx::crypto::BinaryBuf output;
+        try {
+                output = olm::client()->decrypt_message(
+                  inbound_session.get(), OLM_MESSAGE_TYPE_PRE_KEY, content.body);
+        } catch (const olm_exception &e) {
+                log::crypto()->critical(
+                  "failed to decrypt olm message {}: {}", content.body, e.what());
+                return;
+        }
+
+        auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
+        log::crypto()->info("decrypted message: \n {}", plaintext.dump(2));
+
+        std::string room_id, session_id, session_key;
+        try {
+                room_id     = plaintext.at("content").at("room_id");
+                session_id  = plaintext.at("content").at("session_id");
+                session_key = plaintext.at("content").at("session_key");
+        } catch (const nlohmann::json::exception &e) {
+                log::crypto()->critical(
+                  "failed to parse plaintext olm message: {} {}", e.what(), plaintext.dump(2));
+                return;
+        }
+
+        MegolmSessionIndex index;
+        index.room_id    = room_id;
+        index.session_id = session_id;
+        index.sender_key = sender_key;
+
+        if (!cache::client()->inboundMegolmSessionExists(index)) {
+                auto megolm_session = olm::client()->init_inbound_group_session(session_key);
+
+                try {
+                        cache::client()->saveInboundMegolmSession(index, std::move(megolm_session));
+                } catch (const lmdb::error &e) {
+                        log::crypto()->critical("failed to save inbound megolm session: {}",
+                                                e.what());
+                        return;
+                }
+
+                log::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
+        } else {
+                log::crypto()->warn(
+                  "inbound megolm session already exists ({}, {})", room_id, sender);
+        }
+}
+
+void
+handle_olm_normal_message(const std::string &, const std::string &, const OlmCipherContent &)
+{
+        log::crypto()->warn("olm(1) not implemeted yet");
+}
+
+} // namespace olm
diff --git a/src/RoomList.cc b/src/RoomList.cc
index d3ed2e665..4891f746b 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -96,7 +96,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url)
                   opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) {
                           if (err) {
                                   log::net()->warn(
-                                    "failed to download thumbnail: {}, {} - {}",
+                                    "failed to download room avatar: {} {} {}",
                                     opts.mxc_url,
                                     mtx::errors::to_string(err->matrix_error.errcode),
                                     err->matrix_error.error);
diff --git a/src/main.cc b/src/main.cc
index 1df8d0c9f..13a712f4e 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -149,7 +149,13 @@ main(int argc, char *argv[])
             !settings.value("user/window/tray", true).toBool())
                 w.show();
 
-        QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::saveCurrentWindowSize);
+        QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
+                w.saveCurrentWindowSize();
+                if (http::v2::client() != nullptr) {
+                        http::v2::client()->shutdown();
+                        http::v2::client()->close();
+                }
+        });
 
         log::main()->info("starting nheko {}", nheko::version);
 
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index 5ef390a98..9baa1f4a0 100644
--- a/src/timeline/TimelineView.cc
+++ b/src/timeline/TimelineView.cc
@@ -24,6 +24,7 @@
 #include "Config.h"
 #include "FloatingButton.h"
 #include "Logging.hpp"
+#include "Olm.hpp"
 #include "UserSettingsPage.h"
 #include "Utils.h"
 
@@ -235,19 +236,19 @@ TimelineItem *
 TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
                                 TimelineDirection direction)
 {
-        namespace msg     = mtx::events::msg;
-        using AudioEvent  = mtx::events::RoomEvent<msg::Audio>;
-        using EmoteEvent  = mtx::events::RoomEvent<msg::Emote>;
-        using FileEvent   = mtx::events::RoomEvent<msg::File>;
-        using ImageEvent  = mtx::events::RoomEvent<msg::Image>;
-        using NoticeEvent = mtx::events::RoomEvent<msg::Notice>;
-        using TextEvent   = mtx::events::RoomEvent<msg::Text>;
-        using VideoEvent  = mtx::events::RoomEvent<msg::Video>;
+        using namespace mtx::events;
+
+        using AudioEvent  = RoomEvent<msg::Audio>;
+        using EmoteEvent  = RoomEvent<msg::Emote>;
+        using FileEvent   = RoomEvent<msg::File>;
+        using ImageEvent  = RoomEvent<msg::Image>;
+        using NoticeEvent = RoomEvent<msg::Notice>;
+        using TextEvent   = RoomEvent<msg::Text>;
+        using VideoEvent  = RoomEvent<msg::Video>;
 
-        if (mpark::holds_alternative<mtx::events::RedactionEvent<msg::Redaction>>(event)) {
-                auto redaction_event =
-                  mpark::get<mtx::events::RedactionEvent<msg::Redaction>>(event);
-                const auto event_id = QString::fromStdString(redaction_event.redacts);
+        if (mpark::holds_alternative<RedactionEvent<msg::Redaction>>(event)) {
+                auto redaction_event = mpark::get<RedactionEvent<msg::Redaction>>(event);
+                const auto event_id  = QString::fromStdString(redaction_event.redacts);
 
                 QTimer::singleShot(0, this, [event_id, this]() {
                         if (eventIds_.contains(event_id))
@@ -255,35 +256,88 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &
                 });
 
                 return nullptr;
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Audio>>(event)) {
-                auto audio = mpark::get<mtx::events::RoomEvent<msg::Audio>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Audio>>(event)) {
+                auto audio = mpark::get<RoomEvent<msg::Audio>>(event);
                 return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Emote>>(event)) {
-                auto emote = mpark::get<mtx::events::RoomEvent<msg::Emote>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Emote>>(event)) {
+                auto emote = mpark::get<RoomEvent<msg::Emote>>(event);
                 return processMessageEvent<EmoteEvent>(emote, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::File>>(event)) {
-                auto file = mpark::get<mtx::events::RoomEvent<msg::File>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::File>>(event)) {
+                auto file = mpark::get<RoomEvent<msg::File>>(event);
                 return processMessageEvent<FileEvent, FileItem>(file, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Image>>(event)) {
-                auto image = mpark::get<mtx::events::RoomEvent<msg::Image>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Image>>(event)) {
+                auto image = mpark::get<RoomEvent<msg::Image>>(event);
                 return processMessageEvent<ImageEvent, ImageItem>(image, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Notice>>(event)) {
-                auto notice = mpark::get<mtx::events::RoomEvent<msg::Notice>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Notice>>(event)) {
+                auto notice = mpark::get<RoomEvent<msg::Notice>>(event);
                 return processMessageEvent<NoticeEvent>(notice, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Text>>(event)) {
-                auto text = mpark::get<mtx::events::RoomEvent<msg::Text>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Text>>(event)) {
+                auto text = mpark::get<RoomEvent<msg::Text>>(event);
                 return processMessageEvent<TextEvent>(text, direction);
-        } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Video>>(event)) {
-                auto video = mpark::get<mtx::events::RoomEvent<msg::Video>>(event);
+        } else if (mpark::holds_alternative<RoomEvent<msg::Video>>(event)) {
+                auto video = mpark::get<RoomEvent<msg::Video>>(event);
                 return processMessageEvent<VideoEvent, VideoItem>(video, direction);
-        } else if (mpark::holds_alternative<mtx::events::Sticker>(event)) {
-                return processMessageEvent<mtx::events::Sticker, StickerItem>(
-                  mpark::get<mtx::events::Sticker>(event), direction);
+        } else if (mpark::holds_alternative<Sticker>(event)) {
+                return processMessageEvent<Sticker, StickerItem>(mpark::get<Sticker>(event),
+                                                                 direction);
+        } else if (mpark::holds_alternative<EncryptedEvent<msg::Encrypted>>(event)) {
+                auto decrypted =
+                  parseEncryptedEvent(mpark::get<EncryptedEvent<msg::Encrypted>>(event));
+                return parseMessageEvent(decrypted, direction);
         }
 
         return nullptr;
 }
 
+TimelineEvent
+TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
+{
+        MegolmSessionIndex index;
+        index.room_id    = room_id_.toStdString();
+        index.session_id = e.content.session_id;
+        index.sender_key = e.content.sender_key;
+
+        mtx::events::RoomEvent<mtx::events::msg::Text> dummy;
+        dummy.origin_server_ts = e.origin_server_ts;
+        dummy.event_id         = e.event_id;
+        dummy.sender           = e.sender;
+        dummy.content.body     = "-- Encrypted Event (No keys found for decryption) --";
+
+        if (!cache::client()->inboundMegolmSessionExists(index)) {
+                log::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
+                                    index.room_id,
+                                    index.session_id,
+                                    e.sender);
+                // TODO: request megolm session_id & session_key from the sender.
+                return dummy;
+        }
+
+        auto session = cache::client()->getInboundMegolmSession(index);
+        auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
+
+        const auto msg_str = std::string((char *)res.data.data(), res.data.size());
+
+        // Add missing fields for the event.
+        json body                = json::parse(msg_str);
+        body["event_id"]         = e.event_id;
+        body["sender"]           = e.sender;
+        body["origin_server_ts"] = e.origin_server_ts;
+
+        log::crypto()->info("decrypted data: \n {}", body.dump(2));
+
+        json event_array = json::array();
+        event_array.push_back(body);
+
+        std::vector<TimelineEvent> events;
+        mtx::responses::utils::parse_timeline_events(event_array, events);
+
+        if (events.size() == 1)
+                return events.at(0);
+
+        dummy.content.body = "-- Encrypted Event (Unknown event type) --";
+        return dummy;
+}
+
 void
 TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events)
 {
-- 
GitLab