Add support for displaying decrypted messages

This commit is contained in:
Konstantinos Sideris 2018-06-10 20:03:45 +03:00
parent b89257a34b
commit 626c680911
19 changed files with 869 additions and 99 deletions

View File

@ -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

2
deps/CMakeLists.txt vendored
View File

@ -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)

View File

@ -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_;
};

View File

@ -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)

View File

@ -15,4 +15,7 @@ net();
std::shared_ptr<spdlog::logger>
db();
std::shared_ptr<spdlog::logger>
crypto();
}

View File

@ -59,7 +59,6 @@ class MainWindow : public QMainWindow
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
static MainWindow *instance() { return instance_; };
void saveCurrentWindowSize();

View File

@ -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

65
include/Olm.hpp Normal file
View File

@ -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

View File

@ -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,

View File

@ -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)
{

View File

@ -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();
});
}
}
}

View File

@ -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(

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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

139
src/Olm.cpp Normal file
View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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;
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);
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<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)
{