diff --git a/CMakeLists.txt b/CMakeLists.txt index eedf9a69..ef20be39 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 d6bab7e5..5f9b48ca 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 afc7a148..994a6da7 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -17,13 +17,16 @@ #pragma once -#include #include #include + #include #include #include #include +#include +#include + 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 outbound_sessions; + std::map group_inbound_sessions; + std::map group_outbound_sessions; + std::map 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 &devices); + std::vector 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 e99e94ba..d8582993 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -29,8 +29,7 @@ #include "Cache.h" #include "CommunitiesList.h" #include "Community.h" - -#include +#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 &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 c301d80d..bdbd3e2c 100644 --- a/include/Logging.hpp +++ b/include/Logging.hpp @@ -15,4 +15,7 @@ net(); std::shared_ptr db(); + +std::shared_ptr +crypto(); } diff --git a/include/MainWindow.h b/include/MainWindow.h index f0fa9a08..b068e8f6 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 832d6cad..7ea5e0b7 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); +Q_DECLARE_METATYPE(std::vector) 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 00000000..2f7b1d64 --- /dev/null +++ b/include/Olm.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +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 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>(); +} + +mtx::crypto::OlmClient * +client(); + +void +handle_to_device_messages(const std::vector &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 30af97fb..88857222 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 &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 2a555425..150990b7 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>; using Receipts = std::map>; @@ -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(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 lock(session_storage.group_inbound_mtx); + session_storage.group_inbound_sessions[key] = std::move(session); + } +} + +OlmInboundGroupSession * +Cache::getInboundMegolmSession(const MegolmSessionIndex &index) +{ + std::unique_lock 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 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(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 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 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 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(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 lock(session_storage.outbound_mtx); + session_storage.outbound_sessions[curve25519] = std::move(session); + } +} + +bool +Cache::outboundOlmSessionsExists(const std::string &curve25519) noexcept +{ + std::unique_lock 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 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(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(); + + auto session = + unpickle(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(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 64ce69d6..a5a6a8c0 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, QWidget *parent) : QWidget(parent) @@ -612,6 +617,9 @@ ChatPage::ChatPage(QSharedPointer 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(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(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 &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(err->status_code)); + return; + } + + olm::client()->mark_keys_as_published(); + }); + } + } +} diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc index 8ccd5e9d..49affcb7 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 c6c1c502..77e61e09 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -4,9 +4,10 @@ #include namespace { -std::shared_ptr db_logger = nullptr; -std::shared_ptr net_logger = nullptr; -std::shared_ptr main_logger = nullptr; +std::shared_ptr db_logger = nullptr; +std::shared_ptr net_logger = nullptr; +std::shared_ptr crypto_logger = nullptr; +std::shared_ptr 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("net", std::begin(sinks), std::end(sinks)); main_logger = std::make_shared("main", std::begin(sinks), std::end(sinks)); db_logger = std::make_shared("db", std::begin(sinks), std::end(sinks)); + crypto_logger = + std::make_shared("crypto", std::begin(sinks), std::end(sinks)); } std::shared_ptr @@ -47,4 +50,10 @@ db() { return db_logger; } + +std::shared_ptr +crypto() +{ + return crypto_logger; +} } diff --git a/src/MainWindow.cc b/src/MainWindow.cc index 9ba8b28e..cca51f03 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 0eb4658a..d4ab8e33 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -3,7 +3,7 @@ #include namespace { -auto v2_client_ = std::make_shared("matrix.org"); +auto v2_client_ = std::make_shared(); } 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 00000000..769b0234 --- /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(); +} + +namespace olm { + +mtx::crypto::OlmClient * +client() +{ + return client_.get(); +} + +void +handle_to_device_messages(const std::vector &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 d3ed2e66..4891f746 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 1df8d0c9..13a712f4 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 5ef390a9..9baa1f4a 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; - using EmoteEvent = mtx::events::RoomEvent; - using FileEvent = mtx::events::RoomEvent; - using ImageEvent = mtx::events::RoomEvent; - using NoticeEvent = mtx::events::RoomEvent; - using TextEvent = mtx::events::RoomEvent; - using VideoEvent = mtx::events::RoomEvent; + using namespace mtx::events; - if (mpark::holds_alternative>(event)) { - auto redaction_event = - mpark::get>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); + using AudioEvent = RoomEvent; + using EmoteEvent = RoomEvent; + using FileEvent = RoomEvent; + using ImageEvent = RoomEvent; + using NoticeEvent = RoomEvent; + using TextEvent = RoomEvent; + using VideoEvent = RoomEvent; + + if (mpark::holds_alternative>(event)) { + auto redaction_event = mpark::get>(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>(event)) { - auto audio = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto audio = mpark::get>(event); return processMessageEvent(audio, direction); - } else if (mpark::holds_alternative>(event)) { - auto emote = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto emote = mpark::get>(event); return processMessageEvent(emote, direction); - } else if (mpark::holds_alternative>(event)) { - auto file = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto file = mpark::get>(event); return processMessageEvent(file, direction); - } else if (mpark::holds_alternative>(event)) { - auto image = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto image = mpark::get>(event); return processMessageEvent(image, direction); - } else if (mpark::holds_alternative>(event)) { - auto notice = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto notice = mpark::get>(event); return processMessageEvent(notice, direction); - } else if (mpark::holds_alternative>(event)) { - auto text = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto text = mpark::get>(event); return processMessageEvent(text, direction); - } else if (mpark::holds_alternative>(event)) { - auto video = mpark::get>(event); + } else if (mpark::holds_alternative>(event)) { + auto video = mpark::get>(event); return processMessageEvent(video, direction); - } else if (mpark::holds_alternative(event)) { - return processMessageEvent( - mpark::get(event), direction); + } else if (mpark::holds_alternative(event)) { + return processMessageEvent(mpark::get(event), + direction); + } else if (mpark::holds_alternative>(event)) { + auto decrypted = + parseEncryptedEvent(mpark::get>(event)); + return parseMessageEvent(decrypted, direction); } return nullptr; } +TimelineEvent +TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent &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 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 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 &events) {